@verdaccio/ui-components 3.0.0-next-7.4 → 3.0.0-next-7.6
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/CHANGELOG.md +12 -0
- package/build/AppTest/AppRoute.js +34 -0
- package/build/AppTest/pages/Front/Home.js +9 -0
- package/build/AppTest/pages/Front/index.js +14 -0
- package/build/AppTest/pages/Version/Version.js +34 -0
- package/build/AppTest/pages/Version/index.js +14 -0
- package/build/Theme/theme.js +1 -1
- package/build/components/Engines/Engines.js +1 -1
- package/build/components/Forbidden/Forbidden.js +75 -0
- package/build/components/Forbidden/index.js +14 -0
- package/build/components/Install/InstallListItem.js +7 -7
- package/build/components/NotFound/NotFound.js +16 -14
- package/build/components/Repository/Repository.js +9 -9
- package/build/components/Versions/HistoryList.js +26 -22
- package/build/components/Versions/Versions.js +41 -10
- package/build/index.js +8 -0
- package/build/sections/Detail/Tabs.js +1 -3
- package/build/src/AppTest/App.stories.d.ts +8 -0
- package/build/src/AppTest/AppRoute.d.ts +3 -0
- package/build/src/AppTest/pages/Front/Home.d.ts +2 -0
- package/build/src/AppTest/pages/Front/index.d.ts +1 -0
- package/build/src/AppTest/pages/Version/Version.d.ts +3 -0
- package/build/src/AppTest/pages/Version/index.d.ts +1 -0
- package/build/src/components/Forbidden/Forbidden.d.ts +3 -0
- package/build/src/components/Forbidden/Forbidden.test.d.ts +1 -0
- package/build/src/components/Forbidden/index.d.ts +1 -0
- package/build/src/index.d.ts +1 -0
- package/build/src/sections/Detail/Detail.stories.d.ts +1 -0
- package/build/src/store/models/manifest.d.ts +11 -0
- package/build/store/api.js +12 -6
- package/build/store/models/manifest.js +20 -2
- package/package.json +3 -1
- package/src/AppTest/App.stories.tsx +32 -0
- package/src/AppTest/AppRoute.tsx +42 -0
- package/src/AppTest/pages/Front/Home.ts +3 -0
- package/src/AppTest/pages/Front/index.ts +1 -0
- package/src/AppTest/pages/Version/Version.tsx +28 -0
- package/src/AppTest/pages/Version/index.ts +1 -0
- package/src/Theme/theme.ts +1 -1
- package/src/components/Engines/Engines.tsx +1 -1
- package/src/components/Forbidden/Forbidden.test.tsx +38 -0
- package/src/components/Forbidden/Forbidden.tsx +56 -0
- package/src/components/Forbidden/index.ts +1 -0
- package/src/components/Install/InstallListItem.tsx +1 -0
- package/src/components/NotFound/NotFound.tsx +6 -5
- package/src/components/NotFound/Notfound.test.tsx +6 -3
- package/src/components/Repository/Repository.tsx +1 -3
- package/src/components/Versions/HistoryList.tsx +2 -2
- package/src/components/Versions/Versions.test.tsx +24 -8
- package/src/components/Versions/Versions.tsx +46 -18
- package/src/index.ts +1 -0
- package/src/sections/Detail/Detail.stories.tsx +10 -0
- package/src/sections/Detail/Tabs.tsx +1 -7
- package/src/store/api.test.ts +3 -2
- package/src/store/api.ts +13 -6
- package/src/store/models/manifest.ts +19 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
/* eslint-disable verdaccio/jsx-no-style */
|
|
1
2
|
import styled from '@emotion/styled';
|
|
3
|
+
import FolderOffIcon from '@mui/icons-material/FolderOff';
|
|
2
4
|
import Box from '@mui/material/Box';
|
|
3
5
|
import Button from '@mui/material/Button';
|
|
4
6
|
import React, { useCallback } from 'react';
|
|
@@ -7,8 +9,6 @@ import { useHistory } from 'react-router-dom';
|
|
|
7
9
|
|
|
8
10
|
import { Theme } from '../../';
|
|
9
11
|
import Heading from '../Heading';
|
|
10
|
-
// @ts-ignore
|
|
11
|
-
import PackageImg from './img/package.svg';
|
|
12
12
|
|
|
13
13
|
const NotFound: React.FC = () => {
|
|
14
14
|
const history = useHistory();
|
|
@@ -28,7 +28,9 @@ const NotFound: React.FC = () => {
|
|
|
28
28
|
justifyContent="center"
|
|
29
29
|
p={2}
|
|
30
30
|
>
|
|
31
|
-
<
|
|
31
|
+
<Container>
|
|
32
|
+
<FolderOffIcon color="primary" style={{ fontSize: 236 }} />
|
|
33
|
+
</Container>
|
|
32
34
|
<StyledHeading className="not-found-text" variant="h4">
|
|
33
35
|
{t('error.404.sorry-we-could-not-find-it')}
|
|
34
36
|
</StyledHeading>
|
|
@@ -41,8 +43,7 @@ const NotFound: React.FC = () => {
|
|
|
41
43
|
|
|
42
44
|
export default NotFound;
|
|
43
45
|
|
|
44
|
-
const
|
|
45
|
-
width: '150px',
|
|
46
|
+
const Container = styled('div')({
|
|
46
47
|
margin: '0 auto',
|
|
47
48
|
});
|
|
48
49
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { MemoryRouter } from 'react-router';
|
|
3
3
|
|
|
4
|
-
import { fireEvent, render } from '../../test/test-react-testing-library';
|
|
4
|
+
import { fireEvent, render, screen } from '../../test/test-react-testing-library';
|
|
5
5
|
import NotFound from './NotFound';
|
|
6
6
|
|
|
7
7
|
const mockHistory = jest.fn();
|
|
@@ -14,12 +14,15 @@ jest.mock('react-router-dom', () => ({
|
|
|
14
14
|
|
|
15
15
|
describe('<NotFound /> component', () => {
|
|
16
16
|
test('should load the component in default state', () => {
|
|
17
|
-
|
|
17
|
+
render(
|
|
18
18
|
<MemoryRouter>
|
|
19
19
|
<NotFound />
|
|
20
20
|
</MemoryRouter>
|
|
21
21
|
);
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
expect(screen.getByTestId('FolderOffIcon')).toBeInTheDocument();
|
|
24
|
+
expect(screen.getByText('button.go-to-the-home-page')).toBeInTheDocument();
|
|
25
|
+
expect(screen.getByText('error.404.sorry-we-could-not-find-it')).toBeInTheDocument();
|
|
23
26
|
});
|
|
24
27
|
|
|
25
28
|
test('go to Home Page button click', async () => {
|
|
@@ -40,9 +40,7 @@ const RepositoryListItemText = styled(ListItemText)({
|
|
|
40
40
|
const RepositoryAvatar = styled(Avatar)({
|
|
41
41
|
borderRadius: '0px',
|
|
42
42
|
padding: '0',
|
|
43
|
-
|
|
44
|
-
backgroundColor: 'transparent',
|
|
45
|
-
},
|
|
43
|
+
backgroundColor: 'transparent',
|
|
46
44
|
});
|
|
47
45
|
|
|
48
46
|
const Repository: React.FC<{ packageMeta: any }> = ({ packageMeta }) => {
|
|
@@ -44,7 +44,7 @@ const VersionsHistoryList: React.FC<Props> = ({ versions, packageName, time }) =
|
|
|
44
44
|
<Link to={`/-/web/detail/${packageName}/v/${version}`} variant="caption">
|
|
45
45
|
<ListItemText disableTypography={false} primary={version}></ListItemText>
|
|
46
46
|
</Link>
|
|
47
|
-
{typeof versions[version]
|
|
47
|
+
{typeof versions[version]?.deprecated === 'string' ? (
|
|
48
48
|
<Chip
|
|
49
49
|
color="warning"
|
|
50
50
|
data-testid="deprecated-badge"
|
|
@@ -55,7 +55,7 @@ const VersionsHistoryList: React.FC<Props> = ({ versions, packageName, time }) =
|
|
|
55
55
|
/>
|
|
56
56
|
) : null}
|
|
57
57
|
<Spacer />
|
|
58
|
-
<ListItemText title={utils.formatDate(time[version])}>
|
|
58
|
+
<ListItemText data-testid={`version-list-text`} title={utils.formatDate(time[version])}>
|
|
59
59
|
{time[version]
|
|
60
60
|
? utils.formatDateDistance(time[version])
|
|
61
61
|
: t('versions.not-available')}
|
|
@@ -2,24 +2,31 @@
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { MemoryRouter } from 'react-router-dom';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { fireEvent, render, screen } from '../../test/test-react-testing-library';
|
|
6
6
|
import Versions, { Props } from './Versions';
|
|
7
7
|
import data from './__partials__/data.json';
|
|
8
8
|
import dataDeprecated from './__partials__/deprecated-versions.json';
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const VersionsComponent: React.FC<Props> = (props) => (
|
|
11
11
|
<MemoryRouter>
|
|
12
12
|
<Versions {...props} />
|
|
13
13
|
</MemoryRouter>
|
|
14
14
|
);
|
|
15
15
|
|
|
16
|
+
jest.mock('lodash/debounce', () =>
|
|
17
|
+
jest.fn((fn) => {
|
|
18
|
+
fn.cancel = jest.fn();
|
|
19
|
+
return fn;
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
|
|
16
23
|
describe('<Version /> component', () => {
|
|
17
24
|
afterEach(() => {
|
|
18
|
-
|
|
25
|
+
jest.clearAllMocks();
|
|
19
26
|
});
|
|
20
27
|
|
|
21
28
|
test('should render versions', () => {
|
|
22
|
-
const { getByText } = render(<
|
|
29
|
+
const { getByText } = render(<VersionsComponent packageMeta={data} packageName={'foo'} />);
|
|
23
30
|
|
|
24
31
|
expect(getByText('versions.version-history')).toBeTruthy();
|
|
25
32
|
expect(getByText('versions.current-tags')).toBeTruthy();
|
|
@@ -31,17 +38,26 @@ describe('<Version /> component', () => {
|
|
|
31
38
|
expect(screen.queryByTestId('deprecated-badge')).toBeInTheDocument();
|
|
32
39
|
});
|
|
33
40
|
|
|
41
|
+
test('should filter by version', () => {
|
|
42
|
+
render(<VersionsComponent packageMeta={data} packageName={'foo'} />);
|
|
43
|
+
expect(screen.getByText('versions.version-history')).toBeTruthy();
|
|
44
|
+
expect(screen.getByText('versions.current-tags')).toBeTruthy();
|
|
45
|
+
expect(screen.queryAllByTestId('version-list-text')).toHaveLength(65);
|
|
46
|
+
fireEvent.change(screen.getByRole('textbox'), { target: { value: '2.3.0' } });
|
|
47
|
+
expect(screen.queryAllByTestId('version-list-text')).toHaveLength(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
34
50
|
test('should not render versions', () => {
|
|
35
|
-
|
|
51
|
+
render(<VersionsComponent packageMeta={{}} packageName={'foo'} />);
|
|
36
52
|
|
|
37
|
-
expect(queryByText('versions.version-history')).toBeFalsy();
|
|
38
|
-
expect(queryByText('versions.current-tags')).toBeFalsy();
|
|
53
|
+
expect(screen.queryByText('versions.version-history')).toBeFalsy();
|
|
54
|
+
expect(screen.queryByText('versions.current-tags')).toBeFalsy();
|
|
39
55
|
});
|
|
40
56
|
|
|
41
57
|
test('should render versions deprecated settings', () => {
|
|
42
58
|
window.__VERDACCIO_BASENAME_UI_OPTIONS.hideDeprecatedVersions = true;
|
|
43
59
|
const { getByText } = render(
|
|
44
|
-
<
|
|
60
|
+
<VersionsComponent packageMeta={dataDeprecated} packageName={'foo'} />
|
|
45
61
|
);
|
|
46
62
|
expect(getByText('versions.hide-deprecated')).toBeTruthy();
|
|
47
63
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import Alert from '@mui/material/Alert';
|
|
2
|
+
import TextField from '@mui/material/TextField';
|
|
2
3
|
import Typography from '@mui/material/Typography';
|
|
3
4
|
import { useTheme } from '@mui/styles';
|
|
4
|
-
import
|
|
5
|
+
import debounce from 'lodash/debounce';
|
|
6
|
+
import React, { useState } from 'react';
|
|
5
7
|
import { useTranslation } from 'react-i18next';
|
|
8
|
+
import semver from 'semver';
|
|
6
9
|
|
|
7
10
|
import { useConfig } from '../../providers';
|
|
8
11
|
import VersionsHistoryList from './HistoryList';
|
|
@@ -14,16 +17,33 @@ const Versions: React.FC<Props> = ({ packageMeta, packageName }) => {
|
|
|
14
17
|
const { t } = useTranslation();
|
|
15
18
|
const { configOptions } = useConfig();
|
|
16
19
|
const theme = useTheme();
|
|
20
|
+
const { versions = {}, time = {}, ['dist-tags']: distTags = {} } = packageMeta;
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
const [packageVersions, setPackageVersions] = useState(versions);
|
|
23
|
+
if (!packageMeta || Object.keys(packageMeta).length === 0) {
|
|
19
24
|
return null;
|
|
20
25
|
}
|
|
26
|
+
const hideDeprecatedVersions = configOptions.hideDeprecatedVersions;
|
|
27
|
+
const hasDistTags = distTags && Object.keys(distTags).length > 0 && packageName;
|
|
28
|
+
const hasVersionHistory =
|
|
29
|
+
packageVersions && Object.keys(packageVersions).length > 0 && packageName;
|
|
21
30
|
|
|
22
|
-
const
|
|
31
|
+
const filterVersions = (textSearch) => {
|
|
32
|
+
const filteredVersions = Object.keys(versions).reduce((acc, version) => {
|
|
33
|
+
if (textSearch !== '') {
|
|
34
|
+
if (typeof versions[version] !== 'undefined') {
|
|
35
|
+
if (semver.satisfies(version, textSearch, { includePrerelease: true, loose: true })) {
|
|
36
|
+
acc[version] = versions[version];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
acc[version] = versions[version];
|
|
41
|
+
}
|
|
42
|
+
return acc;
|
|
43
|
+
}, {});
|
|
23
44
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const hideDeprecatedVersions = configOptions.hideDeprecatedVersions;
|
|
45
|
+
setPackageVersions(filteredVersions);
|
|
46
|
+
};
|
|
27
47
|
|
|
28
48
|
return (
|
|
29
49
|
<>
|
|
@@ -33,20 +53,28 @@ const Versions: React.FC<Props> = ({ packageMeta, packageName }) => {
|
|
|
33
53
|
<VersionsTagList packageName={packageName} tags={distTags} time={time} />
|
|
34
54
|
</>
|
|
35
55
|
) : null}
|
|
56
|
+
<>
|
|
57
|
+
<Typography variant="subtitle1">{t('versions.version-history')}</Typography>
|
|
58
|
+
<TextField
|
|
59
|
+
helperText={t('versions.search.placeholder')}
|
|
60
|
+
onChange={debounce((e) => {
|
|
61
|
+
filterVersions(e.target.value);
|
|
62
|
+
}, 200)}
|
|
63
|
+
size="small"
|
|
64
|
+
variant="standard"
|
|
65
|
+
/>
|
|
66
|
+
</>
|
|
36
67
|
{hasVersionHistory ? (
|
|
37
68
|
<>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
)}
|
|
48
|
-
<VersionsHistoryList packageName={packageName} time={time} versions={versions} />
|
|
49
|
-
</>
|
|
69
|
+
{hideDeprecatedVersions === true && (
|
|
70
|
+
<Alert
|
|
71
|
+
severity="info"
|
|
72
|
+
sx={{ marginTop: theme.spacing(1), marginBottom: theme.spacing(1) }}
|
|
73
|
+
>
|
|
74
|
+
{t('versions.hide-deprecated')}
|
|
75
|
+
</Alert>
|
|
76
|
+
)}
|
|
77
|
+
<VersionsHistoryList packageName={packageName} time={time} versions={packageVersions} />
|
|
50
78
|
</>
|
|
51
79
|
) : null}
|
|
52
80
|
</>
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ export { default as Label } from './components/Label';
|
|
|
22
22
|
export { default as Logo } from './components/Logo';
|
|
23
23
|
export { default as MenuItem } from './components/MenuItem';
|
|
24
24
|
export { default as NotFound } from './components/NotFound';
|
|
25
|
+
export { default as Forbidden } from './components/Forbidden';
|
|
25
26
|
export { default as LoginDialog } from './components/LoginDialog';
|
|
26
27
|
export { default as Search } from './components/Search';
|
|
27
28
|
export { default as HeaderInfoDialog } from './components/HeaderInfoDialog';
|
|
@@ -27,3 +27,13 @@ export const DetailJquery: any = () => (
|
|
|
27
27
|
</Route>
|
|
28
28
|
</MemoryRouter>
|
|
29
29
|
);
|
|
30
|
+
|
|
31
|
+
export const DetailForbidden: any = () => (
|
|
32
|
+
<MemoryRouter initialEntries={[`/-/web/detail/JSONStream`]}>
|
|
33
|
+
<Route exact={true} path="/-/web/detail/:package">
|
|
34
|
+
<VersionProvider>
|
|
35
|
+
<Detail />
|
|
36
|
+
</VersionProvider>
|
|
37
|
+
</Route>
|
|
38
|
+
</MemoryRouter>
|
|
39
|
+
);
|
|
@@ -14,13 +14,7 @@ interface Props {
|
|
|
14
14
|
const DetailContainerTabs: React.FC<Props> = ({ tabPosition, onChange }) => {
|
|
15
15
|
const { t } = useTranslation();
|
|
16
16
|
return (
|
|
17
|
-
<Tabs
|
|
18
|
-
color={'primary'}
|
|
19
|
-
indicatorColor={'primary'}
|
|
20
|
-
onChange={onChange}
|
|
21
|
-
value={tabPosition}
|
|
22
|
-
variant={'fullWidth'}
|
|
23
|
-
>
|
|
17
|
+
<Tabs onChange={onChange} value={tabPosition} variant={'fullWidth'}>
|
|
24
18
|
<Tab data-testid={'readme-tab'} id={'readme-tab'} label={t('tab.readme')} />
|
|
25
19
|
<Tab data-testid={'dependencies-tab'} id={'dependencies-tab'} label={t('tab.dependencies')} />
|
|
26
20
|
<Tab data-testid={'versions-tab'} id={'versions-tab'} label={t('tab.versions')} />
|
package/src/store/api.test.ts
CHANGED
|
@@ -14,7 +14,8 @@ describe('api', () => {
|
|
|
14
14
|
|
|
15
15
|
const handled = await handleResponseType(response);
|
|
16
16
|
|
|
17
|
-
expect(handled).
|
|
17
|
+
expect(handled[0]).toBeFalsy();
|
|
18
|
+
expect(handled[1]).toBeDefined();
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
test('should test tgz scenario', async () => {
|
|
@@ -123,7 +124,7 @@ describe('api', () => {
|
|
|
123
124
|
})
|
|
124
125
|
);
|
|
125
126
|
|
|
126
|
-
await expect(api.request('/resource')).rejects.toThrow(new Error('
|
|
127
|
+
await expect(api.request('/resource')).rejects.toThrow(new Error('Unknown error'));
|
|
127
128
|
});
|
|
128
129
|
|
|
129
130
|
test('when api returns an error 5.x.x', async () => {
|
package/src/store/api.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import storage from './storage';
|
|
2
2
|
|
|
3
|
+
class CustomError extends Error {
|
|
4
|
+
// @ts-ignore
|
|
5
|
+
code: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
/**
|
|
4
9
|
* Handles response according to content type
|
|
5
10
|
* @param {object} response
|
|
@@ -25,7 +30,8 @@ export function handleResponseType(response: Response): Promise<[boolean, any]>
|
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
// error handling
|
|
34
|
+
return Promise.all([response.ok, response]);
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
const AuthHeader = 'Authorization';
|
|
@@ -55,12 +61,13 @@ class API {
|
|
|
55
61
|
})
|
|
56
62
|
.then(handleResponseType)
|
|
57
63
|
.then((response) => {
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
const [ok, data] = response;
|
|
65
|
+
if (ok === true) {
|
|
66
|
+
resolve(data);
|
|
60
67
|
} else {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
reject(
|
|
68
|
+
const error = new CustomError(data?.statusText ?? 'Unknown error');
|
|
69
|
+
error.code = data?.status ?? 500;
|
|
70
|
+
reject(error);
|
|
64
71
|
}
|
|
65
72
|
})
|
|
66
73
|
.catch((error) => {
|
|
@@ -33,6 +33,18 @@ export const manifest = createModel<RootModel>()({
|
|
|
33
33
|
return {
|
|
34
34
|
...state,
|
|
35
35
|
hasNotBeenFound: true,
|
|
36
|
+
forbidden: false,
|
|
37
|
+
manifest: undefined,
|
|
38
|
+
packageName: undefined,
|
|
39
|
+
packageVersion: undefined,
|
|
40
|
+
readme: undefined,
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
forbidden(state) {
|
|
44
|
+
return {
|
|
45
|
+
...state,
|
|
46
|
+
forbidden: true,
|
|
47
|
+
hasNotBeenFound: false,
|
|
36
48
|
manifest: undefined,
|
|
37
49
|
packageName: undefined,
|
|
38
50
|
packageVersion: undefined,
|
|
@@ -50,6 +62,7 @@ export const manifest = createModel<RootModel>()({
|
|
|
50
62
|
...state,
|
|
51
63
|
isError: true,
|
|
52
64
|
hasNotBeenFound: false,
|
|
65
|
+
forbidden: false,
|
|
53
66
|
manifest: undefined,
|
|
54
67
|
packageName: undefined,
|
|
55
68
|
packageVersion: undefined,
|
|
@@ -64,6 +77,7 @@ export const manifest = createModel<RootModel>()({
|
|
|
64
77
|
packageVersion,
|
|
65
78
|
readme,
|
|
66
79
|
hasNotBeenFound: false,
|
|
80
|
+
forbidden: false,
|
|
67
81
|
};
|
|
68
82
|
},
|
|
69
83
|
},
|
|
@@ -91,7 +105,11 @@ export const manifest = createModel<RootModel>()({
|
|
|
91
105
|
);
|
|
92
106
|
dispatch.manifest.saveManifest({ packageName, packageVersion, manifest, readme });
|
|
93
107
|
} catch (error: any) {
|
|
94
|
-
|
|
108
|
+
if (error.code === 404) {
|
|
109
|
+
dispatch.manifest.notFound();
|
|
110
|
+
} else {
|
|
111
|
+
dispatch.manifest.forbidden();
|
|
112
|
+
}
|
|
95
113
|
}
|
|
96
114
|
},
|
|
97
115
|
}),
|