@salesforcedevs/docs-components 1.14.3-alpha → 1.14.4-edit

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/docs-components",
3
- "version": "1.14.3-alpha",
3
+ "version": "1.14.4-edit",
4
4
  "description": "Docs Lightning web components for DSC",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -13,6 +13,7 @@
13
13
  "dependencies": {
14
14
  "@api-components/amf-helper-mixin": "4.5.29",
15
15
  "classnames": "2.5.1",
16
+ "dompurify": "3.2.4",
16
17
  "kagekiri": "1.4.2",
17
18
  "lodash.orderby": "4.6.0",
18
19
  "lodash.uniqby": "4.7.0",
@@ -0,0 +1,70 @@
1
+ # Markdown Editor Component
2
+
3
+ A Lightning Web Component that provides an HTML-to-Markdown editor with Monaco Editor integration for documentation editing.
4
+
5
+ ## Features
6
+
7
+ - **HTML to Markdown Conversion**: Automatically converts HTML content to Markdown format
8
+ - **Monaco Editor Integration**: Rich code editing experience with syntax highlighting
9
+ - **Basic Save Functionality**: Save edited content to a configured endpoint
10
+ - **Responsive Design**: Works on desktop and mobile devices
11
+ - **Loading States**: Visual feedback during save operations
12
+
13
+ ## Usage
14
+
15
+ ```html
16
+ <doc-markdown-editor
17
+ content="<h1>Your HTML content here</h1>"
18
+ file-path="/docs/your-file.md"
19
+ save-endpoint="/api/save"
20
+ auth-token="your-auth-token">
21
+ </doc-markdown-editor>
22
+ ```
23
+
24
+ ## API Properties
25
+
26
+ | Property | Type | Required | Description |
27
+ |----------|------|----------|-------------|
28
+ | `content` | string | Yes | The HTML content to display and edit |
29
+ | `filePath` | string | No | The file path for saving (used in save requests) |
30
+ | `saveEndpoint` | string | No | The API endpoint for saving content |
31
+ | `authToken` | string | No | Authentication token for API calls |
32
+
33
+ ## Events
34
+
35
+ | Event | Detail | Description |
36
+ |-------|--------|-------------|
37
+ | `saved` | `{success: boolean, content: string, filePath: string}` | Fired when content is successfully saved |
38
+
39
+ ## Dependencies
40
+
41
+ - **Monaco Editor**: Loaded dynamically from CDN
42
+ - **Turndown.js**: Loaded dynamically from CDN for HTML-to-Markdown conversion
43
+
44
+ ## Browser Support
45
+
46
+ - Modern browsers with ES6+ support
47
+ - Requires JavaScript enabled
48
+ - Monaco Editor requires modern browser features
49
+
50
+ ## Development
51
+
52
+ To test the component locally:
53
+
54
+ 1. Start Storybook: `yarn storybook`
55
+ 2. Navigate to "Doc/MarkdownEditor" story
56
+ 3. Test the edit functionality
57
+
58
+ ## Save Endpoint Format
59
+
60
+ The save endpoint should accept POST requests with the following JSON payload:
61
+
62
+ ```json
63
+ {
64
+ "content": "markdown content",
65
+ "filePath": "/path/to/file.md",
66
+ "originalContent": "original html content"
67
+ }
68
+ ```
69
+
70
+ Expected response: HTTP 200 for success, any other status for failure.
@@ -0,0 +1,169 @@
1
+ .markdown-editor {
2
+ border: 1px solid #ddd;
3
+ border-radius: 4px;
4
+ margin: 1rem 0;
5
+ background: #fff;
6
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
7
+ }
8
+
9
+ .editor-toolbar {
10
+ display: flex;
11
+ align-items: center;
12
+ gap: 0.5rem;
13
+ padding: 0.75rem;
14
+ border-bottom: 1px solid #ddd;
15
+ background: #f8f9fa;
16
+ border-radius: 4px 4px 0 0;
17
+ justify-content: space-between;
18
+ }
19
+
20
+ .edit-button {
21
+ margin-left: auto;
22
+ min-width: 80px;
23
+ }
24
+
25
+ .button-group {
26
+ display: flex;
27
+ gap: 0.5rem;
28
+ margin-left: auto;
29
+ }
30
+
31
+ .cancel-button,
32
+ .save-button {
33
+ min-width: 80px;
34
+ }
35
+
36
+ .status-indicator {
37
+ font-size: 0.875rem;
38
+ font-weight: 500;
39
+ }
40
+
41
+ .status-saving {
42
+ color: #007bff;
43
+ }
44
+
45
+ .status-success {
46
+ color: #28a745;
47
+ }
48
+
49
+ .status-error {
50
+ color: #dc3545;
51
+ }
52
+
53
+ .content-viewer {
54
+ padding: 1rem;
55
+ min-height: 100px;
56
+ }
57
+
58
+ .content-display {
59
+ line-height: 1.6;
60
+ }
61
+
62
+ .content-display h1,
63
+ .content-display h2,
64
+ .content-display h3,
65
+ .content-display h4,
66
+ .content-display h5,
67
+ .content-display h6 {
68
+ margin-top: 1.5rem;
69
+ margin-bottom: 0.5rem;
70
+ font-weight: 600;
71
+ }
72
+
73
+ .content-display p {
74
+ margin-bottom: 1rem;
75
+ }
76
+
77
+ .content-display code {
78
+ background: #f1f3f4;
79
+ padding: 0.125rem 0.25rem;
80
+ border-radius: 3px;
81
+ font-size: 0.875em;
82
+ }
83
+
84
+ .content-display pre {
85
+ background: #f8f9fa;
86
+ padding: 1rem;
87
+ border-radius: 4px;
88
+ overflow-x: auto;
89
+ margin: 1rem 0;
90
+ }
91
+
92
+ .content-display pre code {
93
+ background: none;
94
+ padding: 0;
95
+ }
96
+
97
+ .editor-wrapper {
98
+ position: relative;
99
+ min-height: 400px;
100
+ }
101
+
102
+ .editor-container {
103
+ height: 400px;
104
+ min-height: 200px;
105
+ border-radius: 0 0 4px 4px;
106
+ }
107
+
108
+ .simple-editor {
109
+ width: 100%;
110
+ height: 400px;
111
+ min-height: 200px;
112
+ padding: 1rem;
113
+ border: none;
114
+ border-radius: 0 0 4px 4px;
115
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
116
+ font-size: 14px;
117
+ line-height: 1.5;
118
+ resize: vertical;
119
+ background: #f8f9fa;
120
+ color: #333;
121
+ }
122
+
123
+ .simple-editor:focus {
124
+ outline: none;
125
+ background: #fff;
126
+ box-shadow: inset 0 0 0 2px #007bff;
127
+ }
128
+
129
+ .loading-overlay {
130
+ position: absolute;
131
+ top: 0;
132
+ left: 0;
133
+ right: 0;
134
+ bottom: 0;
135
+ background: rgba(255, 255, 255, 0.8);
136
+ display: flex;
137
+ align-items: center;
138
+ justify-content: center;
139
+ z-index: 1000;
140
+ border-radius: 0 0 4px 4px;
141
+ }
142
+
143
+ /* Monaco Editor customizations */
144
+ .monaco-editor {
145
+ border-radius: 0 0 4px 4px;
146
+ }
147
+
148
+ .monaco-editor .margin {
149
+ background: #f8f9fa;
150
+ }
151
+
152
+ /* Responsive design */
153
+ @media (max-width: 768px) {
154
+ .editor-toolbar {
155
+ flex-wrap: wrap;
156
+ gap: 0.25rem;
157
+ }
158
+
159
+ .status-indicator {
160
+ margin-left: 0;
161
+ width: 100%;
162
+ text-align: center;
163
+ margin-top: 0.5rem;
164
+ }
165
+
166
+ .editor-container {
167
+ height: 300px;
168
+ }
169
+ }
@@ -0,0 +1,64 @@
1
+ <template>
2
+ <div class="markdown-editor">
3
+ <!-- Editor Toolbar -->
4
+ <div class="editor-toolbar">
5
+ <template if:true={showViewer}>
6
+ <dx-button
7
+ onclick={handleEdit}
8
+ variant="tertiary"
9
+ class="edit-button">
10
+ Edit Content
11
+ </dx-button>
12
+ </template>
13
+
14
+ <template if:true={showEditor}>
15
+ <div class="button-group">
16
+ <dx-button
17
+ onclick={handleCancel}
18
+ variant="secondary"
19
+ class="cancel-button">
20
+ Cancel
21
+ </dx-button>
22
+
23
+ <dx-button
24
+ onclick={handleSave}
25
+ variant="primary"
26
+ disabled={saveButtonDisabled}
27
+ class="save-button">
28
+ Save
29
+ </dx-button>
30
+ </div>
31
+
32
+ <div class="status-indicator">
33
+ <span class={statusClass}>{statusMessage}</span>
34
+ </div>
35
+ </template>
36
+ </div>
37
+
38
+ <!-- Content Viewer (Read-only) -->
39
+ <template if:true={showViewer}>
40
+ <div class="content-viewer">
41
+ <div class="content-display" innerhtml={content}></div>
42
+ </div>
43
+ </template>
44
+
45
+ <!-- Simple Editor -->
46
+ <template if:true={showEditor}>
47
+ <div class="editor-wrapper">
48
+ <textarea
49
+ class="simple-editor"
50
+ value={markdownContent}
51
+ onchange={handleContentChange}
52
+ placeholder="Enter your markdown content here...">
53
+ </textarea>
54
+ </div>
55
+ </template>
56
+
57
+ <!-- Loading Overlay -->
58
+ <template if:true={isLoading}>
59
+ <div class="loading-overlay">
60
+ <dx-spinner size="medium" alternative-text="Loading"></dx-spinner>
61
+ </div>
62
+ </template>
63
+ </div>
64
+ </template>
@@ -0,0 +1,111 @@
1
+ import { LightningElement, api, track } from 'lwc';
2
+
3
+ export default class MarkdownEditor extends LightningElement {
4
+ @api content: string = '';
5
+ @api filePath: string = '';
6
+ @api saveEndpoint: string = '';
7
+ @api authToken: string = '';
8
+
9
+ @track isEditing: boolean = false;
10
+ @track markdownContent: string = '';
11
+ @track isLoading: boolean = false;
12
+ @track saveStatus: 'idle' | 'saving' | 'success' | 'error' = 'idle';
13
+
14
+ connectedCallback() {
15
+ this.markdownContent = this.content; // Simple fallback for now
16
+ }
17
+
18
+ // Event Handlers
19
+ handleEdit() {
20
+ this.isEditing = true;
21
+ this.saveStatus = 'idle';
22
+ }
23
+
24
+ handleCancel() {
25
+ this.isEditing = false;
26
+ this.saveStatus = 'idle';
27
+ }
28
+
29
+ handleContentChange(event: Event) {
30
+ const target = event.target as HTMLTextAreaElement;
31
+ this.markdownContent = target.value;
32
+ }
33
+
34
+ async handleSave() {
35
+ if (!this.saveEndpoint) {
36
+ console.error('Save endpoint not configured');
37
+ this.saveStatus = 'error';
38
+ return;
39
+ }
40
+
41
+ this.saveStatus = 'saving';
42
+ this.isLoading = true;
43
+
44
+ try {
45
+ const response = await fetch(this.saveEndpoint, {
46
+ method: 'POST',
47
+ headers: {
48
+ 'Content-Type': 'application/json',
49
+ 'Authorization': this.authToken ? `Bearer ${this.authToken}` : ''
50
+ },
51
+ body: JSON.stringify({
52
+ content: this.markdownContent,
53
+ filePath: this.filePath,
54
+ originalContent: this.content
55
+ })
56
+ });
57
+
58
+ if (response.ok) {
59
+ this.saveStatus = 'success';
60
+ this.isEditing = false;
61
+
62
+ // Dispatch save success event
63
+ this.dispatchEvent(new CustomEvent('saved', {
64
+ detail: {
65
+ success: true,
66
+ content: this.markdownContent,
67
+ filePath: this.filePath
68
+ }
69
+ }));
70
+ } else {
71
+ throw new Error(`Save failed: ${response.status}`);
72
+ }
73
+ } catch (error) {
74
+ console.error('Save failed:', error);
75
+ this.saveStatus = 'error';
76
+ } finally {
77
+ this.isLoading = false;
78
+ }
79
+ }
80
+
81
+ // Getters for UI state
82
+ get showEditor() {
83
+ return this.isEditing;
84
+ }
85
+
86
+ get showViewer() {
87
+ return !this.isEditing;
88
+ }
89
+
90
+ get saveButtonDisabled() {
91
+ return this.saveStatus === 'saving' || this.isLoading;
92
+ }
93
+
94
+ get statusMessage() {
95
+ switch (this.saveStatus) {
96
+ case 'saving': return 'Saving...';
97
+ case 'success': return 'Saved successfully!';
98
+ case 'error': return 'Save failed. Please try again.';
99
+ default: return '';
100
+ }
101
+ }
102
+
103
+ get statusClass() {
104
+ switch (this.saveStatus) {
105
+ case 'saving': return 'status-saving';
106
+ case 'success': return 'status-success';
107
+ case 'error': return 'status-error';
108
+ default: return '';
109
+ }
110
+ }
111
+ }
@@ -18,6 +18,7 @@ import { LightningElementWithState } from "dxBaseElements/lightningElementWithSt
18
18
  import { logCoveoPageView, oldVersionDocInfo } from "docUtils/utils";
19
19
  import { Breadcrumb, DocPhaseInfo, Language } from "typings/custom";
20
20
  import { track as trackGTM } from "dxUtils/analytics";
21
+ import DOMPurify from "dompurify";
21
22
 
22
23
  // TODO: Imitating from actual implementation as doc-content use it like this. We should refactor it later.
23
24
  const handleContentError = (error: any): void => console.log(error);
@@ -425,9 +426,19 @@ export default class DocXmlContent extends LightningElementWithState<{
425
426
  .catch(handleContentError);
426
427
  }
427
428
 
429
+ private sanitizeUrlPart(part: string | undefined): string | undefined {
430
+ if (!part) {
431
+ return part;
432
+ }
433
+ return DOMPurify.sanitize(part);
434
+ }
435
+
428
436
  getReferenceFromUrl(): PageReference {
429
437
  const [page, docId, deliverable, contentDocumentId] =
430
- window.location.pathname.substr(1).split("/");
438
+ window.location.pathname
439
+ .substr(1)
440
+ .split("/")
441
+ .map(this.sanitizeUrlPart);
431
442
 
432
443
  const { origin: domain, hash, search } = window.location;
433
444
 
@@ -436,9 +447,9 @@ export default class DocXmlContent extends LightningElementWithState<{
436
447
  deliverable,
437
448
  docId,
438
449
  domain,
439
- hash,
450
+ hash: this.sanitizeUrlPart(hash),
440
451
  page,
441
- search
452
+ search: this.sanitizeUrlPart(search)
442
453
  };
443
454
  }
444
455
 
@@ -701,7 +712,7 @@ export default class DocXmlContent extends LightningElementWithState<{
701
712
 
702
713
  addMetatags(): void {
703
714
  const div = document.createElement("div");
704
- div.innerHTML = this.docContent;
715
+ div.innerHTML = DOMPurify.sanitize(this.docContent);
705
716
  const docDescription = div.querySelector(".shortdesc")?.textContent;
706
717
  const topicTitle = div.querySelector("h1")?.textContent;
707
718