@stoplight/elements-dev-portal 3.0.11 → 3.0.12-beta-0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/main.js +6 -0
- package/.storybook/manager.js +1 -0
- package/.storybook/preview.jsx +46 -0
- package/dist/LICENSE +190 -0
- package/dist/README.md +22 -0
- package/{index.esm.js → dist/index.esm.js} +8 -2
- package/{index.js → dist/index.js} +8 -2
- package/{index.mjs → dist/index.mjs} +8 -2
- package/dist/package.json +51 -0
- package/dist/version.d.ts +1 -0
- package/{web-components.min.js → dist/web-components.min.js} +1 -1
- package/jest.config.js +10 -0
- package/package.json +71 -17
- package/src/__fixtures__/branches.json +28 -0
- package/src/__fixtures__/node-content.json +257 -0
- package/src/__fixtures__/table-of-contents.json +144 -0
- package/src/components/BranchSelector/BranchSelector.spec.tsx +61 -0
- package/src/components/BranchSelector/BranchSelector.stories.tsx +41 -0
- package/src/components/BranchSelector/BranchSelector.tsx +50 -0
- package/src/components/BranchSelector/index.tsx +1 -0
- package/src/components/DevPortalProvider/index.tsx +25 -0
- package/src/components/Forbidden.tsx +11 -0
- package/src/components/Loading.tsx +9 -0
- package/src/components/NodeContent/NodeContent.spec.tsx +128 -0
- package/src/components/NodeContent/NodeContent.stories.tsx +60 -0
- package/src/components/NodeContent/NodeContent.tsx +235 -0
- package/src/components/NodeContent/index.tsx +1 -0
- package/src/components/NotFound.tsx +11 -0
- package/src/components/Search/Search.stories.tsx +151 -0
- package/src/components/Search/Search.tsx +161 -0
- package/src/components/Search/SearchOverlay.tsx +88 -0
- package/src/components/Search/index.tsx +1 -0
- package/src/components/TableOfContents/TableOfContents.stories.tsx +68 -0
- package/src/components/TableOfContents/TableOfContents.tsx +54 -0
- package/src/components/TableOfContents/index.tsx +1 -0
- package/src/components/UpgradeToStarter.tsx +22 -0
- package/src/consts.ts +32 -0
- package/src/containers/StoplightProject.spec.tsx +78 -0
- package/src/containers/StoplightProject.stories.tsx +28 -0
- package/src/containers/StoplightProject.tsx +269 -0
- package/src/handlers/__tests__/getBranches.test.ts +30 -0
- package/src/handlers/__tests__/getNodeContent.test.ts +35 -0
- package/src/handlers/__tests__/getNodes.test.ts +38 -0
- package/src/handlers/__tests__/getTableOfContents.test.ts +34 -0
- package/src/handlers/__tests__/getWorkspace.test.ts +30 -0
- package/src/handlers/getBranches.ts +27 -0
- package/src/handlers/getNodeContent.ts +53 -0
- package/src/handlers/getNodes.ts +69 -0
- package/src/handlers/getTableOfContents.ts +30 -0
- package/src/handlers/getWorkspace.ts +27 -0
- package/src/hooks/__tests__/dataFetching.spec.tsx +42 -0
- package/src/hooks/useGetBranches.ts +17 -0
- package/src/hooks/useGetNodeContent.ts +24 -0
- package/src/hooks/useGetNodes.ts +34 -0
- package/src/hooks/useGetTableOfContents.ts +17 -0
- package/src/hooks/useGetWorkspace.tsx +13 -0
- package/src/index.ts +25 -0
- package/src/styles.css +1 -0
- package/src/types.ts +85 -0
- package/src/version.ts +2 -0
- package/src/web-components/__stories__/StoplightProject.stories.tsx +33 -0
- package/src/web-components/components.ts +19 -0
- package/src/web-components/index.ts +3 -0
- package/tsconfig.build.json +18 -0
- package/tsconfig.json +7 -0
- package/web-components.config.js +1 -0
- package/version.d.ts +0 -1
- /package/{components → dist/components}/BranchSelector/BranchSelector.d.ts +0 -0
- /package/{components → dist/components}/BranchSelector/BranchSelector.spec.d.ts +0 -0
- /package/{components → dist/components}/BranchSelector/BranchSelector.stories.d.ts +0 -0
- /package/{components → dist/components}/BranchSelector/index.d.ts +0 -0
- /package/{components → dist/components}/DevPortalProvider/index.d.ts +0 -0
- /package/{components → dist/components}/Forbidden.d.ts +0 -0
- /package/{components → dist/components}/Loading.d.ts +0 -0
- /package/{components → dist/components}/NodeContent/NodeContent.d.ts +0 -0
- /package/{components → dist/components}/NodeContent/NodeContent.spec.d.ts +0 -0
- /package/{components → dist/components}/NodeContent/NodeContent.stories.d.ts +0 -0
- /package/{components → dist/components}/NodeContent/index.d.ts +0 -0
- /package/{components → dist/components}/NotFound.d.ts +0 -0
- /package/{components → dist/components}/Search/Search.d.ts +0 -0
- /package/{components → dist/components}/Search/Search.stories.d.ts +0 -0
- /package/{components → dist/components}/Search/SearchOverlay.d.ts +0 -0
- /package/{components → dist/components}/Search/index.d.ts +0 -0
- /package/{components → dist/components}/TableOfContents/TableOfContents.d.ts +0 -0
- /package/{components → dist/components}/TableOfContents/TableOfContents.stories.d.ts +0 -0
- /package/{components → dist/components}/TableOfContents/index.d.ts +0 -0
- /package/{components → dist/components}/UpgradeToStarter.d.ts +0 -0
- /package/{consts.d.ts → dist/consts.d.ts} +0 -0
- /package/{containers → dist/containers}/StoplightProject.d.ts +0 -0
- /package/{containers → dist/containers}/StoplightProject.spec.d.ts +0 -0
- /package/{containers → dist/containers}/StoplightProject.stories.d.ts +0 -0
- /package/{handlers → dist/handlers}/getBranches.d.ts +0 -0
- /package/{handlers → dist/handlers}/getNodeContent.d.ts +0 -0
- /package/{handlers → dist/handlers}/getNodes.d.ts +0 -0
- /package/{handlers → dist/handlers}/getTableOfContents.d.ts +0 -0
- /package/{handlers → dist/handlers}/getWorkspace.d.ts +0 -0
- /package/{hooks → dist/hooks}/useGetBranches.d.ts +0 -0
- /package/{hooks → dist/hooks}/useGetNodeContent.d.ts +0 -0
- /package/{hooks → dist/hooks}/useGetNodes.d.ts +0 -0
- /package/{hooks → dist/hooks}/useGetTableOfContents.d.ts +0 -0
- /package/{hooks → dist/hooks}/useGetWorkspace.d.ts +0 -0
- /package/{index.d.ts → dist/index.d.ts} +0 -0
- /package/{styles.min.css → dist/styles.min.css} +0 -0
- /package/{types.d.ts → dist/types.d.ts} +0 -0
- /package/{web-components → dist/web-components}/components.d.ts +0 -0
- /package/{web-components → dist/web-components}/index.d.ts +0 -0
- /package/{web-components.min.js.LICENSE.txt → dist/web-components.min.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CustomLinkComponent,
|
|
3
|
+
findFirstNode,
|
|
4
|
+
ReactRouterMarkdownLink,
|
|
5
|
+
RouterTypeContext,
|
|
6
|
+
RoutingProps,
|
|
7
|
+
ScrollToHashElement,
|
|
8
|
+
SidebarLayout,
|
|
9
|
+
useRouter,
|
|
10
|
+
withStyles,
|
|
11
|
+
} from '@stoplight/elements-core';
|
|
12
|
+
import * as React from 'react';
|
|
13
|
+
import {
|
|
14
|
+
Link,
|
|
15
|
+
Navigate,
|
|
16
|
+
Outlet,
|
|
17
|
+
Route,
|
|
18
|
+
Routes,
|
|
19
|
+
useInRouterContext,
|
|
20
|
+
useNavigate,
|
|
21
|
+
useOutletContext,
|
|
22
|
+
useParams,
|
|
23
|
+
} from 'react-router-dom';
|
|
24
|
+
|
|
25
|
+
import { BranchSelector } from '../components/BranchSelector';
|
|
26
|
+
import { DevPortalProvider } from '../components/DevPortalProvider';
|
|
27
|
+
import { Forbidden } from '../components/Forbidden';
|
|
28
|
+
import { Loading } from '../components/Loading';
|
|
29
|
+
import { NodeContent } from '../components/NodeContent';
|
|
30
|
+
import { NotFound } from '../components/NotFound';
|
|
31
|
+
import { TableOfContents } from '../components/TableOfContents';
|
|
32
|
+
import { UpgradeToStarter } from '../components/UpgradeToStarter';
|
|
33
|
+
import { ResponseError } from '../handlers/getNodeContent';
|
|
34
|
+
import { useGetBranches } from '../hooks/useGetBranches';
|
|
35
|
+
import { useGetNodeContent } from '../hooks/useGetNodeContent';
|
|
36
|
+
import { useGetTableOfContents } from '../hooks/useGetTableOfContents';
|
|
37
|
+
|
|
38
|
+
export interface StoplightProjectProps extends RoutingProps {
|
|
39
|
+
/**
|
|
40
|
+
* The ID of the Stoplight Project.
|
|
41
|
+
* @example "d2s6NDE1NTU"
|
|
42
|
+
*/
|
|
43
|
+
projectId: string;
|
|
44
|
+
/**
|
|
45
|
+
* If your company runs an on-premise deployment of Stoplight,
|
|
46
|
+
* set this prop to point the StoplightProject component at the URL of that instance.
|
|
47
|
+
*/
|
|
48
|
+
platformUrl?: string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Allows to hide TryIt component
|
|
52
|
+
*/
|
|
53
|
+
hideTryIt?: boolean;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Allows to hide mocking button
|
|
57
|
+
*/
|
|
58
|
+
hideMocking?: boolean;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Allows to hide export button
|
|
62
|
+
* @default false
|
|
63
|
+
*/
|
|
64
|
+
hideExport?: boolean;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Allows to the server information
|
|
68
|
+
* @default false
|
|
69
|
+
*/
|
|
70
|
+
hideServerInfo?: boolean;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Allows to hide the security schemes
|
|
74
|
+
* @default false
|
|
75
|
+
*/
|
|
76
|
+
hideSecurityInfo?: boolean;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* If set to true, all table of contents panels will be collapsed.
|
|
80
|
+
* @default false
|
|
81
|
+
*/
|
|
82
|
+
collapseTableOfContents?: boolean;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Fetch credentials policy for TryIt component
|
|
86
|
+
* For more information: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
|
|
87
|
+
* @default "omit"
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
tryItCredentialsPolicy?: 'omit' | 'include' | 'same-origin';
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* URL of a CORS proxy that will be used to send requests in TryIt.
|
|
94
|
+
* Provided url will be prepended to an URL of an actual request.
|
|
95
|
+
* @default false
|
|
96
|
+
*/
|
|
97
|
+
tryItCorsProxy?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const StoplightProjectImpl: React.FC<StoplightProjectProps> = ({ projectId, collapseTableOfContents = false }) => {
|
|
101
|
+
const { branchSlug: encodedBranchSlug = '', '*': nodeSlug = '' } = useParams<{
|
|
102
|
+
branchSlug?: string;
|
|
103
|
+
'*': string;
|
|
104
|
+
}>();
|
|
105
|
+
const branchSlug = decodeURIComponent(encodedBranchSlug);
|
|
106
|
+
const navigate = useNavigate();
|
|
107
|
+
|
|
108
|
+
const { data: tableOfContents, isFetched: isTocFetched } = useGetTableOfContents({ projectId, branchSlug });
|
|
109
|
+
const { data: branches } = useGetBranches({ projectId });
|
|
110
|
+
const {
|
|
111
|
+
data: node,
|
|
112
|
+
isLoading: isLoadingNode,
|
|
113
|
+
isError,
|
|
114
|
+
error: nodeError,
|
|
115
|
+
} = useGetNodeContent({
|
|
116
|
+
nodeSlug: nodeSlug ?? '',
|
|
117
|
+
projectId,
|
|
118
|
+
branchSlug,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const container = React.useRef<HTMLDivElement>(null);
|
|
122
|
+
|
|
123
|
+
if (!nodeSlug && isTocFetched && tableOfContents?.items) {
|
|
124
|
+
const firstNode = findFirstNode(tableOfContents.items);
|
|
125
|
+
if (firstNode) {
|
|
126
|
+
return <Navigate to={branchSlug ? `branches/${branchSlug}/${firstNode.slug}` : `${firstNode.slug}`} replace />;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const handleTocClick = () => {
|
|
131
|
+
if (container.current) {
|
|
132
|
+
container.current.scrollIntoView();
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<SidebarLayout
|
|
138
|
+
ref={container}
|
|
139
|
+
sidebar={
|
|
140
|
+
<>
|
|
141
|
+
{branches && branches.items.length > 1 ? (
|
|
142
|
+
<BranchSelector
|
|
143
|
+
branchSlug={branchSlug}
|
|
144
|
+
branches={branches.items}
|
|
145
|
+
onChange={branch => {
|
|
146
|
+
const encodedBranchSlug = encodeURIComponent(branch.slug);
|
|
147
|
+
navigate(branch.is_default ? `${nodeSlug}` : `branches/${encodedBranchSlug}/${nodeSlug}`);
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
) : null}
|
|
151
|
+
{tableOfContents ? (
|
|
152
|
+
<TableOfContents
|
|
153
|
+
activeId={node?.id || nodeSlug?.split('-')[0] || ''}
|
|
154
|
+
tableOfContents={tableOfContents}
|
|
155
|
+
Link={Link as CustomLinkComponent}
|
|
156
|
+
collapseTableOfContents={collapseTableOfContents}
|
|
157
|
+
onLinkClick={handleTocClick}
|
|
158
|
+
/>
|
|
159
|
+
) : null}
|
|
160
|
+
</>
|
|
161
|
+
}
|
|
162
|
+
>
|
|
163
|
+
<Outlet context={[isLoadingNode, isTocFetched, isError, nodeError, node]} />
|
|
164
|
+
</SidebarLayout>
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const ProjectNode: React.FC<StoplightProjectProps> = ({
|
|
169
|
+
hideTryIt,
|
|
170
|
+
hideSecurityInfo,
|
|
171
|
+
hideServerInfo,
|
|
172
|
+
hideMocking,
|
|
173
|
+
hideExport,
|
|
174
|
+
tryItCredentialsPolicy,
|
|
175
|
+
tryItCorsProxy,
|
|
176
|
+
}) => {
|
|
177
|
+
const { branchSlug: encodedBranchSlug = '', '*': nodeSlug = '' } = useParams<{ branchSlug?: string; '*': string }>();
|
|
178
|
+
const branchSlug = decodeURIComponent(encodedBranchSlug);
|
|
179
|
+
const [isLoadingNode, isTocFetched, isError, nodeError, node] = useOutletContext<any>();
|
|
180
|
+
|
|
181
|
+
if (isLoadingNode || !isTocFetched) {
|
|
182
|
+
return <Loading />;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (isError) {
|
|
186
|
+
if (nodeError instanceof ResponseError) {
|
|
187
|
+
if (nodeError.code === 402) {
|
|
188
|
+
return <UpgradeToStarter />;
|
|
189
|
+
} else if (nodeError.code === 403) {
|
|
190
|
+
return <Forbidden />;
|
|
191
|
+
} else {
|
|
192
|
+
return <NotFound />;
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
return <NotFound />;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!node) {
|
|
200
|
+
return <NotFound />;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (node?.slug && nodeSlug !== node.slug) {
|
|
204
|
+
// Handle redirect to node's slug
|
|
205
|
+
return <Navigate to={branchSlug ? `/branches/${branchSlug}/${node.slug}` : `/${node.slug}`} replace />;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<>
|
|
210
|
+
<ScrollToHashElement />
|
|
211
|
+
<NodeContent
|
|
212
|
+
node={node}
|
|
213
|
+
Link={ReactRouterMarkdownLink}
|
|
214
|
+
hideTryIt={hideTryIt}
|
|
215
|
+
hideMocking={hideMocking}
|
|
216
|
+
hideExport={hideExport}
|
|
217
|
+
hideSecurityInfo={hideSecurityInfo}
|
|
218
|
+
hideServerInfo={hideServerInfo}
|
|
219
|
+
tryItCredentialsPolicy={tryItCredentialsPolicy}
|
|
220
|
+
tryItCorsProxy={tryItCorsProxy}
|
|
221
|
+
/>
|
|
222
|
+
</>
|
|
223
|
+
);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const StoplightProjectRouter = ({
|
|
227
|
+
platformUrl,
|
|
228
|
+
basePath = '/',
|
|
229
|
+
staticRouterPath = '',
|
|
230
|
+
router = 'hash',
|
|
231
|
+
...props
|
|
232
|
+
}: StoplightProjectProps) => {
|
|
233
|
+
const { Router, routerProps } = useRouter(router, basePath, staticRouterPath);
|
|
234
|
+
const outerRouter = useInRouterContext();
|
|
235
|
+
|
|
236
|
+
const InternalRoutes = () => (
|
|
237
|
+
<Routes>
|
|
238
|
+
<Route path="/" element={<StoplightProjectImpl {...props} />}>
|
|
239
|
+
<Route path="/branches/:branchSlug/*" element={<ProjectNode {...props} />} />
|
|
240
|
+
<Route path="/*" element={<ProjectNode {...props} />} />
|
|
241
|
+
</Route>
|
|
242
|
+
</Routes>
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (!outerRouter) {
|
|
246
|
+
return (
|
|
247
|
+
<DevPortalProvider platformUrl={platformUrl}>
|
|
248
|
+
<RouterTypeContext.Provider value={router}>
|
|
249
|
+
<Router {...routerProps} key={basePath}>
|
|
250
|
+
<InternalRoutes />
|
|
251
|
+
</Router>
|
|
252
|
+
</RouterTypeContext.Provider>
|
|
253
|
+
</DevPortalProvider>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<DevPortalProvider platformUrl={platformUrl}>
|
|
259
|
+
<RouterTypeContext.Provider value={router}>
|
|
260
|
+
<InternalRoutes />
|
|
261
|
+
</RouterTypeContext.Provider>
|
|
262
|
+
</DevPortalProvider>
|
|
263
|
+
);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The StoplightProject component displays a traditional documentation UI for an existing Stoplight Project.
|
|
268
|
+
*/
|
|
269
|
+
export const StoplightProject = withStyles(StoplightProjectRouter);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fetchMock from 'jest-fetch-mock';
|
|
2
|
+
|
|
3
|
+
import { getBranches } from '../getBranches';
|
|
4
|
+
|
|
5
|
+
describe('getBranches', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
fetchMock.resetMocks();
|
|
8
|
+
localStorage.clear();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should URI encode the parameters in the request URL', async () => {
|
|
12
|
+
fetchMock.mockResolvedValue(
|
|
13
|
+
new Response('{}', {
|
|
14
|
+
status: 200,
|
|
15
|
+
statusText: 'OK',
|
|
16
|
+
headers: [],
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
await getBranches({
|
|
21
|
+
projectId: 'some/slash',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(fetchMock).toBeCalledWith('https://stoplight.io/api/v1/projects/some%2Fslash/branches', {
|
|
25
|
+
headers: expect.objectContaining({
|
|
26
|
+
'Stoplight-Elements-Version': expect.any(String),
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fetchMock from 'jest-fetch-mock';
|
|
2
|
+
|
|
3
|
+
import { getNodeContent } from '../getNodeContent';
|
|
4
|
+
|
|
5
|
+
describe('getNodeContent', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
fetchMock.resetMocks();
|
|
8
|
+
localStorage.clear();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should URI encode the parameters in the request URL', async () => {
|
|
12
|
+
fetchMock.mockResolvedValue(
|
|
13
|
+
new Response('{}', {
|
|
14
|
+
status: 200,
|
|
15
|
+
statusText: 'OK',
|
|
16
|
+
headers: [],
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
await getNodeContent({
|
|
21
|
+
nodeSlug: 'node&slug',
|
|
22
|
+
projectId: 'some/slash',
|
|
23
|
+
branchSlug: 'test+branch',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(fetchMock).toBeCalledWith(
|
|
27
|
+
'https://stoplight.io/api/v1/projects/some%2Fslash/nodes/node%26slug?branch=test%2Bbranch',
|
|
28
|
+
{
|
|
29
|
+
headers: expect.objectContaining({
|
|
30
|
+
'Stoplight-Elements-Version': expect.any(String),
|
|
31
|
+
}),
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fetchMock from 'jest-fetch-mock';
|
|
2
|
+
|
|
3
|
+
import { getNodes } from '../getNodes';
|
|
4
|
+
|
|
5
|
+
describe('getNodes', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
fetchMock.resetMocks();
|
|
8
|
+
localStorage.clear();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('with a provided workspace identifier', () => {
|
|
12
|
+
it('should URI encode the parameters in the request URL', async () => {
|
|
13
|
+
fetchMock.mockResolvedValue(
|
|
14
|
+
new Response('{}', {
|
|
15
|
+
status: 200,
|
|
16
|
+
statusText: 'OK',
|
|
17
|
+
headers: [],
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
await getNodes({
|
|
22
|
+
workspaceId: 'my?workspace',
|
|
23
|
+
projectIds: ['some/slash'],
|
|
24
|
+
branchSlug: 'test+branch',
|
|
25
|
+
search: 'a?special&search',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(fetchMock).toBeCalledWith(
|
|
29
|
+
'https://stoplight.io/api/v1/workspaces/my%3Fworkspace/nodes?project_ids[0]=some%2Fslash&search=a%3Fspecial%26search&branch=test%2Bbranch',
|
|
30
|
+
{
|
|
31
|
+
headers: expect.objectContaining({
|
|
32
|
+
'Stoplight-Elements-Version': expect.any(String),
|
|
33
|
+
}),
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fetchMock from 'jest-fetch-mock';
|
|
2
|
+
|
|
3
|
+
import { getTableOfContents } from '../getTableOfContents';
|
|
4
|
+
|
|
5
|
+
describe('getTableOfContents', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
fetchMock.resetMocks();
|
|
8
|
+
localStorage.clear();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should URI encode the parameters in the request URL', async () => {
|
|
12
|
+
fetchMock.mockResolvedValue(
|
|
13
|
+
new Response('{}', {
|
|
14
|
+
status: 200,
|
|
15
|
+
statusText: 'OK',
|
|
16
|
+
headers: [],
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
await getTableOfContents({
|
|
21
|
+
projectId: 'some/slash',
|
|
22
|
+
branchSlug: 'test+branch',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(fetchMock).toBeCalledWith(
|
|
26
|
+
'https://stoplight.io/api/v1/projects/some%2Fslash/table-of-contents?branch=test%2Bbranch',
|
|
27
|
+
{
|
|
28
|
+
headers: expect.objectContaining({
|
|
29
|
+
'Stoplight-Elements-Version': expect.any(String),
|
|
30
|
+
}),
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fetchMock from 'jest-fetch-mock';
|
|
2
|
+
|
|
3
|
+
import { getWorkspace } from '../getWorkspace';
|
|
4
|
+
|
|
5
|
+
describe('getWorkspace', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
fetchMock.resetMocks();
|
|
8
|
+
localStorage.clear();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should URI encode the parameters in the request URL', async () => {
|
|
12
|
+
fetchMock.mockResolvedValue(
|
|
13
|
+
new Response('{}', {
|
|
14
|
+
status: 200,
|
|
15
|
+
statusText: 'OK',
|
|
16
|
+
headers: [],
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
await getWorkspace({
|
|
21
|
+
projectIds: ['some/slash'],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(fetchMock).toBeCalledWith('https://stoplight.io/api/v1/projects/some%2Fslash', {
|
|
25
|
+
headers: expect.objectContaining({
|
|
26
|
+
'Stoplight-Elements-Version': expect.any(String),
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Branches } from '../types';
|
|
2
|
+
import { appVersion } from '../version';
|
|
3
|
+
|
|
4
|
+
export const getBranches = async ({
|
|
5
|
+
projectId,
|
|
6
|
+
platformUrl = 'https://stoplight.io',
|
|
7
|
+
platformAuthToken,
|
|
8
|
+
}: {
|
|
9
|
+
projectId: string;
|
|
10
|
+
platformUrl?: string;
|
|
11
|
+
platformAuthToken?: string;
|
|
12
|
+
}): Promise<Branches> => {
|
|
13
|
+
const encodedProjectId = encodeURIComponent(projectId);
|
|
14
|
+
const response = await fetch(`${platformUrl}/api/v1/projects/${encodedProjectId}/branches`, {
|
|
15
|
+
headers: {
|
|
16
|
+
'Stoplight-Elements-Version': appVersion,
|
|
17
|
+
...(platformAuthToken && { Authorization: `Bearer ${platformAuthToken}` }),
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
const data = await response.json();
|
|
21
|
+
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(data);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return data;
|
|
27
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Node } from '../types';
|
|
2
|
+
import { appVersion } from '../version';
|
|
3
|
+
|
|
4
|
+
export class ResponseError extends Error {
|
|
5
|
+
code: number;
|
|
6
|
+
|
|
7
|
+
constructor(message: string, responseCode: number) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'ResponseError';
|
|
10
|
+
this.code = responseCode;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const getNodeContent = async ({
|
|
15
|
+
nodeSlug,
|
|
16
|
+
projectId,
|
|
17
|
+
branchSlug,
|
|
18
|
+
platformUrl = 'https://stoplight.io',
|
|
19
|
+
platformAuthToken,
|
|
20
|
+
}: {
|
|
21
|
+
nodeSlug: string;
|
|
22
|
+
projectId: string;
|
|
23
|
+
branchSlug?: string;
|
|
24
|
+
platformUrl?: string;
|
|
25
|
+
platformAuthToken?: string;
|
|
26
|
+
}): Promise<Node> => {
|
|
27
|
+
const encodedNodeSlug = encodeURIComponent(nodeSlug);
|
|
28
|
+
const encodedProjectId = encodeURIComponent(projectId);
|
|
29
|
+
const encodedBranchSlug = branchSlug ? encodeURIComponent(branchSlug) : '';
|
|
30
|
+
const branchQuery = encodedBranchSlug ? `?branch=${encodedBranchSlug}` : '';
|
|
31
|
+
const response = await fetch(
|
|
32
|
+
`${platformUrl}/api/v1/projects/${encodedProjectId}/nodes/${encodedNodeSlug}${branchQuery}`,
|
|
33
|
+
{
|
|
34
|
+
headers: {
|
|
35
|
+
'Stoplight-Elements-Version': appVersion,
|
|
36
|
+
...(platformAuthToken && { Authorization: `Bearer ${platformAuthToken}` }),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
if (response.status === 402) {
|
|
44
|
+
throw new ResponseError('Payment Required', response.status);
|
|
45
|
+
} else if (response.status === 403) {
|
|
46
|
+
throw new ResponseError('Forbidden', response.status);
|
|
47
|
+
} else {
|
|
48
|
+
throw new ResponseError('Something went wrong', response.status);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return data;
|
|
53
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { NodeSearchResult } from '../types';
|
|
2
|
+
import { appVersion } from '../version';
|
|
3
|
+
|
|
4
|
+
export const getNodes = async ({
|
|
5
|
+
workspaceId,
|
|
6
|
+
branchSlug,
|
|
7
|
+
projectIds,
|
|
8
|
+
search,
|
|
9
|
+
platformUrl = 'https://stoplight.io',
|
|
10
|
+
platformAuthToken,
|
|
11
|
+
}: {
|
|
12
|
+
workspaceId?: string;
|
|
13
|
+
branchSlug?: string;
|
|
14
|
+
projectIds?: string[];
|
|
15
|
+
search?: string;
|
|
16
|
+
platformUrl?: string;
|
|
17
|
+
platformAuthToken?: string;
|
|
18
|
+
}): Promise<NodeSearchResult[]> => {
|
|
19
|
+
const queryParams = [];
|
|
20
|
+
let fetchedWorkspaceId = workspaceId || '';
|
|
21
|
+
|
|
22
|
+
if (!workspaceId && projectIds?.length) {
|
|
23
|
+
const encodedProjectId = encodeURIComponent(projectIds[0]);
|
|
24
|
+
const response = await fetch(`${platformUrl}/api/v1/projects/${encodedProjectId}`, {
|
|
25
|
+
headers: {
|
|
26
|
+
'Stoplight-Elements-Version': appVersion,
|
|
27
|
+
...(platformAuthToken && { Authorization: `Bearer ${platformAuthToken}` }),
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const data = await response.json();
|
|
31
|
+
fetchedWorkspaceId = data.workspace.id;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (projectIds && projectIds.length) {
|
|
35
|
+
queryParams.push(
|
|
36
|
+
...projectIds.map((projectId, index) => {
|
|
37
|
+
const encodedProjectId = encodeURIComponent(projectId);
|
|
38
|
+
return `project_ids[${index}]=${encodedProjectId}`;
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (search) {
|
|
44
|
+
const encodedSearch = encodeURIComponent(search);
|
|
45
|
+
queryParams.push(`search=${encodedSearch}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (branchSlug) {
|
|
49
|
+
const encodedBranchSlug = encodeURIComponent(branchSlug);
|
|
50
|
+
queryParams.push(`branch=${encodedBranchSlug}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const query = queryParams.length ? `?${queryParams.join('&')}` : '';
|
|
54
|
+
|
|
55
|
+
const encodedWorkspaceId = encodeURIComponent(fetchedWorkspaceId);
|
|
56
|
+
const response = await fetch(`${platformUrl}/api/v1/workspaces/${encodedWorkspaceId}/nodes${query}`, {
|
|
57
|
+
headers: {
|
|
58
|
+
'Stoplight-Elements-Version': appVersion,
|
|
59
|
+
...(platformAuthToken && { Authorization: `Bearer ${platformAuthToken}` }),
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new Error(data);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return data;
|
|
69
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ProjectTableOfContents } from '../types';
|
|
2
|
+
import { appVersion } from '../version';
|
|
3
|
+
|
|
4
|
+
export const getTableOfContents = async ({
|
|
5
|
+
projectId,
|
|
6
|
+
branchSlug,
|
|
7
|
+
platformUrl = 'https://stoplight.io',
|
|
8
|
+
platformAuthToken,
|
|
9
|
+
}: {
|
|
10
|
+
projectId: string;
|
|
11
|
+
branchSlug?: string;
|
|
12
|
+
platformUrl?: string;
|
|
13
|
+
platformAuthToken?: string;
|
|
14
|
+
}): Promise<ProjectTableOfContents> => {
|
|
15
|
+
const encodedProjectId = encodeURIComponent(projectId);
|
|
16
|
+
const encodedBranchSlug = branchSlug ? encodeURIComponent(branchSlug) : '';
|
|
17
|
+
const branchQuery = encodedBranchSlug ? `?branch=${encodedBranchSlug}` : '';
|
|
18
|
+
const response = await fetch(`${platformUrl}/api/v1/projects/${encodedProjectId}/table-of-contents${branchQuery}`, {
|
|
19
|
+
headers: {
|
|
20
|
+
'Stoplight-Elements-Version': appVersion,
|
|
21
|
+
...(platformAuthToken && { Authorization: `Bearer ${platformAuthToken}` }),
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
const data = await response.json();
|
|
25
|
+
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(data);
|
|
28
|
+
}
|
|
29
|
+
return data;
|
|
30
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Workspace } from '../types';
|
|
2
|
+
import { appVersion } from '../version';
|
|
3
|
+
|
|
4
|
+
export const getWorkspace = async ({
|
|
5
|
+
projectIds,
|
|
6
|
+
platformUrl = 'https://stoplight.io',
|
|
7
|
+
platformAuthToken,
|
|
8
|
+
}: {
|
|
9
|
+
projectIds: string[];
|
|
10
|
+
platformUrl?: string;
|
|
11
|
+
platformAuthToken?: string;
|
|
12
|
+
}): Promise<Workspace> => {
|
|
13
|
+
const encodedProjectId = encodeURIComponent(projectIds[0]);
|
|
14
|
+
const response = await fetch(`${platformUrl}/api/v1/projects/${encodedProjectId}`, {
|
|
15
|
+
headers: {
|
|
16
|
+
'Stoplight-Elements-Version': appVersion,
|
|
17
|
+
...(platformAuthToken && { Authorization: `Bearer ${platformAuthToken}` }),
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
const data = await response.json();
|
|
21
|
+
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(data);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return data;
|
|
27
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
|
|
3
|
+
import { act, renderHook } from '@testing-library/react-hooks';
|
|
4
|
+
import fetchMock from 'jest-fetch-mock';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
|
|
7
|
+
import { DevPortalProvider } from '../../components/DevPortalProvider';
|
|
8
|
+
import { useGetBranches } from '../useGetBranches';
|
|
9
|
+
import { useGetNodeContent } from '../useGetNodeContent';
|
|
10
|
+
import { useGetNodes } from '../useGetNodes';
|
|
11
|
+
import { useGetTableOfContents } from '../useGetTableOfContents';
|
|
12
|
+
|
|
13
|
+
describe('data fetching', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
fetchMock.mockResponse(() => Promise.resolve({ status: 200 }));
|
|
16
|
+
fetchMock.enableMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
fetchMock.disableMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it.each<[string, Function, Record<string, string>]>([
|
|
24
|
+
['useGetTableOfContents', useGetTableOfContents, { projectId: 'id' }],
|
|
25
|
+
['useGetBranches', useGetBranches, { projectId: 'id' }],
|
|
26
|
+
['useGetNodeContent', useGetNodeContent, { projectId: 'id', nodeSlug: 'node' }],
|
|
27
|
+
['useGetNodes', useGetNodes, { search: 'search', workspaceId: 'id' }],
|
|
28
|
+
])('includes authorization header when auth token is present in context for %s', async (_, hook, args) => {
|
|
29
|
+
const { waitFor } = renderHook(() => hook(args), {
|
|
30
|
+
wrapper: ({ children }) => <DevPortalProvider platformAuthToken="secret">{children}</DevPortalProvider>,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await act(async () => {
|
|
34
|
+
await waitFor(() => expect(fetchMock).toHaveBeenCalled());
|
|
35
|
+
});
|
|
36
|
+
const requestInit = fetchMock.mock.calls[0][1]!;
|
|
37
|
+
|
|
38
|
+
const headers = new Headers(requestInit.headers);
|
|
39
|
+
|
|
40
|
+
expect(headers.get('Authorization')).toBe('Bearer secret');
|
|
41
|
+
});
|
|
42
|
+
});
|