@salesforcedevs/docs-components 0.56.2 → 0.56.3-example

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 (80) hide show
  1. package/lwc.config.json +10 -2
  2. package/package.json +19 -13
  3. package/src/modules/doc/{amfReference/utils.ts → amfModelParser/amfModelParser.ts} +10 -5
  4. package/src/modules/doc/amfReference/amfReference.css +23 -3
  5. package/src/modules/doc/amfReference/amfReference.html +42 -21
  6. package/src/modules/doc/amfReference/amfReference.ts +1022 -341
  7. package/src/modules/doc/amfReference/constants.ts +76 -0
  8. package/src/modules/doc/amfReference/types.ts +45 -13
  9. package/src/modules/doc/amfTopic/amfTopic.css +20 -0
  10. package/src/modules/doc/amfTopic/amfTopic.ts +35 -18
  11. package/src/modules/doc/amfTopic/types.ts +15 -13
  12. package/src/modules/doc/amfTopic/utils.ts +12 -6
  13. package/src/modules/doc/breadcrumbItem/breadcrumbItem.css +2 -1
  14. package/src/modules/doc/breadcrumbItem/breadcrumbItem.html +1 -1
  15. package/src/modules/doc/breadcrumbItem/breadcrumbItem.ts +22 -0
  16. package/src/modules/doc/breadcrumbs/breadcrumbs.css +9 -1
  17. package/src/modules/doc/breadcrumbs/breadcrumbs.html +44 -34
  18. package/src/modules/doc/breadcrumbs/breadcrumbs.ts +62 -23
  19. package/src/modules/doc/componentPlayground/componentPlayground.css +22 -0
  20. package/src/modules/doc/componentPlayground/componentPlayground.html +20 -0
  21. package/src/modules/doc/componentPlayground/componentPlayground.ts +42 -0
  22. package/src/modules/doc/content/content.css +70 -76
  23. package/src/modules/doc/content/content.html +1 -0
  24. package/src/modules/doc/content/content.ts +26 -47
  25. package/src/modules/doc/contentCallout/contentCallout.css +15 -7
  26. package/src/modules/doc/contentCallout/contentCallout.html +13 -4
  27. package/src/modules/doc/contentCallout/contentCallout.ts +14 -2
  28. package/src/modules/doc/contentLayout/contentLayout.css +1 -100
  29. package/src/modules/doc/contentLayout/contentLayout.html +30 -17
  30. package/src/modules/doc/contentLayout/contentLayout.ts +354 -65
  31. package/src/modules/doc/doDont/doDont.css +47 -0
  32. package/src/modules/doc/doDont/doDont.html +27 -0
  33. package/src/modules/doc/doDont/doDont.ts +17 -0
  34. package/src/modules/doc/header/header.css +65 -36
  35. package/src/modules/doc/header/header.html +41 -139
  36. package/src/modules/doc/header/header.ts +54 -76
  37. package/src/modules/doc/heading/heading.css +16 -37
  38. package/src/modules/doc/heading/heading.html +4 -4
  39. package/src/modules/doc/heading/heading.ts +12 -10
  40. package/src/modules/doc/headingAnchor/headingAnchor.css +2 -2
  41. package/src/modules/doc/headingAnchor/headingAnchor.ts +2 -2
  42. package/src/modules/doc/headingContent/headingContent.css +6 -8
  43. package/src/modules/doc/headingContent/headingContent.html +9 -15
  44. package/src/modules/doc/headingContent/headingContent.ts +2 -14
  45. package/src/modules/doc/lwcContentLayout/lwcContentLayout.css +1 -0
  46. package/src/modules/doc/lwcContentLayout/lwcContentLayout.html +68 -0
  47. package/src/modules/doc/lwcContentLayout/lwcContentLayout.ts +256 -0
  48. package/src/modules/doc/overview/overview.css +40 -0
  49. package/src/modules/doc/overview/overview.html +34 -0
  50. package/src/modules/doc/overview/overview.ts +12 -0
  51. package/src/modules/doc/phase/phase.css +18 -3
  52. package/src/modules/doc/phase/phase.html +15 -3
  53. package/src/modules/doc/phase/phase.ts +44 -8
  54. package/src/modules/doc/specificationContent/specificationContent.css +36 -0
  55. package/src/modules/doc/specificationContent/specificationContent.html +167 -0
  56. package/src/modules/doc/specificationContent/specificationContent.ts +127 -0
  57. package/src/modules/doc/sprigSurvey/sprigSurvey.html +20 -0
  58. package/src/modules/doc/sprigSurvey/sprigSurvey.scoped.css +16 -0
  59. package/src/modules/doc/sprigSurvey/sprigSurvey.ts +16 -0
  60. package/src/modules/doc/toc/toc.ts +1 -1
  61. package/src/modules/doc/versionPicker/versionPicker.css +64 -0
  62. package/src/modules/doc/versionPicker/versionPicker.html +38 -0
  63. package/src/modules/doc/versionPicker/versionPicker.ts +65 -0
  64. package/src/modules/doc/xmlContent/types.ts +12 -3
  65. package/src/modules/doc/xmlContent/utils.ts +17 -12
  66. package/src/modules/doc/xmlContent/xmlContent.css +32 -3
  67. package/src/modules/doc/xmlContent/xmlContent.html +41 -15
  68. package/src/modules/doc/xmlContent/xmlContent.ts +295 -95
  69. package/src/modules/docHelpers/amfStyle/amfStyle.css +10 -45
  70. package/src/modules/docHelpers/contentLayoutStyle/contentLayoutStyle.css +131 -0
  71. package/src/modules/docHelpers/imgStyle/imgStyle.css +59 -0
  72. package/src/modules/docHelpers/status/status.css +1 -1
  73. package/src/modules/docUtils/{SearchSyncer/SearchSyncer.ts → searchSyncer/searchSyncer.ts} +1 -0
  74. package/src/modules/docUtils/utils/__mocks__/coveo.analytics.ts +16 -0
  75. package/src/modules/docUtils/utils/coveo.analytics.d.ts +10 -0
  76. package/src/modules/docUtils/utils/utils.ts +32 -0
  77. package/LICENSE +0 -12
  78. package/src/modules/doc/amfReference/route-meta.ts +0 -22
  79. package/src/modules/docBaseElements/lightningElementWithState/lightningElementWithState.ts +0 -93
  80. package/src/modules/docHelpers/phaseContentLayout/phaseContentLayout.css +0 -39
@@ -2,8 +2,9 @@ import { LightningElement, api, track } from "lwc";
2
2
  import { noCase } from "no-case";
3
3
  import { sentenceCase } from "sentence-case";
4
4
  import qs from "query-string";
5
- import { AmfModelParser } from "./utils";
6
- import { RouteMeta } from "./route-meta";
5
+ import { AmfModelParser } from "doc/amfModelParser";
6
+ import { normalizeBoolean, toJson } from "dxUtils/normalizers";
7
+ import type { OptionWithLink } from "typings/custom";
7
8
  import type {
8
9
  AmfConfig,
9
10
  AmfMetadataTopic,
@@ -14,94 +15,62 @@ import type {
14
15
  TopicModel,
15
16
  ReferenceVersion,
16
17
  ReferenceSetConfig,
17
- AmfMetaTopicType
18
+ AmfMetaTopicType,
19
+ RouteMeta,
20
+ ParsedMarkdownTopic
18
21
  } from "./types";
19
22
 
20
- const NAVIGATION_ITEMS = [
21
- {
22
- label: "Summary",
23
- name: "summary",
24
- childrenPropertyName: undefined,
25
- type: "summary"
26
- },
27
- {
28
- label: "Endpoints",
29
- name: "endpoints",
30
- childrenPropertyName: "endpoints",
31
- type: "endpoint"
32
- },
33
- {
34
- label: "Documentation",
35
- name: "documentation",
36
- childrenPropertyName: "docs",
37
- type: "documentation"
38
- },
39
- {
40
- label: "Types",
41
- name: "types",
42
- childrenPropertyName: "types",
43
- type: "type"
44
- },
45
- {
46
- label: "Security",
47
- name: "security",
48
- childrenPropertyName: "security",
49
- type: "security"
50
- }
51
- ];
52
-
53
- const URL_CONFIG = {
54
- summary: {
55
- urlIdentifer: "label"
56
- },
57
- endpoint: {
58
- urlIdentifer: "path"
59
- },
60
- method: {
61
- urlIdentifer: "label"
62
- },
63
- documentation: {
64
- urlIdentifer: "label"
65
- },
66
- type: {
67
- urlIdentifer: "label",
68
- prefix: "type:"
69
- },
70
- security: {
71
- urlIdentifer: "label",
72
- prefix: "security:"
73
- }
74
- };
75
-
76
- const urlHashToMetaRedirectMap = {
77
- "commerce-api-assignments:Summary": "assignments:Summary",
78
- "commerce-api-campaigns:Summary": "campaigns:Summary",
79
- "commerce-api-catalogs:Summary": "catalogs:Summary",
80
- "cdn-zones:Summary": "cdn-api-process-apis:Summary",
81
- "inventory-impex:Summary": "impex:Summary",
82
- "inventory-reservations:Summary": "inventory-reservation-service:Summary",
83
- "shopper-login-and-api-access-service:Summary": "shopper-login:Summary",
84
- "shopper-login-and-api-access-service-admin:Summary": "slas-admin:Summary",
85
- "einstein-recommendations:Summary": "einstein-api-quick-start-guide:Summary"
23
+ import {
24
+ NAVIGATION_ITEMS,
25
+ URL_CONFIG,
26
+ REFERENCE_TYPES,
27
+ oldReferenceIdNewReferenceIdMap
28
+ } from "./constants";
29
+ import { restoreScroll } from "dx/scrollManager";
30
+ import { DocPhaseInfo } from "typings/custom";
31
+ import { logCoveoPageView, oldVersionDocInfo } from "docUtils/utils";
32
+
33
+ type NavigationItem = {
34
+ label: string;
35
+ name: string;
36
+ isExpanded: boolean;
37
+ children: ParsedMarkdownTopic[];
38
+ isChildrenLoading: boolean;
86
39
  };
87
40
 
88
41
  export default class AmfReference extends LightningElement {
89
- @api breadcrumbs?: string = null;
90
- @api sidebarHeader: string;
42
+ @api breadcrumbs: string | null = null;
43
+ @api sidebarHeader!: string;
91
44
  @api coveoOrganizationId!: string;
92
45
  @api coveoPublicAccessToken!: string;
93
- @api coveoAdvancedQueryConfig!: string;
46
+ @api coveoAnalyticsToken!: string;
94
47
  @api coveoSearchHub!: string;
95
- @api useOldSidebar?: boolean = false;
48
+ @api useOldSidebar: boolean = false;
49
+ @api tocTitle?: string;
50
+ @api tocOptions?: string;
51
+ @api languages!: OptionWithLink[];
52
+ @api language!: string;
53
+ @api hideFooter = false;
54
+ @track navigation = [] as NavigationItem[];
55
+ @track versions: Array<ReferenceVersion> = [];
56
+ @track showVersionBanner = false;
57
+ @track _coveoAdvancedQueryConfig!: { [key: string]: any };
96
58
 
97
59
  // Update this to update what component gets rendered in the content block
98
60
  @track
99
- protected topicModel: TopicModel;
61
+ protected topicModel!: TopicModel;
100
62
 
101
63
  get isVersionEnabled(): boolean {
102
64
  return !!this._referenceSetConfig?.versions?.length;
103
65
  }
104
66
 
67
+ /**
68
+ * Gives if the currently selected reference is spec based or not
69
+ */
70
+ get showSpecBasedReference(): boolean {
71
+ return this.isSpecBasedReference(this._currentReferenceId);
72
+ }
73
+
105
74
  @api
106
75
  get referenceSetConfig(): ReferenceSetConfig {
107
76
  return this._referenceSetConfig;
@@ -122,27 +91,57 @@ export default class AmfReference extends LightningElement {
122
91
  this._referenceSetConfig = refConfig;
123
92
  } catch (e) {
124
93
  this._referenceSetConfig = {
125
- refList: []
94
+ refList: [],
95
+ versions: []
126
96
  };
127
97
  }
128
98
 
99
+ this._amfConfigList = this._referenceSetConfig.refList || [];
100
+
101
+ this._amfConfigList.forEach((amfConfig) => {
102
+ this._amfConfigMap.set(amfConfig.id, amfConfig);
103
+ });
104
+
105
+ if (this._amfConfigList.length > 0) {
106
+ this._currentReferenceId =
107
+ this._referenceSetConfig.refId || this._amfConfigList[0].id;
108
+ }
109
+
129
110
  if (this.isVersionEnabled) {
130
111
  const selectedVersion = this.getSelectedVersion();
131
- this.versionToRefMap = this._referenceSetConfig.versionToRefMap;
132
- // if version is not available, then show empty - this will be the default behaviour
133
- this._amfConfig = this.versionToRefMap[selectedVersion.id] || [];
134
- this.versions = this._referenceSetConfig.versions;
112
+
113
+ /**
114
+ * If current selected is markdown based reference,
115
+ * We will assign versions once the selected item url is updated
116
+ */
117
+ if (this.isSpecBasedReference(this._currentReferenceId)) {
118
+ this.versions = this.getVersions();
119
+ }
135
120
  this.selectedVersion = selectedVersion;
121
+ if (this.isSpecBasedReference(this._currentReferenceId)) {
122
+ this.isVersionFetched = true;
123
+ if (this.oldVersionInfo) {
124
+ this.showVersionBanner = true;
125
+ } else {
126
+ this.latestVersion = true;
127
+ }
128
+ }
136
129
  } else {
137
- this._amfConfig = this._referenceSetConfig.refList;
130
+ this.isVersionFetched = true;
138
131
  }
139
132
 
140
- this.updateAmfConfigInView();
133
+ // This is to check if the url is hash based and redirect if needed
134
+ const redirectUrl = this.getHashBasedRedirectUrl();
135
+ if (redirectUrl) {
136
+ window.location.href = redirectUrl;
137
+ } else {
138
+ this.updateAmfConfigInView();
139
+ }
141
140
  }
142
141
 
143
142
  @api
144
- get docPhaseInfo() {
145
- return this.selectedReferenceDocPhase;
143
+ get docPhaseInfo(): string | null {
144
+ return this.selectedReferenceDocPhase || null;
146
145
  }
147
146
 
148
147
  set docPhaseInfo(value: string) {
@@ -152,53 +151,83 @@ export default class AmfReference extends LightningElement {
152
151
  }
153
152
  }
154
153
 
154
+ @api
155
+ get expandChildren() {
156
+ return this._expandChildren;
157
+ }
158
+
159
+ set expandChildren(value) {
160
+ this._expandChildren = normalizeBoolean(value);
161
+ }
162
+
163
+ /*
164
+ * The get coveoAdvancedQueryConfig() method returns this._coveoAdvancedQueryConfig,
165
+ * but before returning it, it checks if there are multiple versions (this.versions.length > 1)
166
+ * and if a version is selected (this.selectedVersion). If both conditions are met,
167
+ * it updates the version property of this._coveoAdvancedQueryConfig with the selected version.
168
+ */
169
+ @api
170
+ get coveoAdvancedQueryConfig(): { [key: string]: any } {
171
+ const coveoConfig = this._coveoAdvancedQueryConfig;
172
+ if (this.versions.length > 1 && this.selectedVersion) {
173
+ const currentGAVersionRef = this.versions[0];
174
+ if (this.selectedVersion.id !== currentGAVersionRef.id) {
175
+ // Currently Coveo only supports query without "v"
176
+ const version = this.selectedVersion.id.replace("v", "");
177
+ coveoConfig.version = version;
178
+ this._coveoAdvancedQueryConfig = coveoConfig;
179
+ }
180
+ }
181
+ return this._coveoAdvancedQueryConfig;
182
+ }
183
+
184
+ set coveoAdvancedQueryConfig(config) {
185
+ this._coveoAdvancedQueryConfig = toJson(config);
186
+ }
187
+
188
+ private get enableFooter(): boolean {
189
+ return !this.hideFooter;
190
+ }
191
+
155
192
  // model
156
- protected _amfConfig: AmfConfig[] = [];
157
- protected _referenceSetConfig: ReferenceSetConfig;
193
+ protected _amfConfigList: AmfConfig[] = [];
194
+ protected _amfConfigMap: Map<string, AmfConfig> = new Map();
195
+ protected _referenceSetConfig!: ReferenceSetConfig;
158
196
  protected _currentReferenceId = "";
159
197
 
198
+ protected parentReferenceUrls = [] as string[];
160
199
  protected amfMap: Record<string, AmfModelRecord> = {};
161
- protected amfFetchPromiseMap = {};
200
+ protected amfFetchPromiseMap = {} as any;
162
201
  protected metadata: { [key: string]: AmfMetadataTopic } = {};
163
- protected selectedTopic: AmfMetaTopicType = undefined;
164
- protected navigation = [];
165
- protected selectedSidebarValue = undefined;
166
- protected versions: Array<ReferenceVersion> = [];
167
- protected selectedVersion: ReferenceVersion = null;
202
+ protected selectedTopic?: AmfMetaTopicType = undefined;
203
+ protected selectedSidebarValue: string | undefined = undefined;
168
204
 
169
- private hasRendered = false;
170
- private navAmfOrder = [];
205
+ protected selectedVersion: ReferenceVersion | null = null;
171
206
 
172
- private versionToRefMap: Map<string, Array<AmfConfig>>;
207
+ private hasRendered = false;
173
208
 
174
209
  private isParentLevelDocPhaseEnabled = false;
175
- private selectedReferenceDocPhase?: string = null;
210
+ private selectedReferenceDocPhase?: string | null = null;
211
+ private _expandChildren?: boolean = false;
212
+ private isVersionFetched = false;
213
+ private latestVersion = false;
176
214
 
177
215
  /**
178
- * Key for storing the currently selected reference meta query param. This will be used to save the
179
- * previously selected reference meta and restoring it when changing between reference versions.
216
+ * Key for storing the currently selected reference url. This will be used to save the
217
+ * previously selected reference url and restoring it when changing between reference versions.
180
218
  */
181
- private readonly docsReferenceMetaSessionKey: string = "docsReferenceMeta";
219
+ private readonly docsReferenceUrlSessionKey: string = "docsReferenceUrl";
182
220
 
183
221
  _boundOnApiNavigationChanged;
184
222
  _boundUpdateSelectedItemFromUrlQuery;
185
223
 
186
- get amfConfigMapped(): AmfConfig[] {
187
- return this._amfConfig.map((config) => {
188
- return config.id in this.amfMap
189
- ? Object.assign(config, this.amfMap[config.id])
190
- : config;
191
- });
192
- }
193
-
194
224
  constructor() {
195
225
  super();
196
- this._boundOnApiNavigationChanged = this.onApiNavigationChanged.bind(
197
- this
198
- );
199
- this._boundUpdateSelectedItemFromUrlQuery = this.updateSelectedItemFromUrlQuery.bind(
200
- this
201
- );
226
+
227
+ this._boundOnApiNavigationChanged =
228
+ this.onApiNavigationChanged.bind(this);
229
+ this._boundUpdateSelectedItemFromUrlQuery =
230
+ this.updateSelectedItemFromUrlQuery.bind(this);
202
231
  }
203
232
 
204
233
  connectedCallback(): void {
@@ -226,7 +255,7 @@ export default class AmfReference extends LightningElement {
226
255
  renderedCallback(): void {
227
256
  if (!this.hasRendered) {
228
257
  this.hasRendered = true;
229
- if (this._amfConfig && this._amfConfig.length) {
258
+ if (this._amfConfigList && this._amfConfigList.length) {
230
259
  // If amfConfig has a value and length, it is assumed that fetch
231
260
  // has already been called and promises stored.
232
261
  this.updateView();
@@ -234,22 +263,136 @@ export default class AmfReference extends LightningElement {
234
263
  }
235
264
  }
236
265
 
266
+ /**
267
+ * Check if the URL hash to see whether this is one we want to redirect
268
+ * See GUS W-10718771 for references where we want hash-based redirects
269
+ * Return if we needs to redirect url to updated url
270
+ */
271
+ private getHashBasedRedirectUrl(): string | undefined {
272
+ const { hash } = window.location;
273
+ let hashBasedRedirectUrl = "";
274
+ if (hash) {
275
+ const strippedHash = hash.startsWith("#") ? hash.slice(1) : hash;
276
+ const strippedHashItems = strippedHash
277
+ ? strippedHash.split(":")
278
+ : [];
279
+ if (strippedHashItems.length) {
280
+ const referenceId = strippedHashItems[0];
281
+ const meta = strippedHashItems[1];
282
+ const encodedMeta = this.getUrlEncoded(meta);
283
+ const updatedReferenceId =
284
+ oldReferenceIdNewReferenceIdMap[referenceId];
285
+ const newReferenceId = updatedReferenceId || referenceId;
286
+ const referenceItemConfig =
287
+ this.getAmfConfigWithId(newReferenceId);
288
+ if (referenceItemConfig) {
289
+ hashBasedRedirectUrl = `${referenceItemConfig.href}?meta=${encodedMeta}`;
290
+ }
291
+ }
292
+ }
293
+ return hashBasedRedirectUrl;
294
+ }
295
+
296
+ /**
297
+ * @param referenceId
298
+ * @returns AMFConfig with given reference Id
299
+ */
300
+ private getAmfConfigWithId(referenceId: string): AmfConfig | undefined {
301
+ return this._amfConfigMap.get(referenceId);
302
+ }
303
+
304
+ /**
305
+ * @param referenceId
306
+ * @returns if the reference is spec based one or not with given referenceId.
307
+ */
308
+ private isSpecBasedReference(referenceId: string): boolean {
309
+ const selectedReference = this.getAmfConfigWithId(referenceId);
310
+ return selectedReference
311
+ ? selectedReference.referenceType !== REFERENCE_TYPES.markdown
312
+ : false;
313
+ }
314
+
315
+ /*
316
+ * Refactor below method when sidebar allows sending extraData along with the name for each item.
317
+ * See if we can refactor the below method using regex.
318
+ */
319
+
320
+ /**
321
+ * @param url
322
+ * @returns reference Id from url path / selected sidebar item.
323
+ */
324
+ private getReferenceIdFromUrl(url: string): string {
325
+ let referenceId = "";
326
+ const urlItems = url.split("/references/");
327
+ if (urlItems.length > 1) {
328
+ const rightSidePart = urlItems[1];
329
+
330
+ //This covers urls like "/project-name/references/reference-id/..."
331
+ const slashSeparatorItems = rightSidePart.split("/");
332
+
333
+ //This covers urls like "/project-name/references/reference-id?meta=Summary"
334
+ const querySeparatorItems = slashSeparatorItems[0].split("?");
335
+
336
+ referenceId = querySeparatorItems[0];
337
+ }
338
+
339
+ return referenceId;
340
+ }
341
+
342
+ private get oldVersionInfo(): DocPhaseInfo | null {
343
+ let info = null;
344
+ if (this.versions.length > 1 && this.selectedVersion) {
345
+ const currentGAVersionRef = this.versions[0];
346
+ if (this.selectedVersion.id !== currentGAVersionRef.id) {
347
+ info = oldVersionDocInfo(currentGAVersionRef.link.href);
348
+ }
349
+ }
350
+ return info;
351
+ }
352
+
353
+ /**
354
+ * @returns versions to be shown in the dropdown
355
+ * For markdown based specs, Adds selected markdown topic url to same references
356
+ */
357
+ private getVersions(): Array<ReferenceVersion> {
358
+ const allVersions = this._referenceSetConfig.versions;
359
+ if (!this.isSpecBasedReference(this._currentReferenceId)) {
360
+ const currentRefMeta = this.getMarkdownReferenceMeta(
361
+ window.location.href
362
+ );
363
+ if (currentRefMeta) {
364
+ for (let i = 0; i < allVersions.length; i++) {
365
+ const versionItem = allVersions[i];
366
+ const referenceLink = versionItem.link.href;
367
+ const referenceId =
368
+ this.getReferenceIdFromUrl(referenceLink);
369
+ if (this._currentReferenceId === referenceId) {
370
+ // This is to navigate to respective topic in the changed version
371
+ versionItem.link.href = `${referenceLink}/${currentRefMeta}`;
372
+ allVersions[i] = versionItem;
373
+ }
374
+ }
375
+ }
376
+ }
377
+ return allVersions;
378
+ }
379
+
237
380
  /**
238
381
  * Returns the selected version or the first available version.
239
382
  */
240
- private getSelectedVersion(): ReferenceVersion {
383
+ private getSelectedVersion(): ReferenceVersion | null {
241
384
  const versions = this._referenceSetConfig?.versions || [];
242
385
  const selectedVersion = versions.find(
243
386
  (v: ReferenceVersion) => v.selected
244
387
  );
245
388
  // return a selected version if there is one, else return the first one.
246
- return selectedVersion || (versions.length && versions[0]);
389
+ return selectedVersion || (versions.length && versions[0]) || null;
247
390
  }
248
391
 
249
392
  private updateAmfConfigInView(): void {
250
- if (this._amfConfig && this._amfConfig.length) {
393
+ if (this._amfConfigList && this._amfConfigList.length) {
251
394
  // fetch AMF Json as soon as config is set
252
- this.fetchAllAmf();
395
+ this.populateReferenceItems();
253
396
  // update() must be called after renderedCallback.
254
397
  if (this.hasRendered) {
255
398
  this.updateView();
@@ -257,9 +400,11 @@ export default class AmfReference extends LightningElement {
257
400
  }
258
401
  }
259
402
 
260
- private async fetchAmf(amfConfig): Promise<AmfModel | AmfModel[]> {
403
+ private async fetchAmf(
404
+ amfConfig: AmfConfig
405
+ ): Promise<AmfModel | AmfModel[]> {
261
406
  const { amf } = amfConfig;
262
- const response = await fetch(amf, {
407
+ const response = await fetch(amf!, {
263
408
  headers: {
264
409
  "Cache-Control": `max-age=86400`
265
410
  }
@@ -269,17 +414,95 @@ export default class AmfReference extends LightningElement {
269
414
  }
270
415
 
271
416
  /**
272
- * Calls the fetch for each AMF in the config.
273
- * Stores each fetch promise for handling after renderCallback.
417
+ * Returns whether current location is project root path like ../example-project/references
274
418
  */
275
- private fetchAllAmf(): void {
276
- for (const [i, amfConfig] of this._amfConfig.entries()) {
277
- const p = this.fetchAmf(amfConfig).then((amfJson) => {
278
- this.updateModel(amfConfig.id, amfJson);
279
- this.assignNavigationItemsFromAmf(amfConfig.id, i);
280
- });
281
- this.amfFetchPromiseMap[amfConfig.id] = p;
419
+ private isProjectRootPath(): boolean {
420
+ return this.getReferenceIdFromUrl(window.location.href) === "";
421
+ }
422
+
423
+ /**
424
+ * Returns whether given url is parent reference path like ../example-project/references/reference-id
425
+ */
426
+ private isParentReferencePath(urlPath?: string | null): boolean {
427
+ if (!urlPath) {
428
+ return false;
282
429
  }
430
+ const parentReferenceIndex = this.parentReferenceUrls.findIndex(
431
+ (referenceUrl: string) => {
432
+ return urlPath.endsWith(referenceUrl);
433
+ }
434
+ );
435
+ return parentReferenceIndex !== -1;
436
+ }
437
+
438
+ /**
439
+ * Expands the children of Markdown-based References.
440
+ */
441
+ private expandChildrenForMarkdownReferences(
442
+ children: ParsedMarkdownTopic[]
443
+ ): void {
444
+ if (!children) {
445
+ return;
446
+ }
447
+ for (const childNode of children) {
448
+ childNode.isExpanded = true;
449
+ this.expandChildrenForMarkdownReferences(childNode.children);
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Populates reference Items from amfConfigList and assigns it to navigation for sidebar
455
+ */
456
+ private populateReferenceItems(): void {
457
+ const navAmfOrder = [] as NavigationItem[];
458
+ for (const [index, amfConfig] of this._amfConfigList.entries()) {
459
+ let navItemChildren = [] as ParsedMarkdownTopic[];
460
+ let isChildrenLoading = false;
461
+ if (amfConfig.referenceType !== REFERENCE_TYPES.markdown) {
462
+ if (amfConfig.isSelected) {
463
+ const amfPromise = this.fetchAmf(amfConfig).then(
464
+ (amfJson) => {
465
+ this.updateModel(amfConfig.id, amfJson);
466
+ this.assignNavigationItemsFromAmf(amfConfig, index);
467
+ }
468
+ );
469
+ this.amfFetchPromiseMap[amfConfig.id] = amfPromise;
470
+ }
471
+ isChildrenLoading = true;
472
+ } else {
473
+ const isExpandChildrenEnabled = this.isExpandChildrenEnabled(
474
+ amfConfig.id
475
+ );
476
+ // check whether we should expand all the child nodes, this is required for Coveo to crawl.
477
+ if (isExpandChildrenEnabled) {
478
+ this.expandChildrenForMarkdownReferences(
479
+ amfConfig.topic!.children
480
+ );
481
+ }
482
+ navItemChildren = amfConfig.topic!.children;
483
+ }
484
+ // store nav items for each spec in order
485
+ navAmfOrder[index] = {
486
+ label: amfConfig.title,
487
+ name: amfConfig.href,
488
+ isExpanded:
489
+ amfConfig.isSelected ||
490
+ this.isExpandChildrenEnabled(amfConfig.id),
491
+ children: navItemChildren,
492
+ isChildrenLoading
493
+ };
494
+ this.parentReferenceUrls.push(amfConfig.href);
495
+ }
496
+ this.navigation = navAmfOrder;
497
+ }
498
+
499
+ /**
500
+ * Returns a boolean indicating whether the children should be expanded or not.
501
+ */
502
+ private isExpandChildrenEnabled(referenceId: string): boolean {
503
+ return (
504
+ !!this.expandChildren && this._currentReferenceId === referenceId
505
+ );
283
506
  }
284
507
 
285
508
  /**
@@ -311,7 +534,7 @@ export default class AmfReference extends LightningElement {
311
534
 
312
535
  /**
313
536
  * Transforms a list of model data for endpoints into corresponding
314
- * navigation list items that is compatible with dx-sidebar.
537
+ * navigation list items that are compatible with dx-sidebar.
315
538
  * Compatible with transforming AMF data parsed from both RAML and OAS spec.
316
539
  * Transforms a flat list of endpoints into a nested list based on indentation level
317
540
  * for RAML spec.
@@ -319,29 +542,47 @@ export default class AmfReference extends LightningElement {
319
542
  * @returns {array<Object>} List of navigation items
320
543
  */
321
544
  private assignEndpointNavItems(
545
+ parentReferencePath: string,
322
546
  referenceId: string,
323
547
  items: ParsedTopicModel[]
324
548
  ): NavItem[] {
325
- const methodList = [];
549
+ const methodList = [] as NavItem[];
326
550
 
327
551
  items.forEach((item) => {
328
552
  item.methods?.forEach((method) => {
329
- const meta = this.addToMetadata(referenceId, "method", method);
553
+ const title =
554
+ this.getTitleForLabel(method.label!) || method.method;
555
+ const meta = this.addToMetadata(
556
+ parentReferencePath,
557
+ referenceId,
558
+ "method",
559
+ method,
560
+ title
561
+ );
330
562
  methodList.push(
331
563
  Object.assign(method, {
332
- name: this.getReferencePathWithMeta(meta),
333
- label:
334
- this.getTitleForLabel(method.label) || method.method
564
+ name: this.getReferencePathWithMeta(
565
+ parentReferencePath,
566
+ meta
567
+ ),
568
+ label: title
335
569
  })
336
570
  );
337
571
  });
338
572
  });
339
-
340
573
  return methodList;
341
574
  }
342
575
 
343
- private getReferencePathWithMeta(meta: string): string {
344
- return meta ? `${window.location.pathname}?meta=${meta}` : "";
576
+ /**
577
+ * Gives URL path for reference items, Which can be used to push to the history
578
+ */
579
+ private getReferencePathWithMeta(
580
+ parentReferencePath: string,
581
+ meta: string
582
+ ): string {
583
+ // update the encoded url meta param
584
+ const encodedMeta = meta ? this.getUrlEncoded(meta) : "";
585
+ return encodedMeta ? `${parentReferencePath}?meta=${encodedMeta}` : "";
345
586
  }
346
587
 
347
588
  /**
@@ -351,12 +592,15 @@ export default class AmfReference extends LightningElement {
351
592
  * The 'endpoint' nav item may have nested children.
352
593
  */
353
594
  private assignNavigationItemsFromAmf(
354
- referenceId: string,
595
+ amfConfig: AmfConfig,
355
596
  amfIdx: number
356
597
  ): void {
598
+ const referenceId = amfConfig.id;
599
+ const parentReferencePath = amfConfig.href;
357
600
  const model = this.amfMap[referenceId].parser.parsedModel;
358
601
 
359
- const children = [];
602
+ const children: any[] = [];
603
+ const expandChildren = this.isExpandChildrenEnabled(referenceId);
360
604
 
361
605
  NAVIGATION_ITEMS.forEach(
362
606
  ({ label, name, childrenPropertyName, type }) => {
@@ -365,58 +609,67 @@ export default class AmfReference extends LightningElement {
365
609
  case "summary": {
366
610
  const summary = model[type];
367
611
  const meta = this.addToMetadata(
612
+ parentReferencePath,
368
613
  referenceId,
369
614
  type,
370
- summary
615
+ summary,
616
+ label
371
617
  );
372
618
  children.push({
373
619
  label,
374
- name: this.getReferencePathWithMeta(meta)
620
+ name: this.getReferencePathWithMeta(
621
+ parentReferencePath,
622
+ meta
623
+ )
375
624
  });
376
625
  break;
377
626
  }
378
627
  case "endpoint":
379
628
  if (
380
- model[childrenPropertyName] &&
381
- model[childrenPropertyName].length
629
+ model[childrenPropertyName!] &&
630
+ model[childrenPropertyName!].length
382
631
  ) {
383
632
  const amfTopicId = this.getFormattedIdentifier(
384
633
  referenceId,
385
634
  indexedName
386
635
  );
387
636
  const childTopics = this.assignEndpointNavItems(
637
+ parentReferencePath,
388
638
  referenceId,
389
- model[childrenPropertyName]
639
+ model[childrenPropertyName!]
390
640
  );
391
641
  children.push({
392
642
  label,
393
643
  name: this.getReferencePathWithMeta(
644
+ parentReferencePath,
394
645
  this.metadata[amfTopicId]?.meta
395
646
  ),
396
- isExpanded: false,
647
+ isExpanded: expandChildren,
397
648
  children: childTopics
398
649
  });
399
650
  }
400
651
  break;
401
652
  case "security":
402
653
  case "type":
403
- if (model[childrenPropertyName]?.length) {
654
+ if (model[childrenPropertyName!]?.length) {
404
655
  // Sorting the types alphabetically
405
- model[childrenPropertyName].sort((typeA, typeB) => {
406
- const typeALbl = typeA.label.toLowerCase();
407
- const typeBLbl = typeB.label.toLowerCase();
408
- return typeALbl < typeBLbl
409
- ? -1
410
- : typeALbl > typeBLbl
411
- ? 1
412
- : 0;
413
- });
656
+ model[childrenPropertyName!].sort(
657
+ (typeA: any, typeB: any) => {
658
+ const typeALbl = typeA.label.toLowerCase();
659
+ const typeBLbl = typeB.label.toLowerCase();
660
+ return typeALbl < typeBLbl
661
+ ? -1
662
+ : typeALbl > typeBLbl
663
+ ? 1
664
+ : 0;
665
+ }
666
+ );
414
667
  }
415
668
  // eslint-disable-next-line no-fallthrough
416
669
  default:
417
670
  if (
418
- model[childrenPropertyName] &&
419
- model[childrenPropertyName].length
671
+ model[childrenPropertyName!] &&
672
+ model[childrenPropertyName!].length
420
673
  ) {
421
674
  const amfTopicId = this.getFormattedIdentifier(
422
675
  referenceId,
@@ -425,19 +678,23 @@ export default class AmfReference extends LightningElement {
425
678
  children.push({
426
679
  label,
427
680
  name: this.getReferencePathWithMeta(
681
+ parentReferencePath,
428
682
  this.metadata[amfTopicId]?.meta
429
683
  ),
430
- isExpanded: false,
431
- children: model[childrenPropertyName].map(
432
- (topic) => {
684
+ isExpanded: expandChildren,
685
+ children: model[childrenPropertyName!].map(
686
+ (topic: any) => {
433
687
  const meta = this.addToMetadata(
688
+ parentReferencePath,
434
689
  referenceId,
435
690
  type,
436
- topic
691
+ topic,
692
+ topic.label
437
693
  );
438
694
  return {
439
695
  label: topic.label,
440
696
  name: this.getReferencePathWithMeta(
697
+ parentReferencePath,
441
698
  meta
442
699
  )
443
700
  };
@@ -449,47 +706,48 @@ export default class AmfReference extends LightningElement {
449
706
  }
450
707
  );
451
708
 
452
- // store nav items for each spec in order
453
- this.navAmfOrder[amfIdx] = {
454
- label: model.title,
455
- name: this.getReferencePathWithMeta(`${referenceId}-root`),
456
- isExpanded: amfIdx === 0, // only expand the first spec
457
- children
709
+ this.navigation[amfIdx] = {
710
+ ...this.navigation[amfIdx],
711
+ children,
712
+ isChildrenLoading: false
458
713
  };
459
-
460
- // update navigation with each specs nav items as they become available
461
- // navigation has to be an array because dx-sidebar expects an array.
462
- const navigation = [];
463
- for (const navAmf of this.navAmfOrder) {
464
- if (navAmf) {
465
- navigation.push(navAmf);
466
- }
467
- }
468
- this.navigation = navigation;
714
+ this.navigation = [...this.navigation];
469
715
  }
470
716
 
471
717
  protected addToMetadata(
718
+ parentReferencePath: string,
472
719
  referenceId: string,
473
720
  type: string,
474
- topic: { id: string; domId: string }
721
+ topic: { id: string; domId: string },
722
+ navTitle: string
475
723
  ): string {
476
- const { urlIdentifer, prefix } = URL_CONFIG[type];
724
+ const config = URL_CONFIG[type as keyof typeof URL_CONFIG];
725
+ const urlIdentifer = config.urlIdentifer;
726
+ let prefix = null;
727
+ if ("prefix" in config) {
728
+ prefix = config.prefix;
729
+ }
477
730
 
478
731
  // encodeURI to avoid special characters in the URL meta.
479
732
  const identifier =
480
- topic[urlIdentifer] && this.encodeIdentifier(topic[urlIdentifer]);
481
- const meta = prefix
482
- ? `${referenceId}:${prefix}${identifier}`
483
- : `${referenceId}:${identifier}`;
484
- this.metadata[meta] = {
485
- meta: meta,
486
- referenceId: referenceId,
487
- amfId: topic.id,
488
- elementId: topic.domId,
489
- identifier,
490
- type
491
- };
492
- return meta;
733
+ urlIdentifer in topic &&
734
+ this.encodeIdentifier(topic[urlIdentifer as keyof typeof topic]);
735
+ let meta;
736
+ // Assuming that there will be an identifier always
737
+ if (identifier) {
738
+ meta = prefix ? `${prefix}${identifier}` : `${identifier}`;
739
+ this.metadata[meta] = {
740
+ parentReferencePath,
741
+ meta,
742
+ referenceId,
743
+ amfId: topic.id,
744
+ elementId: topic.domId,
745
+ identifier,
746
+ type,
747
+ navTitle
748
+ };
749
+ }
750
+ return meta!;
493
751
  }
494
752
 
495
753
  /**
@@ -500,7 +758,7 @@ export default class AmfReference extends LightningElement {
500
758
  (metadata: AmfMetadataTopic) => {
501
759
  return routeMeta.meta === metadata.meta;
502
760
  }
503
- );
761
+ )!;
504
762
  }
505
763
 
506
764
  /**
@@ -514,7 +772,7 @@ export default class AmfReference extends LightningElement {
514
772
  return Object.values(this.metadata).find(
515
773
  (metadata: AmfMetadataTopic) =>
516
774
  referenceId === metadata.referenceId && amfId === metadata.amfId
517
- );
775
+ )!;
518
776
  }
519
777
 
520
778
  /**
@@ -529,7 +787,7 @@ export default class AmfReference extends LightningElement {
529
787
  (metadata: AmfMetadataTopic) =>
530
788
  referenceId === metadata.referenceId &&
531
789
  identifier === metadata.identifier
532
- );
790
+ )!;
533
791
  }
534
792
 
535
793
  /**
@@ -543,58 +801,29 @@ export default class AmfReference extends LightningElement {
543
801
  return Object.values(this.metadata).find(
544
802
  (metadata: AmfMetadataTopic) =>
545
803
  referenceId === metadata.referenceId && type === metadata.type
546
- );
804
+ )!;
547
805
  }
548
806
 
549
807
  /**
550
- * Parses url query params without decoding of params
808
+ * Parses URL query params without decoding of params
551
809
  */
552
- private parseParams(path: string): qs.ParsedQuery<string> {
553
- if (!path) {
810
+ private parseParams(params: string): qs.ParsedQuery<string> {
811
+ if (!params) {
554
812
  return {};
555
813
  }
556
- return qs.parse(path, {
814
+ return qs.parse(params, {
557
815
  decode: false
558
816
  });
559
817
  }
560
818
 
561
- /**
562
- * Gets the portion from the URL query param 'meta'.
563
- */
564
- protected getCurrentRefMeta(
565
- previousRefMetaInSession?: string
566
- ): RouteMeta | null {
567
- const path = window.location.search;
568
- const urlParams = this.parseParams(path);
569
- let meta = urlParams.meta as string;
570
- let routeMeta = null;
571
- if (previousRefMetaInSession) {
572
- const refParts = previousRefMetaInSession.split(":");
573
- const newRefId = refParts.length > 0 ? refParts[0] : null;
574
- const [, type, topicId] = previousRefMetaInSession.split(":");
575
- meta = newRefId ? [newRefId, type, topicId].join(":") : null;
576
- } else if (!meta) {
577
- // If no `meta` explicitly exists, check the URL hash to see whether this is one we
578
- // want to redirect (see GUS W-10718771 for one reference where we want hash-based
579
- // redirects)
580
- const { hash } = window.location;
581
- const strippedHash = hash.startsWith("#") ? hash.slice(1) : hash;
582
- meta = urlHashToMetaRedirectMap[strippedHash];
583
- }
584
- if (meta) {
585
- routeMeta = new RouteMeta(meta);
586
- }
587
- return routeMeta;
588
- }
589
-
590
819
  /**
591
820
  * Normalizes topic identifier by replacing spaces with '+'
592
821
  * and running encodeURI() on it
593
- * @param identifer raw identifer for a topic as parsed from the spec file
822
+ * @param identifier raw identifier for a topic as parsed from the spec file
594
823
  * @returns normalized and encoded identifier
595
824
  */
596
- protected encodeIdentifier(identifer: string): string {
597
- let result = identifer.trim();
825
+ protected encodeIdentifier(identifier: string): string {
826
+ let result = identifier.trim();
598
827
  result = result.replace(new RegExp(/\s+/, "g"), "+");
599
828
  return encodeURI(result);
600
829
  }
@@ -606,86 +835,137 @@ export default class AmfReference extends LightningElement {
606
835
  return `${referenceId}:${id}`;
607
836
  }
608
837
 
609
- protected updateSelectedItemFromUrlQuery(): void {
610
- const currentMeta: RouteMeta | null = this.getCurrentRefMeta();
611
- const metadata = currentMeta && this.getMetadataByUrlQuery(currentMeta);
612
- if (metadata) {
613
- const {
614
- referenceId,
615
- amfId,
616
- type,
617
- elementId
618
- }: AmfMetadataTopic = metadata;
619
- this.loadContent(
620
- referenceId,
621
- amfId,
622
- type,
623
- elementId,
624
- currentMeta.meta
625
- );
626
- }
627
- }
628
- protected onApiNavigationChanged(): void {
629
- // The API Navigation event will always intend to navigate within the current reference
630
- const metadata = this.metadata[
631
- this.getReferencePathWithMeta(this.selectedTopic.meta)
632
- ];
633
- const {
634
- referenceId,
635
- amfId,
636
- type,
637
- elementId
638
- }: AmfMetadataTopic = metadata;
639
- this.loadContent(referenceId, amfId, type, elementId, metadata.meta);
640
- this.updateUrlWithSelected();
641
- }
642
-
643
- protected updateUrlWithSelected(meta?: string): void {
838
+ protected updateUrlWithSelected(
839
+ parentReferencePath: string,
840
+ meta?: string
841
+ ): void {
644
842
  if (meta) {
843
+ // update the encoded url meta param
844
+ const encodedMeta = this.getUrlEncoded(meta);
845
+
645
846
  window.history.pushState(
646
847
  {},
647
848
  "",
648
- `${window.location.pathname}?meta=${meta}`
849
+ `${parentReferencePath}?meta=${encodedMeta}`
649
850
  );
650
851
  }
651
852
  }
652
853
 
653
854
  /**
654
- * Does a replace on the url meta, so it does not create a history entry.
855
+ * Does a replace on the URL meta, so it does not create a history entry.
655
856
  */
656
- protected replaceUrlWithSelected(meta?: string): void {
857
+ protected replaceUrlWithSelected(
858
+ parentReferencePath: string,
859
+ meta?: string
860
+ ): void {
657
861
  if (meta) {
862
+ // update the encoded url meta param
863
+ const encodedMeta = this.getUrlEncoded(meta);
864
+
658
865
  window.history.replaceState(
659
- {},
866
+ window.history.state,
660
867
  "",
661
- `${window.location.pathname}?meta=${meta}`
868
+ `${parentReferencePath}?meta=${encodedMeta}`
662
869
  );
663
870
  }
664
871
  }
665
872
 
873
+ /**
874
+ * This method gets called when the user navigates back and forth using browser arrows
875
+ * Updates content depending on the type of reference - spec based or markdown
876
+ */
877
+ protected updateSelectedItemFromUrlQuery(): void {
878
+ const specBasedReference = this.isSpecBasedReference(
879
+ this._currentReferenceId
880
+ );
881
+ if (specBasedReference) {
882
+ const currentMeta: RouteMeta | undefined =
883
+ this.getReferenceMetaInfo(window.location.href);
884
+ const metadata =
885
+ currentMeta && this.getMetadataByUrlQuery(currentMeta);
886
+ if (metadata) {
887
+ const {
888
+ parentReferencePath,
889
+ referenceId,
890
+ amfId,
891
+ type,
892
+ elementId
893
+ }: AmfMetadataTopic = metadata;
894
+ this.loadSpecReferenceContent(
895
+ parentReferencePath,
896
+ referenceId,
897
+ amfId,
898
+ type,
899
+ elementId,
900
+ currentMeta.meta
901
+ );
902
+ }
903
+ } else {
904
+ this.loadMarkdownBasedReference();
905
+ }
906
+
907
+ restoreScroll(); // don't try this at home kids
908
+ }
909
+
910
+ /**
911
+ * The API Navigation event will always intend to navigate within the current reference
912
+ * @param event
913
+ */
914
+ protected onApiNavigationChanged(): void {
915
+ const specBasedReference = this.isSpecBasedReference(
916
+ this._currentReferenceId
917
+ );
918
+ if (specBasedReference) {
919
+ const { meta } = this.selectedTopic!;
920
+ const metadata = this.metadata[meta];
921
+ if (metadata) {
922
+ const {
923
+ parentReferencePath,
924
+ referenceId,
925
+ amfId,
926
+ type,
927
+ elementId
928
+ }: AmfMetadataTopic = metadata;
929
+ this.loadSpecReferenceContent(
930
+ parentReferencePath,
931
+ referenceId,
932
+ amfId,
933
+ type,
934
+ elementId,
935
+ metadata.meta
936
+ );
937
+ }
938
+ } else {
939
+ this.loadMarkdownBasedReference();
940
+ }
941
+ }
942
+
666
943
  /**
667
944
  * Updates the currently selected amf and topic
668
945
  */
669
- protected loadContent(
946
+ protected loadSpecReferenceContent(
947
+ parentReferencePath: string,
670
948
  referenceId: string,
671
949
  amfId: string,
672
950
  type: string,
673
- elementId = "",
674
- meta = ""
951
+ elementId: string,
952
+ meta: string
675
953
  ): void {
676
954
  this.selectedTopic = {
677
955
  referenceId,
956
+ parentReferencePath,
678
957
  amfId,
679
958
  elementId,
680
959
  type,
681
960
  meta
682
961
  };
683
- this.selectedSidebarValue = this.getReferencePathWithMeta(meta);
962
+ this.selectedSidebarValue = this.getReferencePathWithMeta(
963
+ parentReferencePath,
964
+ meta
965
+ );
684
966
 
685
967
  this.handleSelectedItem();
686
968
 
687
- // Ensures that the URL always has the meta, that way we don't get two history entries for summary
688
- this.replaceUrlWithSelected(meta);
689
969
  this.updateDocPhase();
690
970
  }
691
971
 
@@ -696,10 +976,9 @@ export default class AmfReference extends LightningElement {
696
976
  /* If parent level doc phase is enabled, Individual reference level doc phase should not be considered */
697
977
 
698
978
  if (!this.isParentLevelDocPhaseEnabled) {
699
- const referenceId = this.selectedTopic?.referenceId;
700
- const selectedReference = this._amfConfig.find(
979
+ const selectedReference = this._amfConfigList.find(
701
980
  (referenceItem: AmfConfig) => {
702
- return referenceItem.id === referenceId;
981
+ return referenceItem.id === this._currentReferenceId;
703
982
  }
704
983
  );
705
984
  if (selectedReference) {
@@ -711,103 +990,505 @@ export default class AmfReference extends LightningElement {
711
990
  }
712
991
 
713
992
  /**
714
- * Updates the DOM on first load
993
+ * Returns the decoded meta query param from given Url as it is being used internally.
994
+ */
995
+ getMetaFromUrl(referenceUrl: string): string {
996
+ const indexOfQueryParam = referenceUrl.indexOf("?");
997
+ const urlPath = referenceUrl.substring(
998
+ indexOfQueryParam >= 0 ? indexOfQueryParam : referenceUrl.length
999
+ );
1000
+ const meta = this.parseParams(urlPath).meta as string;
1001
+ // Always get the meta query param encoded and decode it and store it for internal use
1002
+ // This has 2 advantages,
1003
+ // 1. Supports backward compatible meta query param, so there is no need for redirects.
1004
+ // 2. Supports Prerender and Coveo for their crawling.
1005
+ const encodedMeta = meta && this.getUrlEncoded(meta);
1006
+ const decodedMeta = encodedMeta && decodeURIComponent(encodedMeta);
1007
+ return decodedMeta || "";
1008
+ }
1009
+
1010
+ /**
1011
+ *
1012
+ * @returns meta for given markdown based referenceUrl
1013
+ * Consider last topic url in ../references/reference-name/example.html
1014
+ */
1015
+ getMarkdownReferenceMeta(referenceUrl: string): string {
1016
+ let meta = "";
1017
+ if (referenceUrl) {
1018
+ const slashSeparatorItems = referenceUrl.split("/");
1019
+ const lastItem =
1020
+ slashSeparatorItems[slashSeparatorItems.length - 1];
1021
+ if (lastItem.endsWith(".html")) {
1022
+ meta = lastItem;
1023
+ }
1024
+ }
1025
+ return meta;
1026
+ }
1027
+
1028
+ /**
1029
+ * Gets the encoded url.
1030
+ * This method will return the encoded url for 2 cases,
1031
+ * 1. If the url is encoded already
1032
+ * 2. If the url is decoded
1033
+ */
1034
+ getUrlEncoded(url: string): string {
1035
+ // if url matches, then return the encoded url.
1036
+ if (decodeURIComponent(url) === url) {
1037
+ return encodeURIComponent(url);
1038
+ }
1039
+ // return the encoded url.
1040
+ return this.getUrlEncoded(decodeURIComponent(url));
1041
+ }
1042
+
1043
+ /**
1044
+ *
1045
+ * @returns RouteMeta object for given referenceUrl
1046
+ * referenceId - gets referenceId from url
1047
+ * For spec based references gets meta parm from url and then topicId & type from meta
1048
+ * For markdown based references gets topicId as last html path in the name, meta & type will be empty
1049
+ */
1050
+ getReferenceMetaInfo(referenceUrl: string | null): RouteMeta | undefined {
1051
+ let metaReferenceInfo;
1052
+ if (referenceUrl) {
1053
+ const referenceId = this.getReferenceIdFromUrl(referenceUrl);
1054
+ let meta = "";
1055
+ let topicId = "";
1056
+ let type = "";
1057
+ if (this.isSpecBasedReference(referenceId)) {
1058
+ meta = this.getMetaFromUrl(referenceUrl);
1059
+ if (meta) {
1060
+ if (meta.includes(":")) {
1061
+ const metaInfo = meta.split(":");
1062
+ type = metaInfo[0];
1063
+ topicId = metaInfo[1] || type;
1064
+ } else {
1065
+ topicId = meta;
1066
+ }
1067
+ }
1068
+ } else {
1069
+ topicId = this.getMarkdownReferenceMeta(referenceUrl);
1070
+ }
1071
+ metaReferenceInfo = {
1072
+ referenceId,
1073
+ meta,
1074
+ topicId,
1075
+ type
1076
+ };
1077
+ }
1078
+ return metaReferenceInfo;
1079
+ }
1080
+
1081
+ /**
1082
+ * Finds and returns referenceUrl and topicTitle if given topic url matches
1083
+ */
1084
+ getReferenceDetailsInGivenTopics(
1085
+ topics: ParsedMarkdownTopic[],
1086
+ topicMeta: string
1087
+ ): { referenceUrl: string; topicTitle: string } {
1088
+ let referenceUrl = "";
1089
+ let topicTitle = "";
1090
+ for (let i = 0; i < topics.length; i++) {
1091
+ const topic = topics[i];
1092
+ const meta = this.getMarkdownReferenceMeta(topic.link!.href);
1093
+ const childTopics = topic.children;
1094
+ if (meta === topicMeta) {
1095
+ referenceUrl = topic.link!.href;
1096
+ topicTitle = topic.label;
1097
+ } else if (childTopics && childTopics.length) {
1098
+ const referenceDetails = this.getReferenceDetailsInGivenTopics(
1099
+ childTopics,
1100
+ topicMeta
1101
+ );
1102
+ referenceUrl = referenceDetails.referenceUrl;
1103
+ topicTitle = referenceDetails.topicTitle;
1104
+ }
1105
+ if (referenceUrl && topicTitle) {
1106
+ break;
1107
+ }
1108
+ }
1109
+ return {
1110
+ referenceUrl,
1111
+ topicTitle
1112
+ };
1113
+ }
1114
+
1115
+ /**
1116
+ * Gives referenceUrl and topicTitle for given markdown topic url
1117
+ */
1118
+ getRefDetailsForGivenTopicMeta(
1119
+ referenceId: string,
1120
+ topicMeta: string
1121
+ ): { referenceUrl: string; topicTitle: string } | undefined {
1122
+ const amfConfig = this.getAmfConfigWithId(referenceId);
1123
+ let referenceDetails;
1124
+ if (amfConfig) {
1125
+ const topics = amfConfig.topic?.children || [];
1126
+ referenceDetails = this.getReferenceDetailsInGivenTopics(
1127
+ topics,
1128
+ topicMeta
1129
+ );
1130
+ }
1131
+ return referenceDetails;
1132
+ }
1133
+
1134
+ /**
1135
+ * Updates the DOM on the first load
715
1136
  */
716
1137
  updateView(): void {
1138
+ const previousRefUrlInSession = window.sessionStorage.getItem(
1139
+ this.docsReferenceUrlSessionKey
1140
+ );
1141
+ window.sessionStorage.removeItem(this.docsReferenceUrlSessionKey);
1142
+ let previousRefInfo = this.getReferenceMetaInfo(
1143
+ previousRefUrlInSession
1144
+ );
1145
+
1146
+ // For spec based reference, We should consider urlData to show same topic when user reloads after navigating to specific topic
1147
+ if (!previousRefInfo) {
1148
+ const currentUrl = window.location.href;
1149
+ const urlReferenceId = this.getReferenceIdFromUrl(currentUrl);
1150
+ if (urlReferenceId && this.isSpecBasedReference(urlReferenceId)) {
1151
+ if (
1152
+ !this.isProjectRootPath() &&
1153
+ !this.isParentReferencePath(currentUrl)
1154
+ ) {
1155
+ previousRefInfo = this.getReferenceMetaInfo(currentUrl);
1156
+ }
1157
+ }
1158
+ }
1159
+
717
1160
  let referenceId: string;
718
1161
  let topicId = "";
719
- const previousRefMetaInSession = window.sessionStorage.getItem(
720
- this.docsReferenceMetaSessionKey
721
- );
722
- window.sessionStorage.removeItem(this.docsReferenceMetaSessionKey);
723
- const currentMeta = this.getCurrentRefMeta(previousRefMetaInSession);
724
- if (currentMeta) {
725
- referenceId = currentMeta.referenceId;
726
- topicId = currentMeta.topicId;
727
- if (!this.amfFetchPromiseMap[referenceId]) {
728
- // This could happen if they specify a bad query value.
729
- // In this case, we'll do the logic below to use the default amf
730
- referenceId = null;
1162
+
1163
+ if (
1164
+ previousRefInfo &&
1165
+ this._amfConfigMap.has(previousRefInfo.referenceId)
1166
+ ) {
1167
+ referenceId = previousRefInfo.referenceId;
1168
+ topicId = previousRefInfo.topicId;
1169
+ } else {
1170
+ referenceId = this._currentReferenceId;
1171
+ }
1172
+
1173
+ const specBasedReference = this.isSpecBasedReference(referenceId);
1174
+ if (specBasedReference) {
1175
+ // Wait till the AMF is loaded.
1176
+ this.amfFetchPromiseMap[referenceId].then(() => {
1177
+ let selectedItemMetaData = this.getMetadataByIdentifier(
1178
+ referenceId,
1179
+ topicId
1180
+ );
1181
+ if (!selectedItemMetaData) {
1182
+ // Doesn't exist, let's use the summary.
1183
+ selectedItemMetaData = this.getMetadataByType(
1184
+ referenceId,
1185
+ "summary"
1186
+ );
1187
+ }
1188
+
1189
+ if (selectedItemMetaData) {
1190
+ this.loadSpecReferenceContent(
1191
+ selectedItemMetaData.parentReferencePath,
1192
+ selectedItemMetaData.referenceId,
1193
+ selectedItemMetaData.amfId,
1194
+ selectedItemMetaData.type,
1195
+ "",
1196
+ selectedItemMetaData.meta
1197
+ );
1198
+
1199
+ this.updateUrlWithSelected(
1200
+ selectedItemMetaData.parentReferencePath,
1201
+ selectedItemMetaData.meta
1202
+ );
1203
+ this.updateTags(selectedItemMetaData.navTitle);
1204
+ }
1205
+ });
1206
+ } else {
1207
+ let invalidTopicReferenceUrl: string | null = "";
1208
+ if (topicId) {
1209
+ const referenceDetails = this.getRefDetailsForGivenTopicMeta(
1210
+ referenceId,
1211
+ topicId
1212
+ );
1213
+ const selectedItemUrl = referenceDetails?.referenceUrl;
1214
+ if (!selectedItemUrl) {
1215
+ invalidTopicReferenceUrl = previousRefUrlInSession;
1216
+ }
731
1217
  }
1218
+ this.loadMarkdownBasedReference(invalidTopicReferenceUrl);
1219
+ }
1220
+ }
1221
+
1222
+ /**
1223
+ * Navigates to reference of the given URL
1224
+ * @param url
1225
+ */
1226
+ private loadNewReferenceItem(url: string): void {
1227
+ const referenceId = this.getReferenceIdFromUrl(url);
1228
+ const referenceItem = this.getAmfConfigWithId(referenceId);
1229
+ if (referenceItem) {
1230
+ window.location.href = referenceItem.href;
732
1231
  }
1232
+ }
733
1233
 
734
- if (!referenceId) {
735
- referenceId = this._amfConfig[0].id;
736
- if (!this.amfFetchPromiseMap[referenceId]) {
737
- // This should never happen.
738
- referenceId = Object.keys(this.amfFetchPromiseMap)[0];
1234
+ /**
1235
+ * @param referenceUrl to which user wants to navigate
1236
+ * Redirect to first sub item if it's root level item, otherwise content will be loaded
1237
+ * Push the history as a first child item
1238
+ * set selected sidebar value as a pathname
1239
+ */
1240
+
1241
+ private loadMarkdownBasedReference(referenceUrl?: string | null): void {
1242
+ // MILES TODO: figure out if we ever need to log a coveo page view in here
1243
+ // this would be the case if at some point we 'load' a new 'markdown based reference'
1244
+ // without actually triggering a page load
1245
+ let referenceId = "";
1246
+ const currentUrl = window.location.href;
1247
+ if (this.isProjectRootPath()) {
1248
+ /**
1249
+ * CASE1: This case is to consider when the user navigates to references by clicking a project card
1250
+ * Ex: /docs/example-project/references should navigate to the first topic in the first reference
1251
+ */
1252
+ referenceId = this._currentReferenceId;
1253
+ } else if (this.isParentReferencePath(referenceUrl)) {
1254
+ /**
1255
+ * CASE2: This case is to navigate to respective reference when the user clicked on root item
1256
+ * Ex: .../references/markdown-ref should navigate to first topic.
1257
+ */
1258
+ referenceId = this.getReferenceIdFromUrl(referenceUrl!);
1259
+ } else if (this.isParentReferencePath(currentUrl)) {
1260
+ /**
1261
+ * CASE3: This case is to navigate to respective reference when the user entered url with reference id
1262
+ * Ex: .../references/markdown-ref should navigate to first topic.
1263
+ */
1264
+ referenceId = this.getReferenceIdFromUrl(currentUrl);
1265
+ } else if (referenceUrl) {
1266
+ /**
1267
+ * CASE4: This case is to navigate to first item when we don't have topic in the selected version
1268
+ * Ex: .../references/markdown-ref/not-existed-topic-url should navigate to first topic.
1269
+ */
1270
+ const referenceMeta = this.getMarkdownReferenceMeta(referenceUrl);
1271
+ const selectedItemRefId = this.getReferenceIdFromUrl(referenceUrl);
1272
+ const referenceDetails = this.getRefDetailsForGivenTopicMeta(
1273
+ selectedItemRefId,
1274
+ referenceMeta
1275
+ );
1276
+ const selectedItemUrl = referenceDetails?.referenceUrl;
1277
+ if (!selectedItemUrl) {
1278
+ referenceId = this.getReferenceIdFromUrl(referenceUrl);
739
1279
  }
740
1280
  }
741
1281
 
742
- // Wait till the AMF is loaded.
743
- this.amfFetchPromiseMap[referenceId].then(() => {
744
- let topic = this.getMetadataByIdentifier(referenceId, topicId);
745
- if (!topic) {
746
- // Doesn't exist, let's use the summary.
747
- topic = this.getMetadataByType(referenceId, "summary");
1282
+ let isRedirecting = false;
1283
+ if (referenceId) {
1284
+ const amfConfig = this.getAmfConfigWithId(referenceId);
1285
+ let redirectReferenceUrl = "";
1286
+ if (amfConfig) {
1287
+ const childrenItems = amfConfig.topic!.children;
1288
+ if (childrenItems.length > 0) {
1289
+ redirectReferenceUrl = childrenItems[0].link!.href;
1290
+ }
1291
+ }
1292
+ if (redirectReferenceUrl) {
1293
+ if (this.isParentReferencePath(referenceUrl)) {
1294
+ // This is for CASE2 mentioned above, Where we need to navigate user to respective href
1295
+ isRedirecting = true;
1296
+ window.location.href = redirectReferenceUrl;
1297
+ } else {
1298
+ // This is for CASE 1,3 and 4 mentioned above, Where we need to update the browser history
1299
+ window.history.replaceState(
1300
+ window.history.state,
1301
+ "",
1302
+ redirectReferenceUrl
1303
+ );
1304
+ }
1305
+ }
1306
+ }
1307
+ if (!isRedirecting) {
1308
+ const currentReferenceUrl = window.location.href;
1309
+ const referenceMeta =
1310
+ this.getMarkdownReferenceMeta(currentReferenceUrl);
1311
+ const selectedItemRefId =
1312
+ this.getReferenceIdFromUrl(currentReferenceUrl);
1313
+ const referenceDetails = this.getRefDetailsForGivenTopicMeta(
1314
+ selectedItemRefId,
1315
+ referenceMeta
1316
+ );
1317
+ if (referenceDetails) {
1318
+ this.updateTags(referenceDetails.topicTitle);
748
1319
  }
749
1320
 
750
- if (topic) {
751
- this.loadContent(
752
- topic.referenceId,
753
- topic.amfId,
754
- topic.type,
755
- "",
756
- topic.meta
757
- );
1321
+ this.versions = this.getVersions();
1322
+ if (this.oldVersionInfo) {
1323
+ this.showVersionBanner = true;
1324
+ } else {
1325
+ this.latestVersion = true;
758
1326
  }
759
- });
1327
+
1328
+ this.isVersionFetched = true;
1329
+ this.updateDocPhase();
1330
+ this.selectedSidebarValue = window.location.pathname;
1331
+ }
760
1332
  }
761
1333
 
762
1334
  /**
763
- * Currently, used to handle the version change and storing the current meta query param to the session storage.
1335
+ * Currently, used to handle the version change and store the current reference Url.
764
1336
  */
765
1337
  handleVersionChange(): void {
766
- const path = window.location.search;
767
- const urlParams = this.parseParams(path);
768
- const meta = urlParams.meta as string;
769
- if (meta) {
770
- window.sessionStorage.setItem(
771
- this.docsReferenceMetaSessionKey,
772
- meta
773
- );
1338
+ const currentUrl = window.location.href;
1339
+ window.sessionStorage.setItem(
1340
+ this.docsReferenceUrlSessionKey,
1341
+ currentUrl
1342
+ );
1343
+ }
1344
+
1345
+ handleDismissVersionBanner() {
1346
+ this.showVersionBanner = false;
1347
+ }
1348
+
1349
+ private updateTags(navTitle = ""): void {
1350
+ if (!navTitle) {
1351
+ return;
1352
+ }
1353
+
1354
+ // this is required to update the nav title meta tag.
1355
+ // eslint-disable-next-line @lwc/lwc/no-document-query
1356
+ const metaNavTitle = document.querySelector('meta[name="nav-title"]');
1357
+ // eslint-disable-next-line @lwc/lwc/no-document-query
1358
+ const titleTag = document.querySelector("title");
1359
+ const TITLE_SEPARATOR = " | ";
1360
+
1361
+ if (metaNavTitle) {
1362
+ metaNavTitle.setAttribute("content", navTitle);
1363
+ }
1364
+
1365
+ /**
1366
+ * Right now, the title tag only changes when you pick a Ref spec,
1367
+ * not every time you choose a subsection of the Ref spec.
1368
+ * This update aims to refresh the title tag with each selection.
1369
+ * If a Ref spec is chosen, we add the value of the <selected topic> to the title.
1370
+ * If a subsection is selected, we update the first part of the current
1371
+ * title with the new <selected topic>.
1372
+ * Example: Following is a sample project structure.
1373
+ * - Project Name
1374
+ * - Ref Spec1
1375
+ * - Summary
1376
+ * - Endpoints
1377
+ * - E1
1378
+ * - E2
1379
+ * - Ref Spec2
1380
+ * - Summary
1381
+ * - Endpoints
1382
+ * - E1 (Selected)
1383
+ * - E2
1384
+ * Previous Title: Ref Spec2 | Project Name | Salesforce Developer
1385
+ * New Title: E1 | Ref Spec2 | Project Name | Salesforce Developer
1386
+ *
1387
+ */
1388
+ if (titleTag) {
1389
+ let titleTagValue = titleTag.textContent;
1390
+ const titleTagSectionValues: string[] =
1391
+ titleTagValue?.split(TITLE_SEPARATOR);
1392
+ if (titleTagSectionValues) {
1393
+ if (titleTagSectionValues.length <= 3) {
1394
+ titleTagValue = navTitle + TITLE_SEPARATOR + titleTagValue;
1395
+ } else {
1396
+ titleTagSectionValues[0] = navTitle;
1397
+ titleTagValue = titleTagSectionValues.join(TITLE_SEPARATOR);
1398
+ }
1399
+ }
1400
+ titleTag.textContent = titleTagValue;
774
1401
  }
775
1402
  }
776
1403
 
777
1404
  onNavSelect(event: CustomEvent): void {
778
1405
  const name = event.detail.name;
779
- const indexOfQueryParam = name.indexOf("?");
780
- const urlPath = name.substring(
781
- indexOfQueryParam >= 0 ? indexOfQueryParam : name.length
782
- );
783
- const metaVal = this.parseParams(urlPath).meta as string;
784
- const currentSelectedMeta = this.selectedTopic.meta;
1406
+ if (name) {
1407
+ const urlReferenceId = this.getReferenceIdFromUrl(name);
1408
+ const specBasedReference =
1409
+ this.isSpecBasedReference(urlReferenceId);
1410
+ if (specBasedReference) {
1411
+ const metaVal = this.getMetaFromUrl(name);
1412
+ const currentSelectedMeta = this.selectedTopic
1413
+ ? this.selectedTopic.meta
1414
+ : "";
1415
+
1416
+ if (metaVal && metaVal === currentSelectedMeta) {
1417
+ // selecting the same nav item, skip update
1418
+ return;
1419
+ }
785
1420
 
786
- if (metaVal === currentSelectedMeta) {
787
- // selecting the same nav item, skip update
788
- return;
1421
+ const metadata = this.metadata[metaVal];
1422
+ if (metadata) {
1423
+ const {
1424
+ parentReferencePath,
1425
+ referenceId,
1426
+ amfId,
1427
+ type,
1428
+ elementId
1429
+ } = metadata;
1430
+ this.loadSpecReferenceContent(
1431
+ parentReferencePath,
1432
+ referenceId,
1433
+ amfId,
1434
+ type,
1435
+ elementId,
1436
+ metaVal
1437
+ );
1438
+
1439
+ logCoveoPageView(
1440
+ this.coveoOrganizationId,
1441
+ this.coveoAnalyticsToken
1442
+ );
1443
+ this.updateUrlWithSelected(parentReferencePath, metaVal);
1444
+ this.updateTags(metadata.navTitle);
1445
+ } else {
1446
+ if (this.isParentReferencePath(name)) {
1447
+ this.loadNewReferenceItem(name);
1448
+ }
1449
+ }
1450
+ } else {
1451
+ this.loadMarkdownBasedReference(name);
1452
+ }
789
1453
  }
1454
+ }
790
1455
 
791
- const metadata = this.metadata[metaVal];
792
- if (metadata) {
793
- const { referenceId, amfId, type, elementId } = metadata;
794
- this.loadContent(referenceId, amfId, type, elementId, metaVal);
795
- this.updateUrlWithSelected(metaVal);
1456
+ onExpandCollapse(event: CustomEvent): void {
1457
+ const { name, isSelectAction, isExpanded } = event.detail;
1458
+ if (!isSelectAction && isExpanded) {
1459
+ const referenceId = this.getReferenceIdFromUrl(name);
1460
+ const currentUrl = window.location.href;
1461
+ const currentReferenceId = this.getReferenceIdFromUrl(currentUrl);
1462
+ //No need to do anything if user is expanding currently selected reference
1463
+ if (referenceId !== currentReferenceId) {
1464
+ const isSpecBasedReference =
1465
+ this.isSpecBasedReference(referenceId);
1466
+ if (isSpecBasedReference) {
1467
+ // Perform functionality same as item selection
1468
+ this.onNavSelect(event);
1469
+ }
1470
+ }
796
1471
  }
797
1472
  }
798
1473
 
799
1474
  handleSelectedItem(): void {
800
1475
  // update topic view
801
- const { referenceId, amfId, type } = this.selectedTopic;
1476
+ const { referenceId, amfId, type } = this.selectedTopic!;
1477
+
1478
+ // Adding stringify inside try/catch
1479
+ let amfModelString = "";
1480
+ try {
1481
+ amfModelString = JSON.stringify(this.amfMap[referenceId].model);
1482
+ } catch (error) {
1483
+ console.error(`Error stringifying amf model: ${error}`);
1484
+ }
802
1485
 
803
1486
  // This updates the component in the content section.
804
1487
  this.topicModel = {
805
1488
  type,
806
- amf: this.amfMap[referenceId].model,
1489
+ amf: amfModelString,
807
1490
  parser: this.amfMap[referenceId].parser,
808
1491
  id: amfId
809
1492
  };
810
-
811
- window.scrollTo({ top: 0, behavior: "smooth" });
812
1493
  }
813
1494
  }