docusaurus-plugin-openapi-docs 1.0.1 → 1.0.2

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 (42) hide show
  1. package/README.md +7 -6
  2. package/lib/index.js +32 -7
  3. package/lib/markdown/createContactInfo.d.ts +2 -0
  4. package/lib/markdown/createContactInfo.js +49 -0
  5. package/lib/markdown/createLicense.d.ts +2 -0
  6. package/lib/markdown/createLicense.js +33 -0
  7. package/lib/markdown/createSchemaDetails.js +4 -1
  8. package/lib/markdown/createStatusCodes.js +1 -1
  9. package/lib/markdown/createTermsOfService.d.ts +1 -0
  10. package/lib/markdown/createTermsOfService.js +32 -0
  11. package/lib/markdown/index.d.ts +1 -1
  12. package/lib/markdown/index.js +8 -2
  13. package/lib/openapi/openapi.d.ts +4 -3
  14. package/lib/openapi/openapi.js +35 -21
  15. package/lib/openapi/types.d.ts +5 -1
  16. package/lib/sidebars/index.d.ts +2 -1
  17. package/lib/sidebars/index.js +72 -19
  18. package/lib/types.d.ts +2 -1
  19. package/package.json +2 -3
  20. package/src/index.ts +44 -8
  21. package/src/markdown/createContactInfo.ts +52 -0
  22. package/src/markdown/createLicense.ts +34 -0
  23. package/src/markdown/createSchemaDetails.ts +6 -2
  24. package/src/markdown/createStatusCodes.ts +1 -1
  25. package/src/markdown/createTermsOfService.ts +30 -0
  26. package/src/markdown/index.ts +9 -2
  27. package/src/openapi/openapi.ts +32 -16
  28. package/src/openapi/types.ts +5 -1
  29. package/src/{markdown/createRequestBodyTable.ts → postman-collection.d.ts} +2 -9
  30. package/src/sidebars/index.ts +90 -20
  31. package/src/types.ts +2 -2
  32. package/lib/markdown/createFullWidthTable.d.ts +0 -2
  33. package/lib/markdown/createFullWidthTable.js +0 -18
  34. package/lib/markdown/createParamsTable.d.ts +0 -7
  35. package/lib/markdown/createParamsTable.js +0 -80
  36. package/lib/markdown/createRequestBodyTable.d.ts +0 -6
  37. package/lib/markdown/createRequestBodyTable.js +0 -14
  38. package/lib/markdown/createSchemaTable.d.ts +0 -14
  39. package/lib/markdown/createSchemaTable.js +0 -217
  40. package/src/markdown/createFullWidthTable.ts +0 -16
  41. package/src/markdown/createParamsTable.ts +0 -102
  42. package/src/markdown/createSchemaTable.ts +0 -275
package/lib/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Request } from "@paloaltonetworks/postman-collection";
1
+ import type Request from "@paloaltonetworks/postman-collection";
2
2
  import { InfoObject, OperationObject, SecuritySchemeObject } from "./openapi/types";
3
3
  export type { PropSidebarItemCategory, SidebarItemLink, PropSidebar, PropSidebarItem, } from "@docusaurus/plugin-content-docs-types";
4
4
  export interface PluginOptions {
@@ -60,6 +60,7 @@ export interface ApiNavLink {
60
60
  }
61
61
  export interface SidebarOptions {
62
62
  groupPathsBy?: string;
63
+ categoryLinkSource?: string;
63
64
  customProps?: {
64
65
  [key: string]: unknown;
65
66
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "docusaurus-plugin-openapi-docs",
3
3
  "description": "OpenAPI plugin for Docusaurus.",
4
- "version": "1.0.1",
4
+ "version": "1.0.2",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "openapi",
@@ -33,7 +33,6 @@
33
33
  "@types/fs-extra": "^9.0.13",
34
34
  "@types/json-schema": "^7.0.9",
35
35
  "@types/lodash": "^4.14.176",
36
- "@types/postman-collection": "^3.5.7",
37
36
  "utility-types": "^3.10.0"
38
37
  },
39
38
  "dependencies": {
@@ -61,5 +60,5 @@
61
60
  "engines": {
62
61
  "node": ">=14"
63
62
  },
64
- "gitHead": "27f594d26440ffe6f890b35c79847e065a96723e"
63
+ "gitHead": "526f2d10bc187bc7095ac70d5b1453b9b92c114f"
65
64
  }
package/src/index.ts CHANGED
@@ -32,8 +32,7 @@ export default function pluginOpenAPI(
32
32
 
33
33
  try {
34
34
  const openapiFiles = await readOpenapiFiles(contentPath, {});
35
- const loadedApi = await processOpenapiFiles(openapiFiles);
36
-
35
+ const [loadedApi, tags] = await processOpenapiFiles(openapiFiles);
37
36
  if (!fs.existsSync(outputDir)) {
38
37
  try {
39
38
  fs.mkdirSync(outputDir, { recursive: true });
@@ -51,7 +50,8 @@ export default function pluginOpenAPI(
51
50
  const sidebarSlice = generateSidebarSlice(
52
51
  sidebarOptions!, // TODO: find a better way to handle null
53
52
  options,
54
- loadedApi
53
+ loadedApi,
54
+ tags
55
55
  );
56
56
 
57
57
  const sidebarSliceTemplate = template
@@ -81,7 +81,12 @@ export default function pluginOpenAPI(
81
81
  ? fs.readFileSync(template).toString()
82
82
  : `---
83
83
  id: {{{id}}}
84
+ {{^api}}
85
+ sidebar_label: Introduction
86
+ {{/api}}
87
+ {{#api}}
84
88
  sidebar_label: {{{title}}}
89
+ {{/api}}
85
90
  {{^api}}
86
91
  sidebar_position: 0
87
92
  {{/api}}
@@ -100,6 +105,24 @@ sidebar_class_name: "{{{api.method}}} api-method"
100
105
  {{{markdown}}}
101
106
  `;
102
107
 
108
+ const infoMdTemplate = template
109
+ ? fs.readFileSync(template).toString()
110
+ : `---
111
+ id: {{{id}}}
112
+ sidebar_label: {{{title}}}
113
+ hide_title: true
114
+ ---
115
+
116
+ {{{markdown}}}
117
+
118
+ \`\`\`mdx-code-block
119
+ import DocCardList from '@theme/DocCardList';
120
+ import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
121
+
122
+ <DocCardList items={useCurrentSidebarCategory().items}/>
123
+ \`\`\`
124
+ `;
125
+
103
126
  loadedApi.map(async (item) => {
104
127
  const markdown =
105
128
  item.type === "api" ? createApiPageMD(item) : createInfoPageMD(item);
@@ -108,6 +131,7 @@ sidebar_class_name: "{{{api.method}}} api-method"
108
131
  item.json = JSON.stringify(item.api);
109
132
  }
110
133
  const view = render(mdTemplate, item);
134
+ const utils = render(infoMdTemplate, item);
111
135
 
112
136
  if (item.type === "api") {
113
137
  if (!fs.existsSync(`${outputDir}/${item.id}.api.mdx`)) {
@@ -129,15 +153,27 @@ sidebar_class_name: "{{{api.method}}} api-method"
129
153
 
130
154
  // TODO: determine if we actually want/need this
131
155
  if (item.type === "info") {
132
- if (!fs.existsSync(`${outputDir}/index.api.mdx`)) {
156
+ if (!fs.existsSync(`${outputDir}/${item.id}.info.mdx`)) {
133
157
  try {
134
- fs.writeFileSync(`${outputDir}/index.api.mdx`, view, "utf8");
158
+ sidebarOptions?.categoryLinkSource === "info" // Only use utils template if set to "info"
159
+ ? fs.writeFileSync(
160
+ `${outputDir}/${item.id}.info.mdx`,
161
+ utils,
162
+ "utf8"
163
+ )
164
+ : fs.writeFileSync(
165
+ `${outputDir}/${item.id}.info.mdx`,
166
+ view,
167
+ "utf8"
168
+ );
135
169
  console.log(
136
- chalk.green(`Successfully created "${outputDir}/index.api.mdx"`)
170
+ chalk.green(
171
+ `Successfully created "${outputDir}/${item.id}.info.mdx"`
172
+ )
137
173
  );
138
174
  } catch (err) {
139
175
  console.error(
140
- chalk.red(`Failed to write "${outputDir}/index.api.mdx"`),
176
+ chalk.red(`Failed to write "${outputDir}/${item.id}.info.mdx"`),
141
177
  chalk.yellow(err)
142
178
  );
143
179
  }
@@ -155,7 +191,7 @@ sidebar_class_name: "{{{api.method}}} api-method"
155
191
  async function cleanApiDocs(options: APIOptions) {
156
192
  const { outputDir } = options;
157
193
  const apiDir = path.join(siteDir, outputDir);
158
- const apiMdxFiles = await Globby(["*.api.mdx"], {
194
+ const apiMdxFiles = await Globby(["*.api.mdx", "*.info.mdx"], {
159
195
  cwd: path.resolve(apiDir),
160
196
  });
161
197
  const sidebarFile = await Globby(["sidebar.js"], {
@@ -0,0 +1,52 @@
1
+ /* ============================================================================
2
+ * Copyright (c) Palo Alto Networks
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ * ========================================================================== */
7
+
8
+ import { ContactObject } from "../openapi/types";
9
+ import { create, guard } from "./utils";
10
+
11
+ export function createContactInfo(contact: ContactObject) {
12
+ if (!contact || !Object.keys(contact).length) return "";
13
+ const { name, url, email } = contact;
14
+
15
+ return create("div", {
16
+ style: {
17
+ display: "flex",
18
+ flexDirection: "column",
19
+ marginBottom: "var(--ifm-paragraph-margin-bottom)",
20
+ },
21
+ children: [
22
+ create("h3", {
23
+ style: {
24
+ marginBottom: "0.25rem",
25
+ },
26
+ children: "Contact",
27
+ }),
28
+ create("span", {
29
+ children: [
30
+ guard(name, () => `${name}: `),
31
+ guard(email, () =>
32
+ create("a", {
33
+ href: `mailto:${email}`,
34
+ children: `${email}`,
35
+ })
36
+ ),
37
+ ],
38
+ }),
39
+ guard(url, () =>
40
+ create("span", {
41
+ children: [
42
+ "URL: ",
43
+ create("a", {
44
+ href: `${url}`,
45
+ children: `${url}`,
46
+ }),
47
+ ],
48
+ })
49
+ ),
50
+ ],
51
+ });
52
+ }
@@ -0,0 +1,34 @@
1
+ /* ============================================================================
2
+ * Copyright (c) Palo Alto Networks
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ * ========================================================================== */
7
+
8
+ import { LicenseObject } from "../openapi/types";
9
+ import { create, guard } from "./utils";
10
+
11
+ export function createLicense(license: LicenseObject) {
12
+ if (!license || !Object.keys(license).length) return "";
13
+ const { name, url } = license;
14
+
15
+ return create("div", {
16
+ style: {
17
+ marginBottom: "var(--ifm-paragraph-margin-bottom)",
18
+ },
19
+ children: [
20
+ create("h3", {
21
+ style: {
22
+ marginBottom: "0.25rem",
23
+ },
24
+ children: "License",
25
+ }),
26
+ guard(url, () =>
27
+ create("a", {
28
+ href: url,
29
+ children: name ?? url,
30
+ })
31
+ ),
32
+ ],
33
+ });
34
+ }
@@ -242,7 +242,12 @@ interface Props {
242
242
  }
243
243
 
244
244
  export function createSchemaDetails({ title, body, ...rest }: Props) {
245
- if (body === undefined || body.content === undefined) {
245
+ if (
246
+ body === undefined ||
247
+ body.content === undefined ||
248
+ Object.keys(body).length === 0 ||
249
+ Object.keys(body.content).length === 0
250
+ ) {
246
251
  return undefined;
247
252
  }
248
253
 
@@ -250,7 +255,6 @@ export function createSchemaDetails({ title, body, ...rest }: Props) {
250
255
  // NOTE: We just pick a random content-type.
251
256
  // How common is it to have multiple?
252
257
  const randomFirstKey = Object.keys(body.content)[0];
253
-
254
258
  const firstBody = body.content[randomFirstKey].schema;
255
259
 
256
260
  if (firstBody === undefined) {
@@ -26,7 +26,7 @@ export function createStatusCodes({ responses }: Props) {
26
26
 
27
27
  return create("div", {
28
28
  children: [
29
- create("Tabs", {
29
+ create("ApiTabs", {
30
30
  children: codes.map((code) => {
31
31
  return create("TabItem", {
32
32
  label: code,
@@ -0,0 +1,30 @@
1
+ /* ============================================================================
2
+ * Copyright (c) Palo Alto Networks
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ * ========================================================================== */
7
+
8
+ import { create } from "./utils";
9
+
10
+ export function createTermsOfService(termsOfService: string | undefined) {
11
+ if (!termsOfService) return "";
12
+
13
+ return create("div", {
14
+ style: {
15
+ marginBottom: "var(--ifm-paragraph-margin-bottom)",
16
+ },
17
+ children: [
18
+ create("h3", {
19
+ style: {
20
+ marginBottom: "0.25rem",
21
+ },
22
+ children: "Terms of Service",
23
+ }),
24
+ create("a", {
25
+ href: `${termsOfService}`,
26
+ children: termsOfService,
27
+ }),
28
+ ],
29
+ });
30
+ }
@@ -7,12 +7,16 @@
7
7
 
8
8
  import { escape } from "lodash";
9
9
 
10
+ import { ContactObject, LicenseObject } from "../openapi/types";
10
11
  import { ApiPageMetadata, InfoPageMetadata } from "../types";
12
+ import { createContactInfo } from "./createContactInfo";
11
13
  import { createDeprecationNotice } from "./createDeprecationNotice";
12
14
  import { createDescription } from "./createDescription";
15
+ import { createLicense } from "./createLicense";
13
16
  import { createParamsDetails } from "./createParamsDetails";
14
17
  import { createRequestBodyDetails } from "./createRequestBodyDetails";
15
18
  import { createStatusCodes } from "./createStatusCodes";
19
+ import { createTermsOfService } from "./createTermsOfService";
16
20
  import { createVersionBadge } from "./createVersionBadge";
17
21
  import { render } from "./utils";
18
22
 
@@ -30,7 +34,7 @@ export function createApiPageMD({
30
34
  return render([
31
35
  `import ParamsItem from "@theme/ParamsItem";\n`,
32
36
  `import SchemaItem from "@theme/SchemaItem"\n`,
33
- `import Tabs from "@theme/Tabs";\n`,
37
+ `import ApiTabs from "@theme/ApiTabs";\n`,
34
38
  `import TabItem from "@theme/TabItem";\n\n`,
35
39
  `## ${escape(title)}\n\n`,
36
40
  createDeprecationNotice({ deprecated, description: deprecatedDescription }),
@@ -45,11 +49,14 @@ export function createApiPageMD({
45
49
  }
46
50
 
47
51
  export function createInfoPageMD({
48
- info: { title, version, description },
52
+ info: { title, version, description, contact, license, termsOfService },
49
53
  }: InfoPageMetadata) {
50
54
  return render([
51
55
  createVersionBadge(version),
52
56
  `# ${escape(title)}\n\n`,
53
57
  createDescription(description),
58
+ createContactInfo(contact as ContactObject),
59
+ createTermsOfService(termsOfService),
60
+ createLicense(license as LicenseObject),
54
61
  ]);
55
62
  }
@@ -9,8 +9,8 @@ import path from "path";
9
9
 
10
10
  import { Globby, GlobExcludeDefault } from "@docusaurus/utils";
11
11
  import Converter from "@paloaltonetworks/openapi-to-postmanv2";
12
- // @ts-ignore
13
- import sdk, { Collection } from "@paloaltonetworks/postman-collection";
12
+ import sdk from "@paloaltonetworks/postman-collection";
13
+ import Collection from "@paloaltonetworks/postman-collection";
14
14
  import chalk from "chalk";
15
15
  import fs from "fs-extra";
16
16
  import yaml from "js-yaml";
@@ -81,16 +81,20 @@ function createItems(openapiData: OpenApiObject): ApiMetadata[] {
81
81
 
82
82
  // Only create an info page if we have a description.
83
83
  if (openapiData.info.description) {
84
+ const infoId = kebabCase(openapiData.info.title);
84
85
  const infoPage: PartialPage<InfoPageMetadata> = {
85
86
  type: "info",
86
- id: "introduction",
87
- unversionedId: "introduction",
88
- title: "Introduction",
87
+ id: infoId,
88
+ unversionedId: infoId,
89
+ title: openapiData.info.title,
89
90
  description: openapiData.info.description,
90
- slug: "/introduction",
91
+ slug: "/" + infoId,
91
92
  frontMatter: {},
92
93
  info: {
93
94
  ...openapiData.info,
95
+ tags: openapiData.tags?.map((tagName) =>
96
+ getTagDisplayName(tagName.name!, openapiData.tags ?? [])
97
+ ),
94
98
  title: openapiData.info.title ?? "Introduction",
95
99
  },
96
100
  };
@@ -175,8 +179,7 @@ function bindCollectionToApiItems(
175
179
  items: ApiMetadata[],
176
180
  postmanCollection: sdk.Collection
177
181
  ) {
178
- // @ts-ignore
179
- postmanCollection.forEachItem((item) => {
182
+ postmanCollection.forEachItem((item: any) => {
180
183
  const method = item.request.method.toLowerCase();
181
184
  const path = item.request.url
182
185
  .getPath({ unresolved: true }) // unresolved returns "/:variableName" instead of "/<type>"
@@ -246,34 +249,47 @@ export async function readOpenapiFiles(
246
249
 
247
250
  export async function processOpenapiFiles(
248
251
  files: OpenApiFiles[]
249
- ): Promise<ApiMetadata[]> {
252
+ ): Promise<[ApiMetadata[], TagObject[]]> {
250
253
  const promises = files.map(async (file) => {
251
- const items = await processOpenapiFile(file.data);
252
- return items.map((item) => ({
254
+ const processedFile = await processOpenapiFile(file.data);
255
+ const itemsObjectsArray = processedFile[0].map((item) => ({
253
256
  ...item,
254
257
  }));
258
+ const tags = processedFile[1];
259
+ return [itemsObjectsArray, tags];
255
260
  });
256
261
  const metadata = await Promise.all(promises);
257
- const items = metadata.flat();
258
- return items;
262
+ const items = metadata
263
+ .map(function (x) {
264
+ return x[0];
265
+ })
266
+ .flat();
267
+ const tags = metadata.map(function (x) {
268
+ return x[1];
269
+ });
270
+ return [items as ApiMetadata[], tags as TagObject[]];
259
271
  }
260
272
 
261
273
  export async function processOpenapiFile(
262
274
  openapiDataWithRefs: OpenApiObjectWithRef
263
- ): Promise<ApiMetadata[]> {
275
+ ): Promise<[ApiMetadata[], TagObject[]]> {
264
276
  const openapiData = await resolveRefs(openapiDataWithRefs);
265
277
  const postmanCollection = await createPostmanCollection(openapiData);
266
278
  const items = createItems(openapiData);
267
279
 
268
280
  bindCollectionToApiItems(items, postmanCollection);
269
281
 
270
- return items;
282
+ let tags: TagObject[] = [];
283
+ if (openapiData.tags !== undefined) {
284
+ tags = openapiData.tags;
285
+ }
286
+ return [items, tags];
271
287
  }
272
288
 
273
289
  // order for picking items as a display name of tags
274
290
  const tagDisplayNameProperties = ["x-displayName", "name"] as const;
275
291
 
276
- function getTagDisplayName(tagName: string, tags: TagObject[]): string {
292
+ export function getTagDisplayName(tagName: string, tags: TagObject[]): string {
277
293
  // find the very own tagObject
278
294
  const tagObject = tags.find((tagObject) => tagObject.name === tagName) ?? {
279
295
  // if none found, just fake one
@@ -40,6 +40,7 @@ export interface InfoObject {
40
40
  contact?: ContactObject;
41
41
  license?: LicenseObject;
42
42
  version: string;
43
+ tags?: String[];
43
44
  }
44
45
 
45
46
  export interface ContactObject {
@@ -182,6 +183,7 @@ export interface ParameterObject {
182
183
  examples?: Map<ExampleObject>;
183
184
  //
184
185
  content?: Map<MediaTypeObject>;
186
+ param?: Object;
185
187
  // ignoring stylings: matrix, label, form, simple, spaceDelimited,
186
188
  // pipeDelimited and deepObject
187
189
  }
@@ -293,7 +295,7 @@ export type HeaderObject = Omit<ParameterObject, "name" | "in">;
293
295
  export type HeaderObjectWithRef = Omit<ParameterObjectWithRef, "name" | "in">;
294
296
 
295
297
  export interface TagObject {
296
- name: string;
298
+ name?: string;
297
299
  description?: string;
298
300
  externalDocs?: ExternalDocumentationObject;
299
301
  "x-displayName"?: string;
@@ -399,6 +401,8 @@ export interface HttpSecuritySchemeObject {
399
401
  description?: string;
400
402
  scheme: string;
401
403
  bearerFormat?: string;
404
+ name?: string;
405
+ in?: string;
402
406
  }
403
407
 
404
408
  export interface Oauth2SecuritySchemeObject {
@@ -5,13 +5,6 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  * ========================================================================== */
7
7
 
8
- import { createSchemaTable } from "./createSchemaTable";
9
-
10
- interface Props {
11
- title: string;
12
- body: any;
13
- }
14
-
15
- export function createRequestBodyTable({ title, body }: Props) {
16
- return createSchemaTable({ title, body });
8
+ declare module "@paloaltonetworks/postman-collection" {
9
+ export default any;
17
10
  }
@@ -7,11 +7,14 @@
7
7
 
8
8
  import {
9
9
  ProcessedSidebar,
10
+ SidebarItemCategoryLinkConfig,
10
11
  SidebarItemDoc,
11
12
  } from "@docusaurus/plugin-content-docs/src/sidebars/types";
12
13
  import clsx from "clsx";
14
+ import { kebabCase } from "lodash";
13
15
  import uniq from "lodash/uniq";
14
16
 
17
+ import { TagObject } from "../openapi/types";
15
18
  import type {
16
19
  SidebarOptions,
17
20
  APIOptions,
@@ -23,28 +26,37 @@ function isApiItem(item: ApiMetadata): item is ApiMetadata {
23
26
  return item.type === "api";
24
27
  }
25
28
 
29
+ function isInfoItem(item: ApiMetadata): item is ApiMetadata {
30
+ return item.type === "info";
31
+ }
32
+
26
33
  function groupByTags(
27
34
  items: ApiPageMetadata[],
28
35
  sidebarOptions: SidebarOptions,
29
- options: APIOptions
36
+ options: APIOptions,
37
+ tags: TagObject[]
30
38
  ): ProcessedSidebar {
31
- // TODO: Figure out how to handle these
32
- // const intros = items.filter(isInfoItem).map((item) => {
33
- // return {
34
- // type: "link" as const,
35
- // label: item.title,
36
- // href: item.permalink,
37
- // docId: item.id,
38
- // };
39
- // });
40
-
41
39
  const { outputDir } = options;
42
- const { sidebarCollapsed, sidebarCollapsible, customProps } = sidebarOptions;
40
+ const {
41
+ sidebarCollapsed,
42
+ sidebarCollapsible,
43
+ customProps,
44
+ categoryLinkSource,
45
+ } = sidebarOptions;
43
46
 
44
47
  const apiItems = items.filter(isApiItem);
48
+ const infoItems = items.filter(isInfoItem);
49
+ const intros = infoItems.map((item: any) => {
50
+ return {
51
+ id: item.id,
52
+ title: item.title,
53
+ description: item.description,
54
+ tags: item.info.tags,
55
+ };
56
+ });
45
57
 
46
58
  // TODO: make sure we only take the first tag
47
- const tags = uniq(
59
+ const apiTags = uniq(
48
60
  apiItems
49
61
  .flatMap((item) => item.api.tags)
50
62
  .filter((item): item is string => !!item)
@@ -74,11 +86,61 @@ function groupByTags(
74
86
  };
75
87
  }
76
88
 
77
- const tagged = tags
89
+ let rootIntroDoc = undefined;
90
+ if (infoItems.length === 1) {
91
+ const infoItem = infoItems[0];
92
+ const id = infoItem.id;
93
+ rootIntroDoc = {
94
+ type: "doc" as const,
95
+ id: `${basePath}/${id}`,
96
+ };
97
+ }
98
+
99
+ const tagged = apiTags
78
100
  .map((tag) => {
101
+ // Map info object to tag
102
+ const infoObject = intros.find((i) => i.tags.includes(tag));
103
+ const tagObject = tags.flat().find(
104
+ (t) =>
105
+ (tag === t.name || tag === t["x-displayName"]) ?? {
106
+ name: tag,
107
+ description: `${tag} Index`,
108
+ }
109
+ );
110
+
111
+ // TODO: perhaps move this into a getLinkConfig() function
112
+ let linkConfig = undefined;
113
+ if (infoObject !== undefined && categoryLinkSource === "info") {
114
+ linkConfig = {
115
+ type: "doc",
116
+ id: `${basePath}/${infoObject.id}`,
117
+ } as SidebarItemCategoryLinkConfig;
118
+ }
119
+
120
+ // TODO: perhaps move this into a getLinkConfig() function
121
+ if (tagObject !== undefined && categoryLinkSource === "tag") {
122
+ const linkDescription = tagObject?.description;
123
+ linkConfig = {
124
+ type: "generated-index" as "generated-index",
125
+ title: tag,
126
+ description: linkDescription,
127
+ slug: "/category/" + kebabCase(tag),
128
+ } as SidebarItemCategoryLinkConfig;
129
+ }
130
+
131
+ // Default behavior
132
+ if (categoryLinkSource === undefined) {
133
+ linkConfig = {
134
+ type: "generated-index" as "generated-index",
135
+ title: tag,
136
+ slug: "/category/" + kebabCase(tag),
137
+ } as SidebarItemCategoryLinkConfig;
138
+ }
139
+
79
140
  return {
80
141
  type: "category" as const,
81
142
  label: tag,
143
+ link: linkConfig,
82
144
  collapsible: sidebarCollapsible,
83
145
  collapsed: sidebarCollapsed,
84
146
  items: apiItems
@@ -88,33 +150,41 @@ function groupByTags(
88
150
  })
89
151
  .filter((item) => item.items.length > 0); // Filter out any categories with no items.
90
152
 
153
+ // TODO: determine how we want to handle these
91
154
  // const untagged = [
92
- // // TODO: determine if needed and how
93
155
  // {
94
156
  // type: "category" as const,
95
157
  // label: "UNTAGGED",
96
- // // collapsible: options.sidebarCollapsible, TODO: add option
97
- // // collapsed: options.sidebarCollapsed, TODO: add option
158
+ // collapsible: sidebarCollapsible,
159
+ // collapsed: sidebarCollapsed,
98
160
  // items: apiItems
99
- // //@ts-ignore
100
161
  // .filter(({ api }) => api.tags === undefined || api.tags.length === 0)
101
162
  // .map(createDocItem),
102
163
  // },
103
164
  // ];
165
+
166
+ // Shift root intro doc to top of sidebar
167
+ // TODO: Add input validation for categoryLinkSource options
168
+ if (rootIntroDoc && categoryLinkSource !== "info") {
169
+ tagged.unshift(rootIntroDoc as any);
170
+ }
171
+
104
172
  return [...tagged];
105
173
  }
106
174
 
107
175
  export default function generateSidebarSlice(
108
176
  sidebarOptions: SidebarOptions,
109
177
  options: APIOptions,
110
- api: ApiMetadata[]
178
+ api: ApiMetadata[],
179
+ tags: TagObject[]
111
180
  ) {
112
181
  let sidebarSlice: ProcessedSidebar = [];
113
182
  if (sidebarOptions.groupPathsBy === "tags") {
114
183
  sidebarSlice = groupByTags(
115
184
  api as ApiPageMetadata[],
116
185
  sidebarOptions,
117
- options
186
+ options,
187
+ tags
118
188
  );
119
189
  }
120
190
  return sidebarSlice;
package/src/types.ts CHANGED
@@ -5,8 +5,7 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  * ========================================================================== */
7
7
 
8
- // @ts-ignore
9
- import type { Request } from "@paloaltonetworks/postman-collection";
8
+ import type Request from "@paloaltonetworks/postman-collection";
10
9
 
11
10
  import {
12
11
  InfoObject,
@@ -91,6 +90,7 @@ export interface ApiNavLink {
91
90
 
92
91
  export interface SidebarOptions {
93
92
  groupPathsBy?: string;
93
+ categoryLinkSource?: string;
94
94
  customProps?: { [key: string]: unknown };
95
95
  sidebarCollapsible?: boolean;
96
96
  sidebarCollapsed?: boolean;