Skip to content

Commit 05ce16c

Browse files
authored
Bugfix: PrismJS loading improvements & error handling (#988)
* Created a new node script to auto-retrieve & auto-minify PrismJS files (including plugins & languages) to streamline the update process & to switch to minified versions * Updated folder structure for static resources to include a dedicated Prism directory, as well as a themes subdirectory * This is all a bit of a prefactor for eventually having other static resource files, and possibly supporting other Prism themes in the future * Updated LWC loggerCodeViewer to use the new PrismJS minified files & to add error handling for loading static resources * Now when there's an issue loading static resources or executing Prism, the LWC should (hopefully) show a fallback version of the code snippet - it won't look as nice as the Prism version, but it will let users see the relevant data, instead of endlessly displaying a spinner ~_~ * Finally fixed a long-standing issue in LWC loggerCodeViewer where calling Prism.highlightAll() twice was previously needed * Expanded tests for LWC logEntryMetadataViewer, and cleaned up a few bits of code
1 parent 62ceb99 commit 05ce16c

26 files changed

Lines changed: 26968 additions & 8446 deletions

File tree

.devcontainer/devcontainer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
"explorer.fileNesting.expand": false,
2121
"explorer.fileNesting.patterns": {
2222
"*.cls": "${capture}.cls-meta.xml",
23+
"*.css": "${capture}.min.css",
24+
"*.js": "${capture}.min.js",
2325
"*.page": "${capture}.page-meta.xml",
2426
"*.trigger": "${capture}.trigger-meta.xml",
2527
"*.view": "${capture}.view-meta.xml",

.forceignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,10 @@ nebula-logger/managed-package/**/*.testSuite-meta.xml
2121
**/tsconfig.json
2222

2323
**/*.ts
24+
25+
26+
# PrismJS - only the minified files are deployed, the unminified versions are
27+
# only kept in source control to make it easier to see what's changed when upgrading PrismJS.
28+
nebula-logger/core/main/log-management/staticresources/LoggerResources/Prism/prism.js
29+
nebula-logger/core/main/log-management/staticresources/LoggerResources/Prism/themes/prism-tomorrow.css
30+
nebula-logger/core/main/log-management/staticresources/LoggerResources/Prism/README.md

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
.sf/
55
.sfdx/
66
.vscode/
7+
.claude/
78
docs/apex/Miscellaneous/
89
temp/
910
test-coverage/

.prettierignore

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ yarn.lock
2020
*.log
2121
*.xml
2222

23-
# Prism JS
24-
# The CSS for Prism has specifically been modified for Nebula Logger (a bit), so it's NOT ignored.
25-
# nebula-logger/core/main/log-management/staticresources/LoggerResources/prism.css
26-
# The minified JS is taken as-is, so no need for prettier to format it.
27-
nebula-logger/core/main/log-management/staticresources/LoggerResources/prism.js
23+
# PrismJS - these files are automatically updated & minified,
24+
# so no need for prettier to format them.
25+
nebula-logger/core/main/log-management/staticresources/LoggerResources/Prism/prism.js
26+
nebula-logger/core/main/log-management/staticresources/LoggerResources/Prism/prism.min.js
27+
nebula-logger/core/main/log-management/staticresources/LoggerResources/Prism/themes/*.*

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55

66
The most robust observability solution for Salesforce experts. Built 100% natively on the platform, and designed to work seamlessly with Apex, Lightning Components, Flow, OmniStudio, and integrations.
77

8-
## Unlocked Package - v4.18.2
8+
## Unlocked Package - v4.18.3
99

10-
[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04tg70000008YZBAA2)
11-
[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tg70000008YZBAA2)
10+
[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04tg70000009GaDAAU)
11+
[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tg70000009GaDAAU)
1212
[![View Documentation](./images/btn-view-documentation.png)](https://github.com/jongpie/NebulaLogger/wiki)
1313

14-
`sf package install --wait 20 --security-type AdminsOnly --package 04tg70000008YZBAA2`
14+
`sf package install --wait 20 --security-type AdminsOnly --package 04tg70000009GaDAAU`
1515

1616
---
1717

nebula-logger/core/main/log-management/lwc/logEntryMetadataViewer/__tests__/logEntryMetadataViewer.test.js

Lines changed: 217 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jest.mock(
1010
return {
1111
loadScript() {
1212
return new Promise(resolve => {
13-
global.Prism = require('../../../staticresources/LoggerResources/prism.js');
13+
global.Prism = require('../../../staticresources/LoggerResources/Prism/prism.min.js');
1414
resolve();
1515
});
1616
},
@@ -31,6 +31,40 @@ jest.mock(
3131
{ virtual: true }
3232
);
3333

34+
// Number of microtask hops to drain across getRecord, c-logger-code-viewer creation,
35+
// and the Promise.all(map(async)) chain inside _loadPrismResources.
36+
const flushPromises = async () => {
37+
for (let i = 0; i < 8; i++) {
38+
/* eslint-disable-next-line no-await-in-loop */
39+
await Promise.resolve();
40+
}
41+
};
42+
43+
const buildSnippet = overrides => ({
44+
Code: 'some-code-block',
45+
ApiVersion: '65.0',
46+
TotalLinesOfCode: 123,
47+
StartingLineNumber: 55,
48+
TargetLineNumber: 65,
49+
EndingLineNumber: 68,
50+
...overrides
51+
});
52+
53+
const buildLogEntryRecord = ({ source, metadataType, snippet }) => {
54+
const apiNameField = source === 'Exception' ? 'ExceptionSourceApiName__c' : 'OriginSourceApiName__c';
55+
const apiVersionField = source === 'Exception' ? 'ExceptionSourceApiVersion__c' : 'OriginSourceApiVersion__c';
56+
const metadataTypeField = source === 'Exception' ? 'ExceptionSourceMetadataType__c' : 'OriginSourceMetadataType__c';
57+
const snippetField = source === 'Exception' ? 'ExceptionSourceSnippet__c' : 'OriginSourceSnippet__c';
58+
return {
59+
fields: {
60+
[apiNameField]: { value: 'SomeApexClass' },
61+
[apiVersionField]: { value: '65.0' },
62+
[metadataTypeField]: { value: metadataType },
63+
[snippetField]: { value: snippet === null ? null : JSON.stringify(snippet) }
64+
}
65+
};
66+
};
67+
3468
describe('LogEntryMetadataViewer LWC Tests', () => {
3569
afterEach(() => {
3670
while (document.body.firstChild) {
@@ -65,9 +99,7 @@ describe('LogEntryMetadataViewer LWC Tests', () => {
6599

66100
document.body.appendChild(element);
67101
getRecord.emit(mockLogEntryRecord);
68-
await Promise.resolve('resolves getRecord() call');
69-
await Promise.resolve('resolves creating an instance of c-logger-code-viewer');
70-
await Promise.resolve('resolves loading & running PrismJS inside of c-logger-code-viewer');
102+
await flushPromises();
71103

72104
const sectionTitle = element.shadowRoot.querySelector('c-logger-page-section span[slot="title"]');
73105
expect(sectionTitle).toBeTruthy();
@@ -116,9 +148,7 @@ describe('LogEntryMetadataViewer LWC Tests', () => {
116148

117149
document.body.appendChild(element);
118150
getRecord.emit(mockLogEntryRecord);
119-
await Promise.resolve('resolves getRecord() call');
120-
await Promise.resolve('resolves creating an instance of c-logger-code-viewer');
121-
await Promise.resolve('resolves loading & running PrismJS inside of c-logger-code-viewer');
151+
await flushPromises();
122152

123153
const sectionTitle = element.shadowRoot.querySelector('c-logger-page-section span[slot="title"]');
124154
expect(sectionTitle).toBeTruthy();
@@ -140,4 +170,184 @@ describe('LogEntryMetadataViewer LWC Tests', () => {
140170
expect(viewFullSourceButton.label).toBe('View Full Source');
141171
expect(viewFullSourceButton.variant).toBe('inverse');
142172
});
173+
174+
it('should show a spinner before the wired log entry record loads', async () => {
175+
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
176+
element.sourceMetadata = 'Exception';
177+
element.recordId = 'test-log-entry-id';
178+
179+
document.body.appendChild(element);
180+
await flushPromises();
181+
182+
expect(element.shadowRoot.querySelector('lightning-spinner')).toBeTruthy();
183+
expect(element.shadowRoot.querySelector('c-logger-page-section')).toBeNull();
184+
expect(element.shadowRoot.querySelector('c-logger-code-viewer')).toBeNull();
185+
});
186+
187+
it('should render "No source snippet available" when the snippet field is empty', async () => {
188+
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
189+
element.sourceMetadata = 'Exception';
190+
element.recordId = 'test-log-entry-id';
191+
192+
document.body.appendChild(element);
193+
getRecord.emit(buildLogEntryRecord({ source: 'Exception', metadataType: 'ApexClass', snippet: null }));
194+
await flushPromises();
195+
196+
expect(element.shadowRoot.querySelector('lightning-spinner')).toBeNull();
197+
expect(element.shadowRoot.querySelector('c-logger-page-section')).toBeNull();
198+
expect(element.shadowRoot.querySelector('c-logger-code-viewer')).toBeNull();
199+
expect(element.shadowRoot.textContent).toContain('No source snippet available');
200+
});
201+
202+
it('should produce a .trigger title for ApexTrigger metadata', async () => {
203+
getMetadata.mockResolvedValue({ Code: 'full code here', HasCodeBeenModified: false });
204+
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
205+
element.sourceMetadata = 'Exception';
206+
element.recordId = 'test-log-entry-id';
207+
208+
document.body.appendChild(element);
209+
getRecord.emit(buildLogEntryRecord({ source: 'Exception', metadataType: 'ApexTrigger', snippet: buildSnippet() }));
210+
await flushPromises();
211+
212+
const codeViewerTitle = element.shadowRoot.querySelector('c-logger-code-viewer span[slot="title"]');
213+
expect(codeViewerTitle).toBeTruthy();
214+
expect(codeViewerTitle.textContent).toBe('SomeApexClass.trigger - 65.0');
215+
});
216+
217+
it('should hide the "View Full Source" button when there is no full source metadata', async () => {
218+
// getMetadata resolves with an empty object → hasFullSourceMetadata is false.
219+
getMetadata.mockResolvedValue({});
220+
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
221+
element.sourceMetadata = 'Exception';
222+
element.recordId = 'test-log-entry-id';
223+
224+
document.body.appendChild(element);
225+
getRecord.emit(buildLogEntryRecord({ source: 'Exception', metadataType: 'ApexClass', snippet: buildSnippet() }));
226+
await flushPromises();
227+
228+
const actionsSlotButtons = element.shadowRoot.querySelectorAll('c-logger-code-viewer span[slot="actions"] lightning-button');
229+
expect(actionsSlotButtons.length).toBe(0);
230+
});
231+
232+
it('should open the full-source modal with success notification when code is unmodified', async () => {
233+
getMetadata.mockResolvedValue({ Code: 'full code here', HasCodeBeenModified: false });
234+
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
235+
element.sourceMetadata = 'Exception';
236+
element.recordId = 'test-log-entry-id';
237+
document.body.appendChild(element);
238+
getRecord.emit(buildLogEntryRecord({ source: 'Exception', metadataType: 'ApexClass', snippet: buildSnippet() }));
239+
await flushPromises();
240+
241+
const viewFullSourceButton = element.shadowRoot.querySelector('c-logger-code-viewer span[slot="actions"] lightning-button');
242+
viewFullSourceButton.click();
243+
await flushPromises();
244+
245+
const modalSection = element.shadowRoot.querySelector('section.slds-modal');
246+
expect(modalSection).toBeTruthy();
247+
const modalTitle = element.shadowRoot.querySelector('section.slds-modal h2.slds-text-heading_medium');
248+
expect(modalTitle.textContent).toBe('Full Source: SomeApexClass.cls - 65.0');
249+
const notification = element.shadowRoot.querySelector('section.slds-modal div[role="alert"]');
250+
expect(notification.classList.contains('slds-theme_success')).toBe(true);
251+
expect(notification.classList.contains('slds-theme_warning')).toBe(false);
252+
const notificationIcon = notification.querySelector('lightning-icon');
253+
expect(notificationIcon.iconName).toBe('utility:success');
254+
const notificationMessage = notification.querySelector('h2');
255+
expect(notificationMessage.textContent).toBe('This Apex code has not been modified since this log entry was generated.');
256+
const modalCodeViewer = element.shadowRoot.querySelector('section.slds-modal c-logger-code-viewer');
257+
expect(modalCodeViewer).toBeTruthy();
258+
expect(modalCodeViewer.code).toBe('full code here');
259+
});
260+
261+
it('should open the full-source modal with warning notification when code has been modified', async () => {
262+
getMetadata.mockResolvedValue({ Code: 'modified code', HasCodeBeenModified: true });
263+
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
264+
element.sourceMetadata = 'Exception';
265+
element.recordId = 'test-log-entry-id';
266+
document.body.appendChild(element);
267+
getRecord.emit(buildLogEntryRecord({ source: 'Exception', metadataType: 'ApexClass', snippet: buildSnippet() }));
268+
await flushPromises();
269+
270+
element.shadowRoot.querySelector('c-logger-code-viewer span[slot="actions"] lightning-button').click();
271+
await flushPromises();
272+
273+
const notification = element.shadowRoot.querySelector('section.slds-modal div[role="alert"]');
274+
expect(notification.classList.contains('slds-theme_success')).toBe(false);
275+
expect(notification.classList.contains('slds-theme_warning')).toBe(true);
276+
const notificationIcon = notification.querySelector('lightning-icon');
277+
expect(notificationIcon.iconName).toBe('utility:warning');
278+
const notificationMessage = notification.querySelector('h2');
279+
expect(notificationMessage.textContent).toBe('This Apex code has been modified since this log entry was generated.');
280+
});
281+
282+
it('should close the modal when the header close icon is clicked', async () => {
283+
getMetadata.mockResolvedValue({ Code: 'full code here', HasCodeBeenModified: false });
284+
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
285+
element.sourceMetadata = 'Exception';
286+
element.recordId = 'test-log-entry-id';
287+
document.body.appendChild(element);
288+
getRecord.emit(buildLogEntryRecord({ source: 'Exception', metadataType: 'ApexClass', snippet: buildSnippet() }));
289+
await flushPromises();
290+
291+
element.shadowRoot.querySelector('c-logger-code-viewer span[slot="actions"] lightning-button').click();
292+
await flushPromises();
293+
expect(element.shadowRoot.querySelector('section.slds-modal')).toBeTruthy();
294+
295+
element.shadowRoot.querySelector('section.slds-modal button.slds-modal__close').click();
296+
await flushPromises();
297+
expect(element.shadowRoot.querySelector('section.slds-modal')).toBeNull();
298+
});
299+
300+
it('should close the modal when the footer Close button is clicked', async () => {
301+
getMetadata.mockResolvedValue({ Code: 'full code here', HasCodeBeenModified: false });
302+
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
303+
element.sourceMetadata = 'Exception';
304+
element.recordId = 'test-log-entry-id';
305+
document.body.appendChild(element);
306+
getRecord.emit(buildLogEntryRecord({ source: 'Exception', metadataType: 'ApexClass', snippet: buildSnippet() }));
307+
await flushPromises();
308+
309+
element.shadowRoot.querySelector('c-logger-code-viewer span[slot="actions"] lightning-button').click();
310+
await flushPromises();
311+
312+
element.shadowRoot.querySelector('lightning-button[data-id="close-btn"]').click();
313+
await flushPromises();
314+
expect(element.shadowRoot.querySelector('section.slds-modal')).toBeNull();
315+
});
316+
317+
it('should close the modal when the Escape key is pressed', async () => {
318+
getMetadata.mockResolvedValue({ Code: 'full code here', HasCodeBeenModified: false });
319+
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
320+
element.sourceMetadata = 'Exception';
321+
element.recordId = 'test-log-entry-id';
322+
document.body.appendChild(element);
323+
getRecord.emit(buildLogEntryRecord({ source: 'Exception', metadataType: 'ApexClass', snippet: buildSnippet() }));
324+
await flushPromises();
325+
326+
element.shadowRoot.querySelector('c-logger-code-viewer span[slot="actions"] lightning-button').click();
327+
await flushPromises();
328+
329+
const modalSection = element.shadowRoot.querySelector('section.slds-modal');
330+
modalSection.dispatchEvent(new KeyboardEvent('keydown', { code: 'Escape', bubbles: true }));
331+
await flushPromises();
332+
333+
expect(element.shadowRoot.querySelector('section.slds-modal')).toBeNull();
334+
});
335+
336+
it('should leave the modal open when a non-Escape key is pressed', async () => {
337+
getMetadata.mockResolvedValue({ Code: 'full code here', HasCodeBeenModified: false });
338+
const element = createElement('c-log-entry-metadata-viewer', { is: LogEntryMetadataViewer });
339+
element.sourceMetadata = 'Exception';
340+
element.recordId = 'test-log-entry-id';
341+
document.body.appendChild(element);
342+
getRecord.emit(buildLogEntryRecord({ source: 'Exception', metadataType: 'ApexClass', snippet: buildSnippet() }));
343+
await flushPromises();
344+
element.shadowRoot.querySelector('c-logger-code-viewer span[slot="actions"] lightning-button').click();
345+
await flushPromises();
346+
347+
const modalSection = element.shadowRoot.querySelector('section.slds-modal');
348+
modalSection.dispatchEvent(new KeyboardEvent('keydown', { code: 'Enter', bubbles: true }));
349+
await flushPromises();
350+
351+
expect(element.shadowRoot.querySelector('section.slds-modal')).toBeTruthy();
352+
});
143353
});

nebula-logger/core/main/log-management/lwc/logEntryMetadataViewer/logEntryMetadataViewer.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
**********************************************************************************************-->
55

66
<template>
7-
<template if:false={hasLoaded}>
7+
<template if:false={isLoaded}>
88
<div class="slds-is-relative" style="min-height: 6em">
99
<lightning-spinner></lightning-spinner>
1010
</div>
1111
</template>
12-
<template if:true={hasLoaded}>
12+
<template if:true={isLoaded}>
1313
<template if:false={sourceSnippet}>No source snippet available</template>
1414
</template>
1515
<template if:true={sourceSnippet}>

nebula-logger/core/main/log-management/lwc/logEntryMetadataViewer/logEntryMetadataViewer.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export default class LogEntryMetadataViewer extends LightningElement {
3333
@api sourceMetadata;
3434

3535
objectApiName = LOG_ENTRY_OBJECT;
36-
hasLoaded = false;
36+
isLoaded = false;
3737
sourceSnippet;
3838

3939
showFullSourceMetadataModal = false;
@@ -59,7 +59,7 @@ export default class LogEntryMetadataViewer extends LightningElement {
5959

6060
get fullSourceModalNotificationClasses() {
6161
const classNames = ['slds-notify', 'slds-notify_alert'];
62-
classNames.push(this._logEntryMetadata?.HasCodeBeenModified ? 'slds-alert_warning' : 'slds-alert_offline');
62+
classNames.push(this._logEntryMetadata?.HasCodeBeenModified ? 'slds-theme_warning' : 'slds-theme_success');
6363
return classNames.join(' ');
6464
}
6565

@@ -85,6 +85,7 @@ export default class LogEntryMetadataViewer extends LightningElement {
8585
const sourceSnippetJson = getFieldValue(this._logEntry, sourceSnippetField);
8686

8787
if (!sourceSnippetJson) {
88+
this.isLoaded = true;
8889
return;
8990
}
9091

@@ -94,13 +95,13 @@ export default class LogEntryMetadataViewer extends LightningElement {
9495
const sourceApiName = getFieldValue(this._logEntry, sourceApiNameField);
9596
const sourceApiVersion = getFieldValue(this._logEntry, sourceApiVersionField);
9697
const sourceName = `${sourceApiName}.${sourceExtension} - ${sourceApiVersion}`;
97-
this.sourceSnippet = { ...JSON.parse(sourceSnippetJson), ...{ Title: sourceName } };
9898

99-
this.hasLoaded = true;
99+
this.sourceSnippet = { ...JSON.parse(sourceSnippetJson), ...{ Title: sourceName } };
100100
this._logEntryMetadata = await getMetadata({
101101
recordId: this.recordId,
102102
sourceMetadata: this.sourceMetadata
103103
});
104+
this.isLoaded = true;
104105
}
105106
}
106107

nebula-logger/core/main/log-management/lwc/logViewer/__tests__/logViewer.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jest.mock(
3636
return {
3737
loadScript() {
3838
return new Promise((resolve, _) => {
39-
global.Prism = require('../../../staticresources/LoggerResources/prism.js');
39+
global.Prism = require('../../../staticresources/LoggerResources/Prism/prism.min.js');
4040
resolve();
4141
});
4242
},

0 commit comments

Comments
 (0)