Skip to content

Commit 39fdcf8

Browse files
authored
Merge branch 'ServiceNowDevProgram:main' into main
2 parents 3965223 + 2fca83b commit 39fdcf8

File tree

112 files changed

+3076
-11
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

112 files changed

+3076
-11
lines changed

.github/pull_request_template.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# PR Description:
2+
3+
14
# Pull Request Checklist
25

36
## Overview

.github/scripts/validate-structure.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,34 @@ function validateFilePath(filePath) {
4949
const normalized = filePath.replace(/\\/g, '/');
5050
const segments = normalized.split('/');
5151

52+
// Check for invalid characters that break local file systems
53+
for (let i = 0; i < segments.length; i++) {
54+
const segment = segments[i];
55+
56+
// Check for trailing periods (invalid on Windows)
57+
if (segment.endsWith('.')) {
58+
return `Invalid folder/file name '${segment}' in path '${normalized}': Names cannot end with a period (.) as this breaks local file system sync on Windows.`;
59+
}
60+
61+
// Check for trailing spaces (invalid on Windows)
62+
if (segment.endsWith(' ')) {
63+
return `Invalid folder/file name '${segment}' in path '${normalized}': Names cannot end with a space as this breaks local file system sync on Windows.`;
64+
}
65+
66+
// Check for reserved Windows names
67+
const reservedNames = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'];
68+
const nameWithoutExt = segment.split('.')[0].toUpperCase();
69+
if (reservedNames.includes(nameWithoutExt)) {
70+
return `Invalid folder/file name '${segment}' in path '${normalized}': '${nameWithoutExt}' is a reserved name on Windows and will break local file system sync.`;
71+
}
72+
73+
// Check for invalid characters (Windows and general file system restrictions)
74+
const invalidChars = /[<>:"|?*\x00-\x1F]/;
75+
if (invalidChars.test(segment)) {
76+
return `Invalid folder/file name '${segment}' in path '${normalized}': Contains characters that are invalid on Windows file systems (< > : " | ? * or control characters).`;
77+
}
78+
}
79+
5280
if (!allowedCategories.has(segments[0])) {
5381
return null;
5482
}

.github/workflows/validate-structure.yml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ jobs:
6565
cat "$tmp_error" >&2
6666
6767
if grep -q 'Folder structure violations found' "$tmp_output" "$tmp_error"; then
68+
# Save validation output for use in PR comment
69+
cat "$tmp_output" "$tmp_error" > "$RUNNER_TEMP/validation_output.txt"
6870
echo "status=failed" >> "$GITHUB_OUTPUT"
6971
exit 0
7072
fi
@@ -87,11 +89,37 @@ jobs:
8789
const owner = context.repo.owner;
8890
const repo = context.repo.repo;
8991
92+
const fs = require('fs');
93+
const output = fs.readFileSync(process.env.RUNNER_TEMP + '/validation_output.txt', 'utf8');
94+
95+
let commentBody = `Thank you for your contribution. However, it doesn't comply with our contributing guidelines.\n\n`;
96+
97+
// Check if the error is about invalid file/folder names
98+
if (output.includes('Names cannot end with a period') ||
99+
output.includes('Names cannot end with a space') ||
100+
output.includes('is a reserved name on Windows') ||
101+
output.includes('Contains characters that are invalid')) {
102+
commentBody += `**❌ Invalid File/Folder Names Detected**\n\n`;
103+
commentBody += `Your contribution contains file or folder names that will break when syncing to local file systems (especially Windows):\n\n`;
104+
commentBody += `\`\`\`\n${output}\n\`\`\`\n\n`;
105+
commentBody += `**Common issues:**\n`;
106+
commentBody += `- Folder/file names ending with a period (.) - not allowed on Windows\n`;
107+
commentBody += `- Folder/file names ending with spaces - not allowed on Windows\n`;
108+
commentBody += `- Reserved names like CON, PRN, AUX, NUL, COM1-9, LPT1-9 - not allowed on Windows\n`;
109+
commentBody += `- Invalid characters: < > : " | ? * or control characters\n\n`;
110+
commentBody += `Please rename these files/folders to be compatible with all operating systems.\n\n`;
111+
} else {
112+
commentBody += `As a reminder, the general requirements (as outlined in the [CONTRIBUTING.md file](https://github.com/ServiceNowDevProgram/code-snippets/blob/main/CONTRIBUTING.md)) are the following: follow the folder+subfolder guidelines and include a README.md file explaining what the code snippet does.\n\n`;
113+
commentBody += `**Validation errors:**\n\`\`\`\n${output}\n\`\`\`\n\n`;
114+
}
115+
116+
commentBody += `Review your contribution against the guidelines and make the necessary adjustments. Closing this for now. Once you make additional changes, feel free to re-open this Pull Request or create a new one.`;
117+
90118
await github.rest.issues.createComment({
91119
owner,
92120
repo,
93121
issue_number: pullNumber,
94-
body: `Thank you for your contribution. However, it doesn't comply with our contributing guidelines. As a reminder, the general requirements (as outlined in the [CONTRIBUTING.md file](https://github.com/ServiceNowDevProgram/code-snippets/blob/main/CONTRIBUTING.md)) are the following: follow the folder+subfolder guidelines and include a README.md file explaining what the code snippet does. Review your contribution against the guidelines and make the necessary adjustments. Closing this for now. Once you make additional changes, feel free to re-open this Pull Request or create a new one.`.trim()
122+
body: commentBody.trim()
95123
});
96124
97125
await github.rest.pulls.update({
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Auto Save Draft Feature for Catalog Items
2+
3+
This snippet provides automatic draft saving functionality for ServiceNow Catalog Items, helping prevent data loss by automatically saving form data at regular intervals.
4+
5+
## Overview
6+
7+
The feature includes two implementations:
8+
1. Basic Implementation (`basic_implementation.js`)
9+
2. Advanced Implementation (`advanced_implementation.js`)
10+
11+
## Basic Implementation
12+
13+
### Features
14+
- Auto-saves form data every minute
15+
- Stores single draft in sessionStorage
16+
- Provides draft restoration on form load
17+
- Basic error handling and user feedback
18+
19+
### Usage
20+
```javascript
21+
// Apply in Catalog Client Script
22+
// Select "onLoad" for "Client script runs"
23+
// Copy content from basic_implementation.js
24+
```
25+
26+
## Advanced Implementation
27+
28+
### Enhanced Features
29+
- Multiple draft support (keeps last 3 drafts)
30+
- Advanced draft management
31+
- Draft selection dialog
32+
- Detailed metadata tracking
33+
- Improved error handling
34+
- User-friendly notifications
35+
36+
### Usage
37+
```javascript
38+
// Apply in Catalog Client Script
39+
// Select "onLoad" for "Client script runs"
40+
// Copy content from advanced_implementation.js
41+
```
42+
43+
## Technical Details
44+
45+
### Dependencies
46+
- ServiceNow Platform UI Framework
47+
- GlideForm API
48+
- GlideModal (advanced implementation only)
49+
50+
### Browser Support
51+
- Modern browsers with sessionStorage support
52+
- ES5+ compatible
53+
54+
### Security Considerations
55+
- Uses browser's sessionStorage (cleared on session end)
56+
- No sensitive data transmission
57+
- Instance-specific storage
58+
59+
## Implementation Guide
60+
61+
1. Create a new Catalog Client Script:
62+
- Table: Catalog Client Script [catalog_script_client]
63+
- Type: onLoad
64+
- Active: true
65+
66+
2. Choose implementation:
67+
- For basic needs: Copy `basic_implementation.js`
68+
- For advanced features: Copy `advanced_implementation.js`
69+
70+
3. Apply to desired Catalog Items:
71+
- Select applicable Catalog Items
72+
- Test in dev environment first
73+
74+
## Best Practices
75+
76+
1. Testing:
77+
- Test with various form states
78+
- Verify draft restoration
79+
- Check browser storage limits
80+
81+
2. Performance:
82+
- Default 60-second interval is recommended
83+
- Adjust based on form complexity
84+
- Monitor browser memory usage
85+
86+
3. User Experience:
87+
- Clear feedback messages
88+
- Confirmation dialogs
89+
- Error notifications
90+
91+
## Limitations
92+
93+
- Browser session dependent
94+
- Storage size limits
95+
- Form field compatibility varies
96+
97+
## Troubleshooting
98+
99+
Common issues and solutions:
100+
1. Draft not saving
101+
- Check browser console for errors
102+
- Verify sessionStorage availability
103+
- Check form modification detection
104+
105+
2. Restoration fails
106+
- Validate stored data format
107+
- Check browser storage permissions
108+
- Verify form field compatibility
109+
110+
## Version Information
111+
112+
- Compatible with ServiceNow: Rome and later
113+
- Browser Requirements: Modern browsers with ES5+ support
114+
- Last Updated: October 2025
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/**
2+
* Advanced Auto-save Draft Implementation with Enhanced Features
3+
* This version adds multi-draft support and advanced error handling
4+
*/
5+
6+
function onLoad() {
7+
var autosaveInterval = 60000; // 1 minute
8+
var maxDrafts = 3; // Maximum number of drafts to keep
9+
10+
// Initialize draft manager
11+
initializeDraftManager();
12+
13+
// Try to restore previous draft
14+
restoreLastDraft();
15+
16+
// Set up auto-save interval
17+
setInterval(function() {
18+
if (g_form.isModified()) {
19+
saveAdvancedDraft();
20+
}
21+
}, autosaveInterval);
22+
}
23+
24+
function initializeDraftManager() {
25+
window.draftManager = {
26+
maxDrafts: 3,
27+
draftPrefix: 'catalogDraft_' + g_form.getUniqueValue() + '_',
28+
29+
getAllDrafts: function() {
30+
var drafts = [];
31+
for (var i = 0; i < sessionStorage.length; i++) {
32+
var key = sessionStorage.key(i);
33+
if (key.startsWith(this.draftPrefix)) {
34+
drafts.push({
35+
key: key,
36+
data: JSON.parse(sessionStorage.getItem(key))
37+
});
38+
}
39+
}
40+
return drafts.sort((a, b) => b.data.timestamp - a.data.timestamp);
41+
},
42+
43+
cleanup: function() {
44+
var drafts = this.getAllDrafts();
45+
if (drafts.length > this.maxDrafts) {
46+
drafts.slice(this.maxDrafts).forEach(function(draft) {
47+
sessionStorage.removeItem(draft.key);
48+
});
49+
}
50+
}
51+
};
52+
}
53+
54+
function saveAdvancedDraft() {
55+
try {
56+
var draftData = {};
57+
g_form.serialize(draftData);
58+
59+
// Add metadata
60+
var draftKey = window.draftManager.draftPrefix + new Date().getTime();
61+
var draftInfo = {
62+
timestamp: new Date().getTime(),
63+
data: draftData,
64+
user: g_user.userName,
65+
catalog_item: g_form.getTableName(),
66+
fields_modified: g_form.getModifiedFields()
67+
};
68+
69+
sessionStorage.setItem(draftKey, JSON.stringify(draftInfo));
70+
window.draftManager.cleanup();
71+
72+
// Show success message with draft count
73+
var remainingDrafts = window.draftManager.getAllDrafts().length;
74+
g_form.addInfoMessage('Draft saved. You have ' + remainingDrafts + ' saved draft(s).');
75+
76+
} catch (e) {
77+
console.error('Error saving draft: ' + e);
78+
g_form.addErrorMessage('Failed to save draft: ' + e.message);
79+
}
80+
}
81+
82+
function restoreLastDraft() {
83+
try {
84+
var drafts = window.draftManager.getAllDrafts();
85+
86+
if (drafts.length > 0) {
87+
// If multiple drafts exist, show selection dialog
88+
if (drafts.length > 1) {
89+
showDraftSelectionDialog(drafts);
90+
} else {
91+
promptToRestoreDraft(drafts[0].data);
92+
}
93+
}
94+
} catch (e) {
95+
console.error('Error restoring draft: ' + e);
96+
g_form.addErrorMessage('Failed to restore draft: ' + e.message);
97+
}
98+
}
99+
100+
function showDraftSelectionDialog(drafts) {
101+
var dialog = new GlideModal('select_draft_dialog');
102+
dialog.setTitle('Available Drafts');
103+
104+
var html = '<div class="draft-list">';
105+
drafts.forEach(function(draft, index) {
106+
var date = new Date(draft.data.timestamp).toLocaleString();
107+
html += '<div class="draft-item" onclick="selectDraft(' + index + ')">';
108+
html += '<strong>Draft ' + (index + 1) + '</strong> - ' + date;
109+
html += '<br>Modified fields: ' + draft.data.fields_modified.join(', ');
110+
html += '</div>';
111+
});
112+
html += '</div>';
113+
114+
dialog.renderWithContent(html);
115+
}
116+
117+
function promptToRestoreDraft(draftInfo) {
118+
var timestamp = new Date(draftInfo.timestamp);
119+
if (confirm('A draft from ' + timestamp.toLocaleString() + ' was found. Would you like to restore it?')) {
120+
Object.keys(draftInfo.data).forEach(function(field) {
121+
g_form.setValue(field, draftInfo.data[field]);
122+
});
123+
g_form.addInfoMessage('Draft restored from ' + timestamp.toLocaleString());
124+
}
125+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Basic Auto-save Draft Implementation
3+
* This version provides core functionality for auto-saving catalog item form data
4+
*/
5+
6+
function onLoad() {
7+
var autosaveInterval = 60000; // 1 minute
8+
9+
// Try to restore previous draft
10+
restoreLastDraft();
11+
12+
// Set up auto-save interval
13+
setInterval(function() {
14+
if (g_form.isModified()) {
15+
saveDraft();
16+
}
17+
}, autosaveInterval);
18+
}
19+
20+
function saveDraft() {
21+
try {
22+
var draftData = {};
23+
g_form.serialize(draftData);
24+
25+
var draftKey = 'catalogDraft_' + g_form.getUniqueValue();
26+
sessionStorage.setItem(draftKey, JSON.stringify({
27+
timestamp: new Date().getTime(),
28+
data: draftData
29+
}));
30+
31+
g_form.addInfoMessage('Draft saved automatically');
32+
} catch (e) {
33+
console.error('Error saving draft: ' + e);
34+
}
35+
}
36+
37+
function restoreLastDraft() {
38+
try {
39+
var draftKey = 'catalogDraft_' + g_form.getUniqueValue();
40+
var savedDraft = sessionStorage.getItem(draftKey);
41+
42+
if (savedDraft) {
43+
var draftData = JSON.parse(savedDraft);
44+
var timestamp = new Date(draftData.timestamp);
45+
46+
if (confirm('A draft from ' + timestamp.toLocaleString() + ' was found. Would you like to restore it?')) {
47+
Object.keys(draftData.data).forEach(function(field) {
48+
g_form.setValue(field, draftData.data[field]);
49+
});
50+
g_form.addInfoMessage('Draft restored from ' + timestamp.toLocaleString());
51+
} else {
52+
sessionStorage.removeItem(draftKey);
53+
}
54+
}
55+
} catch (e) {
56+
console.error('Error restoring draft: ' + e);
57+
}
58+
}

0 commit comments

Comments
 (0)