@stoplight/elements-dev-portal 3.0.10 → 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.
Files changed (107) hide show
  1. package/.storybook/main.js +6 -0
  2. package/.storybook/manager.js +1 -0
  3. package/.storybook/preview.jsx +46 -0
  4. package/dist/LICENSE +190 -0
  5. package/dist/README.md +22 -0
  6. package/{index.esm.js → dist/index.esm.js} +8 -2
  7. package/{index.js → dist/index.js} +8 -2
  8. package/{index.mjs → dist/index.mjs} +8 -2
  9. package/dist/package.json +51 -0
  10. package/dist/version.d.ts +1 -0
  11. package/{web-components.min.js → dist/web-components.min.js} +1 -1
  12. package/jest.config.js +10 -0
  13. package/package.json +71 -17
  14. package/src/__fixtures__/branches.json +28 -0
  15. package/src/__fixtures__/node-content.json +257 -0
  16. package/src/__fixtures__/table-of-contents.json +144 -0
  17. package/src/components/BranchSelector/BranchSelector.spec.tsx +61 -0
  18. package/src/components/BranchSelector/BranchSelector.stories.tsx +41 -0
  19. package/src/components/BranchSelector/BranchSelector.tsx +50 -0
  20. package/src/components/BranchSelector/index.tsx +1 -0
  21. package/src/components/DevPortalProvider/index.tsx +25 -0
  22. package/src/components/Forbidden.tsx +11 -0
  23. package/src/components/Loading.tsx +9 -0
  24. package/src/components/NodeContent/NodeContent.spec.tsx +128 -0
  25. package/src/components/NodeContent/NodeContent.stories.tsx +60 -0
  26. package/src/components/NodeContent/NodeContent.tsx +235 -0
  27. package/src/components/NodeContent/index.tsx +1 -0
  28. package/src/components/NotFound.tsx +11 -0
  29. package/src/components/Search/Search.stories.tsx +151 -0
  30. package/src/components/Search/Search.tsx +161 -0
  31. package/src/components/Search/SearchOverlay.tsx +88 -0
  32. package/src/components/Search/index.tsx +1 -0
  33. package/src/components/TableOfContents/TableOfContents.stories.tsx +68 -0
  34. package/src/components/TableOfContents/TableOfContents.tsx +54 -0
  35. package/src/components/TableOfContents/index.tsx +1 -0
  36. package/src/components/UpgradeToStarter.tsx +22 -0
  37. package/src/consts.ts +32 -0
  38. package/src/containers/StoplightProject.spec.tsx +78 -0
  39. package/src/containers/StoplightProject.stories.tsx +28 -0
  40. package/src/containers/StoplightProject.tsx +269 -0
  41. package/src/handlers/__tests__/getBranches.test.ts +30 -0
  42. package/src/handlers/__tests__/getNodeContent.test.ts +35 -0
  43. package/src/handlers/__tests__/getNodes.test.ts +38 -0
  44. package/src/handlers/__tests__/getTableOfContents.test.ts +34 -0
  45. package/src/handlers/__tests__/getWorkspace.test.ts +30 -0
  46. package/src/handlers/getBranches.ts +27 -0
  47. package/src/handlers/getNodeContent.ts +53 -0
  48. package/src/handlers/getNodes.ts +69 -0
  49. package/src/handlers/getTableOfContents.ts +30 -0
  50. package/src/handlers/getWorkspace.ts +27 -0
  51. package/src/hooks/__tests__/dataFetching.spec.tsx +42 -0
  52. package/src/hooks/useGetBranches.ts +17 -0
  53. package/src/hooks/useGetNodeContent.ts +24 -0
  54. package/src/hooks/useGetNodes.ts +34 -0
  55. package/src/hooks/useGetTableOfContents.ts +17 -0
  56. package/src/hooks/useGetWorkspace.tsx +13 -0
  57. package/src/index.ts +25 -0
  58. package/src/styles.css +1 -0
  59. package/src/types.ts +85 -0
  60. package/src/version.ts +2 -0
  61. package/src/web-components/__stories__/StoplightProject.stories.tsx +33 -0
  62. package/src/web-components/components.ts +19 -0
  63. package/src/web-components/index.ts +3 -0
  64. package/tsconfig.build.json +18 -0
  65. package/tsconfig.json +7 -0
  66. package/web-components.config.js +1 -0
  67. package/version.d.ts +0 -1
  68. /package/{components → dist/components}/BranchSelector/BranchSelector.d.ts +0 -0
  69. /package/{components → dist/components}/BranchSelector/BranchSelector.spec.d.ts +0 -0
  70. /package/{components → dist/components}/BranchSelector/BranchSelector.stories.d.ts +0 -0
  71. /package/{components → dist/components}/BranchSelector/index.d.ts +0 -0
  72. /package/{components → dist/components}/DevPortalProvider/index.d.ts +0 -0
  73. /package/{components → dist/components}/Forbidden.d.ts +0 -0
  74. /package/{components → dist/components}/Loading.d.ts +0 -0
  75. /package/{components → dist/components}/NodeContent/NodeContent.d.ts +0 -0
  76. /package/{components → dist/components}/NodeContent/NodeContent.spec.d.ts +0 -0
  77. /package/{components → dist/components}/NodeContent/NodeContent.stories.d.ts +0 -0
  78. /package/{components → dist/components}/NodeContent/index.d.ts +0 -0
  79. /package/{components → dist/components}/NotFound.d.ts +0 -0
  80. /package/{components → dist/components}/Search/Search.d.ts +0 -0
  81. /package/{components → dist/components}/Search/Search.stories.d.ts +0 -0
  82. /package/{components → dist/components}/Search/SearchOverlay.d.ts +0 -0
  83. /package/{components → dist/components}/Search/index.d.ts +0 -0
  84. /package/{components → dist/components}/TableOfContents/TableOfContents.d.ts +0 -0
  85. /package/{components → dist/components}/TableOfContents/TableOfContents.stories.d.ts +0 -0
  86. /package/{components → dist/components}/TableOfContents/index.d.ts +0 -0
  87. /package/{components → dist/components}/UpgradeToStarter.d.ts +0 -0
  88. /package/{consts.d.ts → dist/consts.d.ts} +0 -0
  89. /package/{containers → dist/containers}/StoplightProject.d.ts +0 -0
  90. /package/{containers → dist/containers}/StoplightProject.spec.d.ts +0 -0
  91. /package/{containers → dist/containers}/StoplightProject.stories.d.ts +0 -0
  92. /package/{handlers → dist/handlers}/getBranches.d.ts +0 -0
  93. /package/{handlers → dist/handlers}/getNodeContent.d.ts +0 -0
  94. /package/{handlers → dist/handlers}/getNodes.d.ts +0 -0
  95. /package/{handlers → dist/handlers}/getTableOfContents.d.ts +0 -0
  96. /package/{handlers → dist/handlers}/getWorkspace.d.ts +0 -0
  97. /package/{hooks → dist/hooks}/useGetBranches.d.ts +0 -0
  98. /package/{hooks → dist/hooks}/useGetNodeContent.d.ts +0 -0
  99. /package/{hooks → dist/hooks}/useGetNodes.d.ts +0 -0
  100. /package/{hooks → dist/hooks}/useGetTableOfContents.d.ts +0 -0
  101. /package/{hooks → dist/hooks}/useGetWorkspace.d.ts +0 -0
  102. /package/{index.d.ts → dist/index.d.ts} +0 -0
  103. /package/{styles.min.css → dist/styles.min.css} +0 -0
  104. /package/{types.d.ts → dist/types.d.ts} +0 -0
  105. /package/{web-components → dist/web-components}/components.d.ts +0 -0
  106. /package/{web-components → dist/web-components}/index.d.ts +0 -0
  107. /package/{web-components.min.js.LICENSE.txt → dist/web-components.min.js.LICENSE.txt} +0 -0
@@ -0,0 +1,151 @@
1
+ import { faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons';
2
+ import { Box, Icon, Input, useModalState } from '@stoplight/mosaic';
3
+ import { Story } from '@storybook/react';
4
+ import * as React from 'react';
5
+
6
+ import { useGetNodes } from '../../hooks/useGetNodes';
7
+ import { useGetTableOfContents } from '../../hooks/useGetTableOfContents';
8
+ import { useGetWorkspace } from '../../hooks/useGetWorkspace';
9
+ import { NodeSearchResult } from '../../types';
10
+ import { TableOfContents } from '../TableOfContents';
11
+ import { Search, SearchResults } from './';
12
+
13
+ type SearchWrapperProps = { projectIds: string[]; workspaceId: string; isInResponsiveMode?: boolean };
14
+ // Wrapper to show how to use the node content hook
15
+ const SearchWrapper = ({ projectIds, workspaceId }: SearchWrapperProps) => {
16
+ const { isOpen, open, close } = useModalState();
17
+ const [search, setSearch] = React.useState('');
18
+ const { data, isFetching } = useGetNodes({
19
+ search,
20
+ projectIds,
21
+ workspaceId,
22
+ });
23
+
24
+ const { data: workspace } = useGetWorkspace({
25
+ projectIds,
26
+ });
27
+
28
+ const handleClose = () => {
29
+ close();
30
+ setSearch('');
31
+ };
32
+
33
+ const handleClick = (searchResult: NodeSearchResult) => {
34
+ console.log('Search clicked', searchResult);
35
+ window.open(
36
+ `https://${workspace?.workspace.slug}.stoplight.io/docs/${searchResult.project_slug}${searchResult.uri}`,
37
+ '_blank',
38
+ );
39
+
40
+ handleClose();
41
+ };
42
+
43
+ return (
44
+ <>
45
+ <Input placeholder="Search..." onClick={open} />
46
+ <Search
47
+ isLoading={isFetching}
48
+ search={search}
49
+ searchResults={data}
50
+ onSearch={setSearch}
51
+ isOpen={isOpen}
52
+ onClose={handleClose}
53
+ onClick={handleClick}
54
+ />
55
+ </>
56
+ );
57
+ };
58
+
59
+ const EmbeddedSearchWrapper = ({ projectIds, workspaceId, isInResponsiveMode }: SearchWrapperProps) => {
60
+ const [search, setSearch] = React.useState('');
61
+ const { data, isFetching } = useGetNodes({
62
+ search,
63
+ projectIds,
64
+ workspaceId,
65
+ });
66
+
67
+ const { data: workspace } = useGetWorkspace({
68
+ projectIds,
69
+ });
70
+ const { data: tableOfContents } = useGetTableOfContents({ projectId: projectIds[0], branchSlug: '' });
71
+
72
+ const handleClick = (searchResult: NodeSearchResult) => {
73
+ console.log('Search clicked', searchResult);
74
+ window.open(
75
+ `https://${workspace?.workspace.slug}.stoplight.io/docs/${searchResult.project_slug}${searchResult.uri}`,
76
+ '_blank',
77
+ );
78
+ };
79
+
80
+ return (
81
+ <>
82
+ <Box bg="canvas" pos="sticky" style={{ top: 0 }}>
83
+ <Box bg="canvas" w="full" pt={3}>
84
+ <Input
85
+ appearance="minimal"
86
+ border
87
+ icon={<Box as={Icon} ml={1} icon={isFetching ? faSpinner : faSearch} spin={isFetching} />}
88
+ autoFocus
89
+ placeholder="Search..."
90
+ value={search}
91
+ onChange={e => {
92
+ setSearch(e.currentTarget.value);
93
+ }}
94
+ type="search"
95
+ />
96
+ </Box>
97
+ </Box>
98
+ <Box>
99
+ {isInResponsiveMode && !search && tableOfContents ? (
100
+ <TableOfContents
101
+ isInResponsiveMode={isInResponsiveMode}
102
+ tableOfContents={tableOfContents}
103
+ activeId="b3A6MTE0"
104
+ Link={({ children, ...props }) => {
105
+ return (
106
+ <a
107
+ onClick={() => {
108
+ console.log('Link clicked!', props);
109
+ }}
110
+ >
111
+ {children}
112
+ </a>
113
+ );
114
+ }}
115
+ />
116
+ ) : (
117
+ !search && isInResponsiveMode && <>Loading...</>
118
+ )}
119
+ {/* show search results first if not in responsive mode */}
120
+ {!isInResponsiveMode || (isInResponsiveMode && search) ? (
121
+ <Box p={5}>
122
+ <SearchResults searchResults={data} onClick={handleClick} isEmbedded={true} />
123
+ </Box>
124
+ ) : null}
125
+ </Box>
126
+ </>
127
+ );
128
+ };
129
+ export default {
130
+ title: 'Public/Search',
131
+ component: SearchWrapper,
132
+ argTypes: {
133
+ workspaceId: { table: { category: 'Input' } },
134
+ projectIds: { table: { category: 'Input' } },
135
+ platformUrl: { table: { category: 'Input' } },
136
+ isInResponsiveMode: { table: { category: 'Input' } },
137
+ },
138
+ args: {
139
+ projectIds: ['cHJqOjYwNjYx'],
140
+ workspaceId: 'd2s6NDE1NTU',
141
+ platformUrl: 'https://stoplight.io',
142
+ isInResponsiveMode: false,
143
+ },
144
+ };
145
+
146
+ export const Playground: Story<SearchWrapperProps> = args => <SearchWrapper {...args} />;
147
+ export const EmbeddedSearch: Story<SearchWrapperProps> = args => <EmbeddedSearchWrapper {...args} />;
148
+
149
+ Playground.storyName = 'Studio Demo';
150
+
151
+ EmbeddedSearch.storyName = 'Embedded Search';
@@ -0,0 +1,161 @@
1
+ import { faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons';
2
+ import {
3
+ NodeTypeColors,
4
+ NodeTypeIconDefs,
5
+ withMosaicProvider,
6
+ withPersistenceBoundary,
7
+ withQueryClientProvider,
8
+ withStyles,
9
+ } from '@stoplight/elements-core';
10
+ import { Box, Flex, Icon, Input, ListBox, ListBoxItem, Modal, ModalProps } from '@stoplight/mosaic';
11
+ import { flow } from 'lodash';
12
+ import * as React from 'react';
13
+
14
+ import { NodeSearchResult } from '../../types';
15
+
16
+ export type SearchProps = {
17
+ isLoading?: boolean;
18
+ search?: string;
19
+ searchResults?: NodeSearchResult[];
20
+ onSearch: (search: string) => void;
21
+ onClick: (result: NodeSearchResult) => void;
22
+ isOpen?: boolean;
23
+ onClose: ModalProps['onClose'];
24
+ };
25
+
26
+ export type SearchResultsListProps = {
27
+ searchResults?: NodeSearchResult[];
28
+ isEmbedded?: boolean;
29
+ showDivider?: boolean;
30
+ onClick: (result: NodeSearchResult) => void;
31
+ };
32
+
33
+ const SearchImpl = ({ isLoading, search, searchResults, isOpen, onClose, onClick, onSearch }: SearchProps) => {
34
+ const listBoxRef = React.useRef<HTMLDivElement>(null);
35
+
36
+ const onChange = React.useCallback(e => onSearch(e.currentTarget.value), [onSearch]);
37
+
38
+ const onKeyDown = React.useCallback(e => {
39
+ // Focus the search results on arrow down
40
+ if (e.key === 'ArrowDown') {
41
+ e.preventDefault();
42
+ listBoxRef.current?.focus();
43
+ }
44
+ }, []);
45
+
46
+ return (
47
+ <Modal
48
+ renderHeader={() => (
49
+ <Input
50
+ appearance="minimal"
51
+ borderB
52
+ size="lg"
53
+ icon={<Box as={Icon} ml={1} icon={isLoading ? faSpinner : faSearch} spin={isLoading} />}
54
+ autoFocus
55
+ placeholder="Search..."
56
+ value={search}
57
+ onChange={onChange}
58
+ onKeyDown={onKeyDown}
59
+ />
60
+ )}
61
+ isOpen={!!isOpen}
62
+ onClose={onClose}
63
+ >
64
+ <SearchResultsList searchResults={searchResults} onClick={onClick} />
65
+ </Modal>
66
+ );
67
+ };
68
+
69
+ export const SearchResultsList = ({
70
+ searchResults,
71
+ onClick,
72
+ isEmbedded,
73
+ showDivider = true,
74
+ }: SearchResultsListProps) => {
75
+ const listBoxRef = React.useRef<HTMLDivElement>(null);
76
+ const onSelectionChange = React.useCallback(
77
+ keys => {
78
+ const selectedId = keys.values().next().value;
79
+ const selectedResult = searchResults?.find(
80
+ searchResult => `${searchResult.id}-${searchResult.project_id}` === selectedId,
81
+ );
82
+ if (selectedResult) {
83
+ onClick(selectedResult);
84
+ }
85
+ },
86
+ [searchResults, onClick],
87
+ );
88
+
89
+ return (
90
+ <>
91
+ {searchResults && searchResults.length > 0 ? (
92
+ <ListBox
93
+ ref={listBoxRef}
94
+ aria-label="Search"
95
+ overflowY="auto"
96
+ h={isEmbedded ? undefined : 80}
97
+ m={-5}
98
+ items={searchResults}
99
+ selectionMode="single"
100
+ onSelectionChange={onSelectionChange}
101
+ >
102
+ {(searchResult: NodeSearchResult) => {
103
+ return (
104
+ <ListBoxItem key={`${searchResult.id}-${searchResult.project_id}`} textValue={searchResult.title}>
105
+ <Box p={3} borderB={!showDivider ? undefined : true}>
106
+ <Flex align="center">
107
+ <Box
108
+ as={Icon}
109
+ w={4}
110
+ icon={NodeTypeIconDefs[searchResult.type as keyof typeof NodeTypeIconDefs]}
111
+ style={{ color: NodeTypeColors[searchResult.type as keyof typeof NodeTypeIconDefs] }}
112
+ />
113
+
114
+ <Box
115
+ flex={1}
116
+ fontSize="lg"
117
+ dangerouslySetInnerHTML={{ __html: searchResult.highlighted.name ?? '' }}
118
+ fontWeight="medium"
119
+ textOverflow="overflow-ellipsis"
120
+ mx={2}
121
+ />
122
+
123
+ <Box fontSize="sm" color="muted">
124
+ {searchResult.project_name}
125
+ </Box>
126
+ </Flex>
127
+
128
+ <Box
129
+ dangerouslySetInnerHTML={{ __html: searchResult.highlighted.summary ?? '' }}
130
+ color="muted"
131
+ fontSize="sm"
132
+ mt={1}
133
+ ml={6}
134
+ />
135
+ </Box>
136
+ </ListBoxItem>
137
+ );
138
+ }}
139
+ </ListBox>
140
+ ) : (
141
+ <Flex w="full" h={80} align="center" justify="center" m={-5}>
142
+ No search results
143
+ </Flex>
144
+ )}
145
+ </>
146
+ );
147
+ };
148
+
149
+ export const SearchResults = flow(
150
+ withStyles,
151
+ withPersistenceBoundary,
152
+ withMosaicProvider,
153
+ withQueryClientProvider,
154
+ )(SearchResultsList);
155
+
156
+ export const Search = flow(
157
+ withStyles,
158
+ withPersistenceBoundary,
159
+ withMosaicProvider,
160
+ withQueryClientProvider,
161
+ )(SearchImpl);
@@ -0,0 +1,88 @@
1
+ import { faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons';
2
+ import type { CustomLinkComponent } from '@stoplight/elements-core';
3
+ import { Box, Button, Flex, Icon, Input, Modal } from '@stoplight/mosaic';
4
+ import * as React from 'react';
5
+ import { Link } from 'react-router-dom';
6
+
7
+ import { NodeSearchResult, ProjectTableOfContents } from '../../types';
8
+ import { TableOfContents } from '../TableOfContents';
9
+ import { SearchResults } from './Search';
10
+
11
+ type SearchOverlayProps = {
12
+ isFetching: boolean;
13
+ search: string;
14
+ setSearch: (search: string) => void;
15
+ data?: NodeSearchResult[];
16
+ toc?: ProjectTableOfContents;
17
+ nodeSlug?: string;
18
+ projectSlug?: string;
19
+ branchSlug?: string;
20
+ isSearchShowing: boolean;
21
+ onClose: () => void;
22
+ onClick: (item?: NodeSearchResult) => void;
23
+ };
24
+
25
+ export const SearchOverlay = ({
26
+ isFetching,
27
+ search,
28
+ setSearch,
29
+ data,
30
+ toc,
31
+ nodeSlug,
32
+ projectSlug,
33
+ branchSlug,
34
+ isSearchShowing,
35
+ onClose,
36
+ onClick,
37
+ }: SearchOverlayProps) => {
38
+ return (
39
+ <Modal isOpen={isSearchShowing} onClose={onClose}>
40
+ <Box className="sl-overlay" bg="canvas" overflowY="scroll" data-test="search-overlay">
41
+ <Flex alignItems="center" h="3xl" px={7} bg="canvas" borderB pos="sticky" zIndex={20}>
42
+ <Flex w="full">
43
+ <Box pt={1} pr={2}>
44
+ <Button
45
+ appearance="minimal"
46
+ onClick={() => {
47
+ onClose();
48
+ }}
49
+ data-test="search-overlay-back-button"
50
+ >
51
+ <Icon icon={['fas', 'arrow-left']} color="gray" size="lg" />
52
+ </Button>
53
+ </Box>
54
+ <Input
55
+ border
56
+ data-test="docs-search-input"
57
+ display="inline-block"
58
+ appearance="minimal"
59
+ icon={<Box as={Icon} ml={1} icon={isFetching ? faSpinner : faSearch} spin={isFetching} />}
60
+ autoFocus
61
+ placeholder={projectSlug ? 'Search within the project' : 'Search...'}
62
+ value={search}
63
+ onChange={e => {
64
+ setSearch(e.currentTarget.value);
65
+ }}
66
+ type="search"
67
+ w="full"
68
+ size="lg"
69
+ />
70
+ </Flex>
71
+ </Flex>
72
+ <Box px={5} py={toc && !search ? 0 : 5} data-test="responsive-project-toc">
73
+ {toc && !search && projectSlug && (
74
+ <TableOfContents
75
+ tableOfContents={{ ...toc, hide_powered_by: true }}
76
+ activeId={nodeSlug || ''}
77
+ Link={Link as CustomLinkComponent}
78
+ onLinkClick={onClick}
79
+ />
80
+ )}
81
+ {search && (
82
+ <SearchResults searchResults={data} onClick={item => onClick(item)} isEmbedded={true} showDivider={false} />
83
+ )}
84
+ </Box>
85
+ </Box>
86
+ </Modal>
87
+ );
88
+ };
@@ -0,0 +1 @@
1
+ export * from './Search';
@@ -0,0 +1,68 @@
1
+ import { Story } from '@storybook/react';
2
+ import * as React from 'react';
3
+
4
+ import { useGetTableOfContents } from '../../hooks/useGetTableOfContents';
5
+ import { TableOfContents } from './';
6
+
7
+ // Wrapper to show how to use the node content hook
8
+ const TableOfContentsWrapper = ({
9
+ projectId,
10
+ branchSlug,
11
+ isInResponsiveMode,
12
+ }: {
13
+ projectId: string;
14
+ branchSlug?: string;
15
+ isInResponsiveMode?: boolean;
16
+ }) => {
17
+ const { data } = useGetTableOfContents({ projectId, branchSlug });
18
+
19
+ return data ? (
20
+ <TableOfContents
21
+ isInResponsiveMode={isInResponsiveMode}
22
+ activeId="b3A6MTE0"
23
+ tableOfContents={data}
24
+ Link={({ children, ...props }) => {
25
+ return (
26
+ <a
27
+ onClick={() => {
28
+ console.log('Link clicked!', props);
29
+ }}
30
+ >
31
+ {children}
32
+ </a>
33
+ );
34
+ }}
35
+ style={{
36
+ width: '300px',
37
+ }}
38
+ />
39
+ ) : (
40
+ <>Loading</>
41
+ );
42
+ };
43
+
44
+ export default {
45
+ title: 'Public/TableOfContents',
46
+ component: TableOfContentsWrapper,
47
+ argTypes: {
48
+ projectId: { table: { category: 'Input' } },
49
+ branchSlug: { table: { category: 'Input' } },
50
+ platformUrl: { table: { category: 'Input' } },
51
+ isInResponsiveMode: { table: { category: 'Input' } },
52
+ },
53
+ args: {
54
+ projectId: 'cHJqOjYwNjYx',
55
+ branchSlug: '',
56
+ platformUrl: 'https://stoplight.io',
57
+ isInResponsiveMode: false,
58
+ },
59
+ };
60
+
61
+ export const Playground: Story<{
62
+ nodeSlug: string;
63
+ projectId: string;
64
+ branchSlug?: string;
65
+ isInResponsiveMode?: boolean;
66
+ }> = args => <TableOfContentsWrapper {...args} />;
67
+
68
+ Playground.storyName = 'Studio Demo';
@@ -0,0 +1,54 @@
1
+ import {
2
+ CustomLinkComponent,
3
+ PoweredByLink,
4
+ TableOfContents as ElementsTableOfContents,
5
+ } from '@stoplight/elements-core';
6
+ import { BoxProps, Flex } from '@stoplight/mosaic';
7
+ import * as React from 'react';
8
+
9
+ import { ProjectTableOfContents } from '../../types';
10
+
11
+ export type TableOfContentsProps = BoxProps<'div'> & {
12
+ activeId: string;
13
+ tableOfContents: ProjectTableOfContents;
14
+ Link: CustomLinkComponent;
15
+ collapseTableOfContents?: boolean;
16
+ externalScrollbar?: boolean;
17
+ isInResponsiveMode?: boolean;
18
+ onLinkClick?(): void;
19
+ };
20
+
21
+ export const TableOfContents = ({
22
+ tableOfContents,
23
+ activeId,
24
+ Link,
25
+ collapseTableOfContents = false,
26
+ externalScrollbar,
27
+ isInResponsiveMode = false,
28
+ onLinkClick,
29
+ ...boxProps
30
+ }: TableOfContentsProps) => {
31
+ return (
32
+ <Flex bg={isInResponsiveMode ? 'canvas' : 'canvas-100'} {...boxProps} flexDirection="col" maxH="full">
33
+ <Flex flexGrow flexShrink overflowY="auto">
34
+ <ElementsTableOfContents
35
+ tree={tableOfContents.items}
36
+ activeId={activeId}
37
+ Link={Link}
38
+ maxDepthOpenByDefault={collapseTableOfContents ? 0 : 1}
39
+ externalScrollbar={externalScrollbar}
40
+ onLinkClick={onLinkClick}
41
+ isInResponsiveMode={isInResponsiveMode}
42
+ />
43
+ </Flex>
44
+
45
+ {tableOfContents.hide_powered_by ? null : (
46
+ <PoweredByLink
47
+ source={activeId}
48
+ pathname={typeof window !== 'undefined' ? window.location.pathname : ''}
49
+ packageType="elements-dev-portal"
50
+ />
51
+ )}
52
+ </Flex>
53
+ );
54
+ };
@@ -0,0 +1 @@
1
+ export * from './TableOfContents';
@@ -0,0 +1,22 @@
1
+ import { Box, Flex, Icon } from '@stoplight/mosaic';
2
+ import React from 'react';
3
+
4
+ export const UpgradeToStarter = () => (
5
+ <Flex
6
+ as="a"
7
+ href="https://stoplight.io/pricing/"
8
+ target="_blank"
9
+ rel="noreferrer noopener"
10
+ justify="center"
11
+ alignItems="center"
12
+ w="full"
13
+ minH="screen"
14
+ color="muted"
15
+ flexDirection="col"
16
+ >
17
+ <Icon icon={['fas', 'exclamation-triangle']} size="4x" />
18
+ <Box pt={3}>
19
+ Please upgrade your Stoplight Workspace to the Starter Plan to use Elements Dev Portal in production.
20
+ </Box>
21
+ </Flex>
22
+ );
package/src/consts.ts ADDED
@@ -0,0 +1,32 @@
1
+ const ROOT_CACHE_KEY = '@stoplight/elements-dev-portal/client-query';
2
+
3
+ export const devPortalCacheKeys = {
4
+ all: [ROOT_CACHE_KEY] as const,
5
+
6
+ projects: () => [ROOT_CACHE_KEY, 'projects'] as const,
7
+ project: (projectId: string) => [...devPortalCacheKeys.projects(), projectId] as const,
8
+ projectsList: () => [...devPortalCacheKeys.projects(), 'list'] as const,
9
+ projectDetails: (projectId: string) => [...devPortalCacheKeys.project(projectId), 'details'] as const,
10
+
11
+ branches: (projectId: string) => [...devPortalCacheKeys.project(projectId), 'branches'] as const,
12
+ branch: (projectId: string, branch: string) => [...devPortalCacheKeys.branches(projectId), branch] as const,
13
+ branchesList: (projectId: string) => [...devPortalCacheKeys.branches(projectId), 'list'] as const,
14
+ branchDetails: (projectId: string, branch: string) =>
15
+ [...devPortalCacheKeys.branch(projectId, branch), 'details'] as const,
16
+ branchTOC: (projectId: string, branch: string) => [...devPortalCacheKeys.branch(projectId, branch), 'toc'] as const,
17
+
18
+ branchNodes: (projectId: string, branch: string) =>
19
+ [...devPortalCacheKeys.branch(projectId, branch), 'nodes'] as const,
20
+ branchNode: (projectId: string, branch: string, node: string) =>
21
+ [...devPortalCacheKeys.branchNodes(projectId, branch), node] as const,
22
+ branchNodesList: (projectId: string, branch: string) =>
23
+ [...devPortalCacheKeys.branchNodes(projectId, branch), 'list'] as const,
24
+ branchNodeDetails: (projectId: string, branch: string, node: string) =>
25
+ [...devPortalCacheKeys.branchNode(projectId, branch, node), 'details'] as const,
26
+
27
+ search: () => [...devPortalCacheKeys.all, 'search'],
28
+ searchNodes: (filters: { projectIds?: string[]; branchSlug?: string; workspaceId?: string; search?: string }) => [
29
+ ...devPortalCacheKeys.search(),
30
+ filters,
31
+ ],
32
+ };
@@ -0,0 +1,78 @@
1
+ import '@testing-library/jest-dom';
2
+
3
+ import { render, screen } from '@testing-library/react';
4
+ import fetchMock from 'jest-fetch-mock';
5
+ import * as React from 'react';
6
+
7
+ import branches from '../__fixtures__/branches.json';
8
+ import nodeContent from '../__fixtures__/node-content.json';
9
+ import tableOfContents from '../__fixtures__/table-of-contents.json';
10
+ import { StoplightProject } from './StoplightProject';
11
+
12
+ describe('Stoplight Project', () => {
13
+ beforeEach(() => {
14
+ fetchMock.mockResponse(request => {
15
+ if (request.url.match(/\/api\/v1\/projects\/[a-zA-Z0-9_.-]+\/table-of-contents/i)) {
16
+ return Promise.resolve({
17
+ body: JSON.stringify(tableOfContents),
18
+ status: 200,
19
+ statusText: 'OK',
20
+ headers: [],
21
+ });
22
+ } else if (request.url.match(/\/api\/v1\/projects\/[a-zA-Z0-9_.-]+\/nodes/i)) {
23
+ return Promise.resolve({
24
+ body: JSON.stringify(nodeContent),
25
+ status: 200,
26
+ statusText: 'OK',
27
+ headers: [],
28
+ });
29
+ } else if (request.url.match(/\/api\/v1\/projects\/[a-zA-Z0-9_.-]+\/branches/i)) {
30
+ return Promise.resolve({
31
+ body: JSON.stringify(branches),
32
+ status: 200,
33
+ statusText: 'OK',
34
+ headers: [],
35
+ });
36
+ } else {
37
+ return Promise.resolve({ status: 404, statusText: 'Not Found', headers: [], body: '' });
38
+ }
39
+ });
40
+ fetchMock.enableMocks();
41
+ });
42
+
43
+ afterEach(() => {
44
+ fetchMock.disableMocks();
45
+ });
46
+
47
+ it('loads correctly using memory router', async () => {
48
+ render(<StoplightProject router="memory" projectId="cHJqOjYwNjYx" platformUrl="https://stoplight.io" />);
49
+
50
+ expect(
51
+ await screen.findByText(
52
+ 'Markdown is supported in descriptions. Add information here for users to get accustomed to endpoints',
53
+ {},
54
+ { timeout: 10000 },
55
+ ),
56
+ ).toBeInTheDocument();
57
+ });
58
+
59
+ it('loads correctly using static router', async () => {
60
+ render(
61
+ <StoplightProject
62
+ router="static"
63
+ projectId="cHJqOjYwNjYx"
64
+ basePath=""
65
+ staticRouterPath="/b3A6Mzg5NDM2-create-todo"
66
+ platformUrl="https://stoplight.io"
67
+ />,
68
+ );
69
+
70
+ expect(
71
+ await screen.findByText(
72
+ 'Markdown is supported in descriptions. Add information here for users to get accustomed to endpoints',
73
+ {},
74
+ { timeout: 10000 },
75
+ ),
76
+ ).toBeInTheDocument();
77
+ });
78
+ });
@@ -0,0 +1,28 @@
1
+ import { Story } from '@storybook/react';
2
+ import * as React from 'react';
3
+
4
+ import { StoplightProject, StoplightProjectProps } from './StoplightProject';
5
+
6
+ export default {
7
+ title: 'Public/StoplightProject',
8
+ component: StoplightProject,
9
+ argTypes: {
10
+ projectId: { table: { category: 'Input' } },
11
+ hideTryIt: { table: { category: 'Input' } },
12
+ hideMocking: { table: { category: 'Input' } },
13
+ basePath: { table: { category: 'Routing' } },
14
+ router: { table: { category: 'Routing' } },
15
+ platformUrl: { table: { category: 'Advanced' } },
16
+ },
17
+ args: {
18
+ router: 'memory',
19
+ platformUrl: 'https://stoplight.io',
20
+ },
21
+ };
22
+
23
+ export const Playground: Story<StoplightProjectProps> = args => <StoplightProject {...args} />;
24
+ Playground.storyName = 'Studio Demo';
25
+ Playground.args = {
26
+ projectId: 'cHJqOjYwNjYx',
27
+ platformUrl: 'https://stoplight.io',
28
+ };