sf-metadata-selector 1.0.0
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/README.md +268 -0
- package/index.js +673 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Salesforce Metadata Selector CLI
|
|
2
|
+
|
|
3
|
+
> A beautiful, interactive Node.js CLI tool that helps you browse, select, and generate `package.xml` files for Salesforce metadata retrieval.
|
|
4
|
+
|
|
5
|
+
It connects to your Salesforce org via the Salesforce CLI (`sf`), lists all available metadata components, and provides a dark-themed web interface (using Tailwind CSS + Monaco Editor) where you can:
|
|
6
|
+
|
|
7
|
+
- 🔍 **Search and filter** metadata types and components in real-time
|
|
8
|
+
- ✅ **Select/deselect** individual components or entire types
|
|
9
|
+
- 💾 **Save and load** selections as JSON for reuse
|
|
10
|
+
- 🎯 **Use wildcards** (`*`) for entire metadata types
|
|
11
|
+
- 🎨 **Customize API version** dynamically
|
|
12
|
+
- 📊 **Track selection counts** in real-time
|
|
13
|
+
- ⌨️ **Keyboard shortcuts** for common actions
|
|
14
|
+
- 🚫 **Filter namespaced** components (exclude by default, include with flag)
|
|
15
|
+
- 📥 **Download** the final `package.xml` with one click
|
|
16
|
+
|
|
17
|
+
## Screenshot
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
## ✨ New Features
|
|
21
|
+
|
|
22
|
+
### 🔍 Search & Filter
|
|
23
|
+
- **Real-time search** across all metadata types and component names
|
|
24
|
+
- **Highlight matching** components for easy visibility
|
|
25
|
+
- **Auto-hide** non-matching items to focus your selection
|
|
26
|
+
|
|
27
|
+
### 💾 Save & Load Selections
|
|
28
|
+
- **Export selections** to JSON file for backup or sharing
|
|
29
|
+
- **Import selections** to quickly restore previous work
|
|
30
|
+
- Saves API version and wildcard preferences
|
|
31
|
+
|
|
32
|
+
### 🎯 Wildcard Support
|
|
33
|
+
- **Toggle wildcard mode** to use `*` for entire metadata types
|
|
34
|
+
- Automatically uses `*` when all components of a type are selected
|
|
35
|
+
- Reduces package.xml size for large deployments
|
|
36
|
+
|
|
37
|
+
### 📊 Selection Counter
|
|
38
|
+
- **Real-time counter** showing selected vs. total components
|
|
39
|
+
- Helps track your selection progress
|
|
40
|
+
- Updates instantly as you select/deselect
|
|
41
|
+
|
|
42
|
+
### ⌨️ Keyboard Shortcuts
|
|
43
|
+
- `Ctrl+S` / `Cmd+S` - Download package.xml
|
|
44
|
+
- Easy navigation with keyboard accessibility
|
|
45
|
+
|
|
46
|
+
### 🎨 Custom API Version
|
|
47
|
+
- **Editable API version** field (defaults to 60.0 or your specified version)
|
|
48
|
+
- Updates package.xml in real-time
|
|
49
|
+
- Supports all Salesforce API versions
|
|
50
|
+
|
|
51
|
+
### 📈 Progress Indicator
|
|
52
|
+
- Shows percentage progress while fetching metadata
|
|
53
|
+
- Better feedback during long-running operations
|
|
54
|
+
|
|
55
|
+
## 📋 Features
|
|
56
|
+
|
|
57
|
+
- Lists all metadata types using `sf org list metadata-types`
|
|
58
|
+
- Fetches components per type with `sf org list metadata`
|
|
59
|
+
- **Dark theme** UI with Monaco Editor for real-time `package.xml` preview
|
|
60
|
+
- **Per-type** Select All / Deselect All buttons
|
|
61
|
+
- Global Select All / Deselect All
|
|
62
|
+
- **Namespaced components filtering** (exclude by default, include with `--include-namespaced`)
|
|
63
|
+
- Prevents `ENOBUFS` errors with increased buffer sizes
|
|
64
|
+
- Generates a standalone HTML file
|
|
65
|
+
|
|
66
|
+
## 🔧 Requirements
|
|
67
|
+
|
|
68
|
+
- **Node.js** ≥ 16
|
|
69
|
+
- **[Salesforce CLI (`sf`)](https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_install_cli.htm)** installed and authenticated
|
|
70
|
+
- **npm** to install dependencies
|
|
71
|
+
|
|
72
|
+
## 📦 Installation
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm install -g sf-extract-pkg
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 🚀 Usage
|
|
80
|
+
|
|
81
|
+
### Basic Usage
|
|
82
|
+
```bash
|
|
83
|
+
# Excludes namespaced components by default
|
|
84
|
+
./sf-metadata-ui.js -o myOrgAlias
|
|
85
|
+
|
|
86
|
+
# Include namespaced components (managed package items, etc.)
|
|
87
|
+
./sf-metadata-ui.js -o myOrgAlias --include-namespaced
|
|
88
|
+
|
|
89
|
+
# Specify API version
|
|
90
|
+
./sf-metadata-ui.js -o myOrgAlias --api-version 65.0
|
|
91
|
+
|
|
92
|
+
# Custom output filename
|
|
93
|
+
./sf-metadata-ui.js -o myOrgAlias --output my-selector.html
|
|
94
|
+
|
|
95
|
+
# Using username instead of alias
|
|
96
|
+
./sf-metadata-ui.js -o user@example.com.mySandbox
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Command Options
|
|
100
|
+
```
|
|
101
|
+
Options:
|
|
102
|
+
-o, --org <alias|username> Salesforce org alias or username (required)
|
|
103
|
+
--include-namespaced Include components with namespace prefix
|
|
104
|
+
--api-version <version> API version for package.xml (default: 60.0)
|
|
105
|
+
--output <filename> Output HTML filename (default: sf-metadata-selector.html)
|
|
106
|
+
-h, --help Display help for command
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 📖 How It Works
|
|
110
|
+
|
|
111
|
+
1. **Connects** to your org using `sf org display`
|
|
112
|
+
2. **Discovers** metadata types with `sf org list metadata-types`
|
|
113
|
+
3. **Lists** components for each type with `sf org list metadata` (with progress indicator)
|
|
114
|
+
4. **Filters** out namespaced components unless `--include-namespaced` is used
|
|
115
|
+
5. **Generates** a rich HTML interface:
|
|
116
|
+
- **Left panel**: searchable metadata types + checkboxes + controls
|
|
117
|
+
- **Right panel**: Monaco Editor showing real-time `package.xml`
|
|
118
|
+
6. **Download** button creates `package.xml` ready for deployment
|
|
119
|
+
|
|
120
|
+
## 🎯 Usage Tips
|
|
121
|
+
|
|
122
|
+
### Searching
|
|
123
|
+
- Type in the search box to filter by metadata type or component name
|
|
124
|
+
- Matching items are highlighted
|
|
125
|
+
- Non-matching items are hidden
|
|
126
|
+
|
|
127
|
+
### Saving Selections
|
|
128
|
+
1. Select your desired components
|
|
129
|
+
2. Click **💾 Save** button
|
|
130
|
+
3. Save the JSON file to your computer
|
|
131
|
+
4. Later, click **📂 Load** to restore selections
|
|
132
|
+
|
|
133
|
+
### Using Wildcards
|
|
134
|
+
1. Select all components you want for a metadata type
|
|
135
|
+
2. Enable **"Use wildcards (*) for all"** checkbox
|
|
136
|
+
3. Types with all components selected will use `*` instead of listing each member
|
|
137
|
+
4. Great for CustomObject, ApexClass, etc.
|
|
138
|
+
|
|
139
|
+
### Keyboard Navigation
|
|
140
|
+
- Use `Tab` to navigate between controls
|
|
141
|
+
- `Space` to toggle checkboxes
|
|
142
|
+
- `Ctrl+S` (or `Cmd+S` on Mac) to download package.xml
|
|
143
|
+
|
|
144
|
+
## 📄 Example Generated package.xml
|
|
145
|
+
|
|
146
|
+
### Without Wildcards
|
|
147
|
+
```xml
|
|
148
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
149
|
+
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
150
|
+
<types>
|
|
151
|
+
<members>Account</members>
|
|
152
|
+
<members>Contact</members>
|
|
153
|
+
<members>CustomObject__c</members>
|
|
154
|
+
<name>CustomObject</name>
|
|
155
|
+
</types>
|
|
156
|
+
<types>
|
|
157
|
+
<members>MyController</members>
|
|
158
|
+
<members>MyUtility</members>
|
|
159
|
+
<name>ApexClass</name>
|
|
160
|
+
</types>
|
|
161
|
+
<version>64.0</version>
|
|
162
|
+
</Package>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### With Wildcards (all components selected)
|
|
166
|
+
```xml
|
|
167
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
168
|
+
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
169
|
+
<types>
|
|
170
|
+
<members>*</members>
|
|
171
|
+
<name>CustomObject</name>
|
|
172
|
+
</types>
|
|
173
|
+
<types>
|
|
174
|
+
<members>*</members>
|
|
175
|
+
<name>ApexClass</name>
|
|
176
|
+
</types>
|
|
177
|
+
<version>60.0</version>
|
|
178
|
+
</Package>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## 🛠️ Troubleshooting
|
|
182
|
+
|
|
183
|
+
### ENOBUFS Error
|
|
184
|
+
Already mitigated with larger `maxBuffer` (10–20 MB). If still occurring on very large orgs:
|
|
185
|
+
- Use `--include-namespaced false` to reduce component count
|
|
186
|
+
- Split retrieval into multiple package.xml files
|
|
187
|
+
- Increase buffer in the script if needed
|
|
188
|
+
|
|
189
|
+
### No Components Shown
|
|
190
|
+
- Ensure the org alias/username is authenticated: `sf org list`
|
|
191
|
+
- Check that you have permissions to view metadata
|
|
192
|
+
- Try running with `--include-namespaced` to see if components are namespaced
|
|
193
|
+
|
|
194
|
+
### package.xml Not Updating
|
|
195
|
+
- Refresh the HTML page
|
|
196
|
+
- Check browser console (F12) for JavaScript errors
|
|
197
|
+
- Ensure Monaco Editor loaded correctly
|
|
198
|
+
|
|
199
|
+
### Command Not Found
|
|
200
|
+
- Make sure `sf` CLI is installed and in your PATH
|
|
201
|
+
- Run `sf --version` to verify installation
|
|
202
|
+
- See [Salesforce CLI Setup Guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_install_cli.htm)
|
|
203
|
+
|
|
204
|
+
### Search Not Working
|
|
205
|
+
- Clear the search box to see all components
|
|
206
|
+
- Search is case-insensitive
|
|
207
|
+
- Try searching for partial names
|
|
208
|
+
|
|
209
|
+
### Load Selection Failed
|
|
210
|
+
- Ensure the JSON file is from this tool's Save feature
|
|
211
|
+
- Check that the file isn't corrupted
|
|
212
|
+
- Verify JSON syntax if manually edited
|
|
213
|
+
|
|
214
|
+
## 🎨 UI Features
|
|
215
|
+
|
|
216
|
+
### Left Panel
|
|
217
|
+
- **Search box** - Real-time filtering
|
|
218
|
+
- **Selection counter** - Shows X / Total selected
|
|
219
|
+
- **Namespace indicator** - Shows if namespaced components are included/excluded
|
|
220
|
+
- **Action buttons** - Select All, Deselect All, Save, Load
|
|
221
|
+
- **Collapsible sections** - Click to expand/collapse metadata types
|
|
222
|
+
- **Per-type controls** - Select/Deselect All for each type
|
|
223
|
+
- **Highlight on search** - Matching items highlighted in blue
|
|
224
|
+
|
|
225
|
+
### Right Panel
|
|
226
|
+
- **API Version input** - Change version on the fly
|
|
227
|
+
- **Wildcard toggle** - Enable/disable wildcard mode
|
|
228
|
+
- **Monaco Editor** - Syntax-highlighted XML editor
|
|
229
|
+
- **Download button** - One-click package.xml download
|
|
230
|
+
- **Keyboard hint** - Shows Ctrl+S shortcut
|
|
231
|
+
|
|
232
|
+
## 🔮 Future Enhancements
|
|
233
|
+
|
|
234
|
+
Potential features for future versions:
|
|
235
|
+
- [ ] **Metadata comparison** between orgs
|
|
236
|
+
- [ ] **Dependency analysis** to show related components
|
|
237
|
+
- [ ] **Multiple package.xml** generation (e.g., split by feature)
|
|
238
|
+
- [ ] **Recent selections** history
|
|
239
|
+
- [ ] **Preset filters** (e.g., "All Apex", "All Objects")
|
|
240
|
+
- [ ] **Diff view** against existing package.xml
|
|
241
|
+
- [ ] **Cloud storage** integration for selections
|
|
242
|
+
- [ ] **Batch operations** across multiple orgs
|
|
243
|
+
- [ ] **Component descriptions** on hover
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
### Testing
|
|
247
|
+
```bash
|
|
248
|
+
# Test with your org
|
|
249
|
+
./sf-metadata-ui.js -o yourOrgAlias
|
|
250
|
+
|
|
251
|
+
# Test with different options
|
|
252
|
+
./sf-metadata-ui.js -o yourOrg --include-namespaced --api-version 59.0
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
## 📄 License
|
|
257
|
+
|
|
258
|
+
MIT License - see LICENSE file for details
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
**Built with ❤️ for Salesforce developers**
|
|
263
|
+
|
|
264
|
+
## 🔗 Useful Links
|
|
265
|
+
|
|
266
|
+
- [Salesforce CLI Documentation](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference.htm)
|
|
267
|
+
- [Metadata API Developer Guide](https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/)
|
|
268
|
+
- [Package.xml Reference](https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_deploy.htm)
|
package/index.js
ADDED
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { openResource } = require('open-resource');
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.name('sf-metadata-ui')
|
|
11
|
+
.description('Generate an interactive HTML page to select metadata and build package.xml')
|
|
12
|
+
.requiredOption('-o, --org <alias|username>', 'Salesforce org alias or username')
|
|
13
|
+
.option('--include-namespaced', 'Include components with namespace prefix (default: exclude them)')
|
|
14
|
+
.option('--api-version <version>', 'API version for package.xml (default: 64.0)', '64.0')
|
|
15
|
+
.option('--output <filename>', 'Output HTML filename (default: sf-metadata-selector.html)', 'sf-metadata-selector.html')
|
|
16
|
+
.parse();
|
|
17
|
+
|
|
18
|
+
const options = program.opts();
|
|
19
|
+
const org = options.org;
|
|
20
|
+
const includeNamespaced = !!options.includeNamespaced;
|
|
21
|
+
const apiVersion = options.apiVersion;
|
|
22
|
+
const outputFile = options.output;
|
|
23
|
+
|
|
24
|
+
async function main() {
|
|
25
|
+
try {
|
|
26
|
+
console.log(`Fetching org info for: ${org} ...`);
|
|
27
|
+
|
|
28
|
+
const orgInfoRaw = execSync(
|
|
29
|
+
`sf org display --target-org "${org}" --json`,
|
|
30
|
+
{ encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 }
|
|
31
|
+
);
|
|
32
|
+
const orgInfo = JSON.parse(orgInfoRaw).result;
|
|
33
|
+
|
|
34
|
+
console.log(`Connected to: ${orgInfo.alias || orgInfo.username} (${orgInfo.instanceUrl})`);
|
|
35
|
+
|
|
36
|
+
console.log('Listing available metadata types...');
|
|
37
|
+
const typesCmd = `sf org list metadata-types --target-org "${org}" --json`;
|
|
38
|
+
const typesRaw = execSync(typesCmd, {
|
|
39
|
+
encoding: 'utf-8',
|
|
40
|
+
maxBuffer: 10 * 1024 * 1024
|
|
41
|
+
});
|
|
42
|
+
const typesData = JSON.parse(typesRaw);
|
|
43
|
+
|
|
44
|
+
let metadataTypes = [];
|
|
45
|
+
|
|
46
|
+
if (typesData.result && Array.isArray(typesData.result)) {
|
|
47
|
+
metadataTypes = typesData.result.map(item => item.metadataName || item.name || item.xmlName);
|
|
48
|
+
} else if (Array.isArray(typesData)) {
|
|
49
|
+
metadataTypes = typesData.map(item => item.metadataName || item.name || item.xmlName);
|
|
50
|
+
} else if (typesData.metadataObjects) {
|
|
51
|
+
metadataTypes = typesData.metadataObjects.map(obj => obj.xmlName);
|
|
52
|
+
} else {
|
|
53
|
+
console.warn('Unexpected format for metadata types. Using fallback list.');
|
|
54
|
+
metadataTypes = getFallbackMetadataTypes();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
metadataTypes = [...new Set(metadataTypes.filter(t => t && typeof t === 'string'))];
|
|
58
|
+
|
|
59
|
+
console.log(`Found ${metadataTypes.length} metadata types`);
|
|
60
|
+
|
|
61
|
+
const components = [];
|
|
62
|
+
const totalTypes = metadataTypes.length;
|
|
63
|
+
let processedTypes = 0;
|
|
64
|
+
|
|
65
|
+
for (const type of metadataTypes.sort()) {
|
|
66
|
+
try {
|
|
67
|
+
processedTypes++;
|
|
68
|
+
const progress = Math.round((processedTypes / totalTypes) * 100);
|
|
69
|
+
console.log(`[${progress}%] Listing components for ${type} ...`);
|
|
70
|
+
|
|
71
|
+
const listCmd = `sf org list metadata --metadata-type "${type}" --target-org "${org}" --json`;
|
|
72
|
+
const listRaw = execSync(listCmd, {
|
|
73
|
+
encoding: 'utf-8',
|
|
74
|
+
maxBuffer: 20 * 1024 * 1024
|
|
75
|
+
});
|
|
76
|
+
const listData = JSON.parse(listRaw);
|
|
77
|
+
|
|
78
|
+
let items = [];
|
|
79
|
+
|
|
80
|
+
if (listData.result && Array.isArray(listData.result)) {
|
|
81
|
+
items = listData.result;
|
|
82
|
+
} else if (Array.isArray(listData)) {
|
|
83
|
+
items = listData;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
items.forEach(item => {
|
|
87
|
+
const fullName = item.fullName || item.fullname || item.name;
|
|
88
|
+
if (fullName && (includeNamespaced || !hasNamespace(fullName))) {
|
|
89
|
+
components.push({ type, fullName });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const msg = err.message || err.toString();
|
|
94
|
+
console.warn(`Skipping ${type} — ${msg.split('\n')[0] || 'error'}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
components.sort((a, b) =>
|
|
99
|
+
a.type.localeCompare(b.type) || a.fullName.localeCompare(b.fullName)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
console.log(`Found ${components.length} metadata components (namespaced: ${includeNamespaced ? 'included' : 'excluded'})`);
|
|
103
|
+
|
|
104
|
+
const html = generateHtml(components, includeNamespaced, apiVersion, orgInfo);
|
|
105
|
+
|
|
106
|
+
fs.writeFileSync(outputFile, html, 'utf-8');
|
|
107
|
+
|
|
108
|
+
console.log(`\nSuccess! Generated file:`);
|
|
109
|
+
console.log(`→ ${path.resolve(outputFile)}`);
|
|
110
|
+
console.log('Open it in your browser to select components and build package.xml');
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
openResource(outputFile);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.log('Could not auto-open file. Please open it manually.');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error('Error occurred:');
|
|
120
|
+
console.error(err.message);
|
|
121
|
+
if (err.stderr) console.error('stderr:', err.stderr);
|
|
122
|
+
if (err.stdout) console.error('stdout:', err.stdout);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function hasNamespace(name) {
|
|
128
|
+
return /([a-zA-Z0-9]+__)[\w]+/.test(name);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getFallbackMetadataTypes() {
|
|
132
|
+
return [
|
|
133
|
+
'ApexClass', 'ApexComponent', 'ApexPage', 'ApexTrigger',
|
|
134
|
+
'AuraDefinitionBundle', 'CustomObject', 'CustomField',
|
|
135
|
+
'CustomLabel', 'CustomMetadata', 'CustomTab', 'FlexiPage',
|
|
136
|
+
'LightningComponentBundle', 'Profile', 'PermissionSet',
|
|
137
|
+
'StaticResource', 'Layout', 'RecordType', 'ValidationRule',
|
|
138
|
+
'Flow', 'FlowDefinition', 'EmailTemplate', 'ReportType',
|
|
139
|
+
'Report', 'Dashboard', 'ListView', 'CustomApplication',
|
|
140
|
+
'CompactLayout', 'QuickAction', 'Territory2Type'
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function generateHtml(components, includeNamespaced, apiVersion, orgInfo) {
|
|
145
|
+
const groups = {};
|
|
146
|
+
components.forEach(c => {
|
|
147
|
+
if (!groups[c.type]) groups[c.type] = [];
|
|
148
|
+
groups[c.type].push(c.fullName);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const orgName = orgInfo.alias || orgInfo.username;
|
|
152
|
+
|
|
153
|
+
return `<!DOCTYPE html>
|
|
154
|
+
<html lang="en" class="dark">
|
|
155
|
+
<head>
|
|
156
|
+
<meta charset="UTF-8" />
|
|
157
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
158
|
+
<title>Salesforce Metadata Selector - ${orgName}</title>
|
|
159
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
160
|
+
<link rel="icon" type="image/x-icon"
|
|
161
|
+
href="https://mohan-chinnappan-n5.github.io/dfv/img/mc_favIcon.ico" />
|
|
162
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.44.0/min/vs/loader.min.js"></script>
|
|
163
|
+
<style>
|
|
164
|
+
body { background: #111827; color: #e5e7eb; font-family: ui-sans-serif, system-ui, sans-serif; }
|
|
165
|
+
details summary::-webkit-details-marker { display: none; }
|
|
166
|
+
details summary::before { content: '▶ '; transition: transform 0.2s; display: inline-block; }
|
|
167
|
+
details[open] summary::before { content: '▼ '; transform: rotate(0deg); }
|
|
168
|
+
.highlight {
|
|
169
|
+
background-color: rgba(59, 130, 246, 0.25);
|
|
170
|
+
animation: highlightPulse 0.3s ease-in-out;
|
|
171
|
+
}
|
|
172
|
+
@keyframes highlightPulse {
|
|
173
|
+
0%, 100% { background-color: rgba(59, 130, 246, 0.25); }
|
|
174
|
+
50% { background-color: rgba(59, 130, 246, 0.4); }
|
|
175
|
+
}
|
|
176
|
+
#search-box:focus {
|
|
177
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
|
|
178
|
+
}
|
|
179
|
+
button {
|
|
180
|
+
transition: all 0.2s ease-in-out;
|
|
181
|
+
}
|
|
182
|
+
button:active {
|
|
183
|
+
transform: scale(0.98);
|
|
184
|
+
}
|
|
185
|
+
.metadata-type-summary {
|
|
186
|
+
transition: background-color 0.2s ease-in-out;
|
|
187
|
+
}
|
|
188
|
+
</style>
|
|
189
|
+
</head>
|
|
190
|
+
<body class="min-h-screen flex flex-col">
|
|
191
|
+
<!-- Enhanced Header -->
|
|
192
|
+
<header class="bg-gradient-to-r from-blue-900 to-blue-800 border-b border-blue-700 px-6 py-4 shadow-lg">
|
|
193
|
+
<div class="max-w-screen-2xl mx-auto">
|
|
194
|
+
<div class="flex items-center justify-between">
|
|
195
|
+
<!-- Title Section -->
|
|
196
|
+
<div class="flex items-center space-x-4">
|
|
197
|
+
<div class="flex items-center justify-center w-10 h-10 bg-blue-600 rounded-lg shadow-md">
|
|
198
|
+
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
199
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/>
|
|
200
|
+
</svg>
|
|
201
|
+
</div>
|
|
202
|
+
<div>
|
|
203
|
+
<h1 class="text-xl font-bold text-white">Salesforce Metadata Selector</h1>
|
|
204
|
+
<p class="text-xs text-blue-200 mt-0.5">Generate package.xml for deployment</p>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<!-- Org Info Badge -->
|
|
209
|
+
<div class="flex items-center space-x-3">
|
|
210
|
+
<div class="text-right hidden sm:block">
|
|
211
|
+
<p class="text-xs text-blue-200">Connected Org</p>
|
|
212
|
+
<p style='display:none;' class="text-sm font-semibold text-white">${orgName}</p>
|
|
213
|
+
</div>
|
|
214
|
+
<div class="bg-blue-700 rounded-full px-4 py-2 border border-blue-600">
|
|
215
|
+
<span class="text-xs font-mono text-blue-100">${orgName}</span>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</header>
|
|
221
|
+
|
|
222
|
+
<div class="flex flex-1 gap-6 overflow-hidden p-6">
|
|
223
|
+
<!-- Left Panel: Component Selection -->
|
|
224
|
+
<div class="w-2/5 bg-gray-900 rounded-xl p-5 overflow-auto border border-gray-700 flex flex-col">
|
|
225
|
+
<!-- Search Box -->
|
|
226
|
+
<div class="mb-4 relative">
|
|
227
|
+
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
228
|
+
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
229
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
230
|
+
</svg>
|
|
231
|
+
</div>
|
|
232
|
+
<input
|
|
233
|
+
type="text"
|
|
234
|
+
id="search-box"
|
|
235
|
+
placeholder="Search metadata types or components..."
|
|
236
|
+
class="w-full pl-10 pr-4 py-2.5 bg-gray-800 border border-gray-600 rounded-lg text-sm focus:outline-none focus:border-blue-500 transition-all"
|
|
237
|
+
/>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<!-- Info Bar -->
|
|
241
|
+
<div class="mb-4 bg-gray-800 rounded-lg p-3 border border-gray-700">
|
|
242
|
+
<div class="flex justify-between items-center text-sm">
|
|
243
|
+
<div class="flex items-center space-x-2">
|
|
244
|
+
<svg class="w-4 h-4 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
245
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
246
|
+
</svg>
|
|
247
|
+
<span class="text-gray-400">Selection:</span>
|
|
248
|
+
<span class="font-bold text-blue-400" id="selected-count">0</span>
|
|
249
|
+
<span class="text-gray-500">/</span>
|
|
250
|
+
<span class="text-gray-300" id="total-count">${components.length}</span>
|
|
251
|
+
</div>
|
|
252
|
+
<div class="flex items-center space-x-2">
|
|
253
|
+
<svg class="w-4 h-4 ${includeNamespaced ? 'text-green-400' : 'text-orange-400'}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
254
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
|
|
255
|
+
</svg>
|
|
256
|
+
<span class="text-gray-400">Namespaced:</span>
|
|
257
|
+
<span class="font-medium ${includeNamespaced ? 'text-green-400' : 'text-orange-400'}">
|
|
258
|
+
${includeNamespaced ? 'Included' : 'Excluded'}
|
|
259
|
+
</span>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<!-- Action Buttons -->
|
|
265
|
+
<div class="mb-5 pb-4 border-b border-gray-700">
|
|
266
|
+
<div class="flex flex-col sm:flex-row justify-between items-stretch sm:items-center gap-3">
|
|
267
|
+
<!-- Selection Controls -->
|
|
268
|
+
<div class="flex items-center gap-2">
|
|
269
|
+
<button id="select-all" class="flex-1 sm:flex-none bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2">
|
|
270
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
271
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
272
|
+
</svg>
|
|
273
|
+
Select All
|
|
274
|
+
</button>
|
|
275
|
+
<button id="deselect-all" class="flex-1 sm:flex-none bg-red-600 hover:bg-red-700 px-4 py-2 rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2">
|
|
276
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
277
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
278
|
+
</svg>
|
|
279
|
+
Deselect All
|
|
280
|
+
</button>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<!-- File Operations -->
|
|
284
|
+
<div class="flex items-center gap-2">
|
|
285
|
+
<button id="save-selection" class="flex-1 sm:flex-none bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2" title="Save selection to JSON">
|
|
286
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
287
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"/>
|
|
288
|
+
</svg>
|
|
289
|
+
Save
|
|
290
|
+
</button>
|
|
291
|
+
<button id="load-selection" class="flex-1 sm:flex-none bg-indigo-600 hover:bg-indigo-700 px-4 py-2 rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2" title="Load selection from JSON">
|
|
292
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
293
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
|
|
294
|
+
</svg>
|
|
295
|
+
Load
|
|
296
|
+
</button>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<!-- Components List -->
|
|
302
|
+
<div id="components" class="flex-1 overflow-auto"></div>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<!-- Right Panel: XML Editor -->
|
|
306
|
+
<div class="w-3/5 flex flex-col bg-gray-900 rounded-xl border border-gray-700 overflow-hidden shadow-xl">
|
|
307
|
+
<!-- API Version & Controls Header -->
|
|
308
|
+
<div class="bg-gray-800 border-b border-gray-700 px-5 py-3">
|
|
309
|
+
<div class="flex flex-col sm:flex-row justify-between items-stretch sm:items-center gap-3">
|
|
310
|
+
<!-- API Version -->
|
|
311
|
+
<div class="flex items-center space-x-3">
|
|
312
|
+
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
313
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
|
|
314
|
+
</svg>
|
|
315
|
+
<label class="text-sm font-medium text-gray-300">API Version:</label>
|
|
316
|
+
<input
|
|
317
|
+
type="text"
|
|
318
|
+
id="api-version"
|
|
319
|
+
value="${apiVersion}"
|
|
320
|
+
class="w-24 px-3 py-1.5 bg-gray-900 border border-gray-600 rounded-lg text-sm font-mono focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-all"
|
|
321
|
+
placeholder="64.0"
|
|
322
|
+
/>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
<!-- Wildcard Toggle -->
|
|
326
|
+
<div class="flex items-center space-x-2 bg-gray-900 rounded-lg px-4 py-2 border border-gray-700">
|
|
327
|
+
<input type="checkbox" id="use-wildcards" class="w-4 h-4 accent-blue-500 rounded" />
|
|
328
|
+
<label for="use-wildcards" class="text-sm text-gray-300 cursor-pointer select-none flex items-center gap-2">
|
|
329
|
+
<svg class="w-4 h-4 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
330
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>
|
|
331
|
+
</svg>
|
|
332
|
+
Use wildcards (*) for all
|
|
333
|
+
</label>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
<!-- Monaco Editor -->
|
|
339
|
+
<div id="editor" class="flex-1"></div>
|
|
340
|
+
|
|
341
|
+
<!-- Download Section -->
|
|
342
|
+
<div class="bg-gray-800 border-t border-gray-700 px-5 py-4">
|
|
343
|
+
<div class="flex justify-between items-center">
|
|
344
|
+
<div class="flex items-center space-x-4 text-sm text-gray-400">
|
|
345
|
+
<div class="flex items-center gap-2">
|
|
346
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
347
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
|
348
|
+
</svg>
|
|
349
|
+
<span>Keyboard shortcut:</span>
|
|
350
|
+
<kbd class="px-2 py-1 bg-gray-900 rounded text-xs font-mono border border-gray-700">Ctrl+S</kbd>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
<button id="download" class="bg-green-600 hover:bg-green-700 px-8 py-2.5 rounded-lg font-medium transition-all hover:shadow-lg hover:shadow-green-600/50 flex items-center gap-2">
|
|
354
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
355
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
|
|
356
|
+
</svg>
|
|
357
|
+
Download package.xml
|
|
358
|
+
</button>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
<!-- Hidden file input for loading -->
|
|
365
|
+
<input type="file" id="file-input" accept=".json" style="display: none;" />
|
|
366
|
+
|
|
367
|
+
<script>
|
|
368
|
+
const groups = ${JSON.stringify(groups, null, 2)};
|
|
369
|
+
const container = document.getElementById('components');
|
|
370
|
+
const searchBox = document.getElementById('search-box');
|
|
371
|
+
const selectedCountEl = document.getElementById('selected-count');
|
|
372
|
+
const totalCountEl = document.getElementById('total-count');
|
|
373
|
+
const apiVersionInput = document.getElementById('api-version');
|
|
374
|
+
const useWildcardsCheckbox = document.getElementById('use-wildcards');
|
|
375
|
+
|
|
376
|
+
let allDetailsElements = [];
|
|
377
|
+
|
|
378
|
+
function hasNamespace(name) {
|
|
379
|
+
return /([a-zA-Z0-9]+__)\\w+/.test(name);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function updateCounts() {
|
|
383
|
+
const checked = document.querySelectorAll('#components input[type="checkbox"]:checked');
|
|
384
|
+
selectedCountEl.textContent = checked.length;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function updateXml() {
|
|
388
|
+
if (!editor) return;
|
|
389
|
+
|
|
390
|
+
const apiVersion = apiVersionInput.value || '64.0';
|
|
391
|
+
const useWildcards = useWildcardsCheckbox.checked;
|
|
392
|
+
const checked = document.querySelectorAll('#components input[type="checkbox"]:checked');
|
|
393
|
+
const typeMap = new Map();
|
|
394
|
+
|
|
395
|
+
checked.forEach(chk => {
|
|
396
|
+
const t = chk.dataset.type;
|
|
397
|
+
const m = chk.dataset.fullname;
|
|
398
|
+
if (t && m) {
|
|
399
|
+
if (!typeMap.has(t)) typeMap.set(t, []);
|
|
400
|
+
typeMap.get(t).push(m);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
let xml = '<?xml version="1.0" encoding="UTF-8"?>\\n';
|
|
405
|
+
xml += '<Package xmlns="http://soap.sforce.com/2006/04/metadata">\\n';
|
|
406
|
+
|
|
407
|
+
[...typeMap.keys()].sort().forEach(t => {
|
|
408
|
+
xml += ' <types>\\n';
|
|
409
|
+
|
|
410
|
+
if (useWildcards) {
|
|
411
|
+
// Check if all components of this type are selected
|
|
412
|
+
const totalForType = groups[t]?.length || 0;
|
|
413
|
+
const selectedForType = typeMap.get(t).length;
|
|
414
|
+
|
|
415
|
+
if (totalForType > 0 && selectedForType === totalForType) {
|
|
416
|
+
xml += ' <members>*</members>\\n';
|
|
417
|
+
} else {
|
|
418
|
+
const members = typeMap.get(t).sort();
|
|
419
|
+
members.forEach(mem => {
|
|
420
|
+
xml += \` <members>\${mem}</members>\\n\`;
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
const members = typeMap.get(t).sort();
|
|
425
|
+
members.forEach(mem => {
|
|
426
|
+
xml += \` <members>\${mem}</members>\\n\`;
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
xml += \` <name>\${t}</name>\\n </types>\\n\`;
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
xml += \` <version>\${apiVersion}</version>\\n</Package>\`;
|
|
434
|
+
|
|
435
|
+
editor.setValue(xml);
|
|
436
|
+
updateCounts();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Search/Filter functionality
|
|
440
|
+
searchBox.addEventListener('input', (e) => {
|
|
441
|
+
const query = e.target.value.toLowerCase();
|
|
442
|
+
|
|
443
|
+
allDetailsElements.forEach(({ details, type, content }) => {
|
|
444
|
+
const typeMatches = type.toLowerCase().includes(query);
|
|
445
|
+
const labels = content.querySelectorAll('label');
|
|
446
|
+
let hasVisibleMembers = false;
|
|
447
|
+
|
|
448
|
+
labels.forEach(label => {
|
|
449
|
+
const memberName = label.querySelector('span').textContent.toLowerCase();
|
|
450
|
+
const matches = memberName.includes(query);
|
|
451
|
+
|
|
452
|
+
label.style.display = (typeMatches || matches || query === '') ? 'flex' : 'none';
|
|
453
|
+
if (matches || typeMatches || query === '') hasVisibleMembers = true;
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
details.style.display = (hasVisibleMembers || query === '') ? 'block' : 'none';
|
|
457
|
+
|
|
458
|
+
// Highlight matches
|
|
459
|
+
if (query !== '') {
|
|
460
|
+
labels.forEach(label => {
|
|
461
|
+
const span = label.querySelector('span');
|
|
462
|
+
const text = span.textContent;
|
|
463
|
+
if (text.toLowerCase().includes(query)) {
|
|
464
|
+
label.classList.add('highlight');
|
|
465
|
+
} else {
|
|
466
|
+
label.classList.remove('highlight');
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
} else {
|
|
470
|
+
labels.forEach(label => label.classList.remove('highlight'));
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Save selection to JSON
|
|
476
|
+
document.getElementById('save-selection').addEventListener('click', () => {
|
|
477
|
+
const checked = document.querySelectorAll('#components input[type="checkbox"]:checked');
|
|
478
|
+
const selection = {
|
|
479
|
+
apiVersion: apiVersionInput.value,
|
|
480
|
+
useWildcards: useWildcardsCheckbox.checked,
|
|
481
|
+
selectedComponents: Array.from(checked).map(chk => ({
|
|
482
|
+
type: chk.dataset.type,
|
|
483
|
+
fullName: chk.dataset.fullname
|
|
484
|
+
}))
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const blob = new Blob([JSON.stringify(selection, null, 2)], { type: 'application/json' });
|
|
488
|
+
const url = URL.createObjectURL(blob);
|
|
489
|
+
const a = document.createElement('a');
|
|
490
|
+
a.href = url;
|
|
491
|
+
a.download = 'metadata-selection.json';
|
|
492
|
+
a.click();
|
|
493
|
+
URL.revokeObjectURL(url);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Load selection from JSON
|
|
497
|
+
document.getElementById('load-selection').addEventListener('click', () => {
|
|
498
|
+
document.getElementById('file-input').click();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
document.getElementById('file-input').addEventListener('change', (e) => {
|
|
502
|
+
const file = e.target.files[0];
|
|
503
|
+
if (!file) return;
|
|
504
|
+
|
|
505
|
+
const reader = new FileReader();
|
|
506
|
+
reader.onload = (event) => {
|
|
507
|
+
try {
|
|
508
|
+
const selection = JSON.parse(event.target.result);
|
|
509
|
+
|
|
510
|
+
// Deselect all first
|
|
511
|
+
document.querySelectorAll('#components input[type="checkbox"]').forEach(c => c.checked = false);
|
|
512
|
+
|
|
513
|
+
// Apply loaded selection
|
|
514
|
+
if (selection.apiVersion) apiVersionInput.value = selection.apiVersion;
|
|
515
|
+
if (typeof selection.useWildcards === 'boolean') useWildcardsCheckbox.checked = selection.useWildcards;
|
|
516
|
+
|
|
517
|
+
selection.selectedComponents.forEach(item => {
|
|
518
|
+
const checkbox = document.querySelector(
|
|
519
|
+
\`#components input[data-type="\${item.type}"][data-fullname="\${item.fullName}"]\`
|
|
520
|
+
);
|
|
521
|
+
if (checkbox) checkbox.checked = true;
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
updateXml();
|
|
525
|
+
alert('Selection loaded successfully!');
|
|
526
|
+
} catch (err) {
|
|
527
|
+
alert('Error loading selection file: ' + err.message);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
reader.readAsText(file);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// Render metadata types with per-type select/deselect
|
|
534
|
+
Object.keys(groups).sort().forEach(type => {
|
|
535
|
+
const members = groups[type];
|
|
536
|
+
if (members.length === 0) return;
|
|
537
|
+
|
|
538
|
+
const count = members.length;
|
|
539
|
+
|
|
540
|
+
const details = document.createElement('details');
|
|
541
|
+
details.className = 'mb-3';
|
|
542
|
+
|
|
543
|
+
const summary = document.createElement('summary');
|
|
544
|
+
summary.className = 'metadata-type-summary bg-gray-800 hover:bg-gray-700 px-4 py-3 rounded-lg cursor-pointer font-medium flex justify-between items-center group';
|
|
545
|
+
|
|
546
|
+
const left = document.createElement('span');
|
|
547
|
+
left.textContent = \`\${type} (\${count})\`;
|
|
548
|
+
|
|
549
|
+
const right = document.createElement('div');
|
|
550
|
+
right.className = 'space-x-2';
|
|
551
|
+
right.onclick = (e) => e.stopPropagation(); // Prevent details toggle
|
|
552
|
+
|
|
553
|
+
const selectAllBtn = document.createElement('button');
|
|
554
|
+
selectAllBtn.textContent = 'Select All';
|
|
555
|
+
selectAllBtn.className = 'text-xs bg-blue-600 hover:bg-blue-500 px-3 py-1 rounded-md transition-colors opacity-0 group-hover:opacity-100';
|
|
556
|
+
selectAllBtn.type = 'button';
|
|
557
|
+
|
|
558
|
+
const deselectAllBtn = document.createElement('button');
|
|
559
|
+
deselectAllBtn.textContent = 'Deselect All';
|
|
560
|
+
deselectAllBtn.className = 'text-xs bg-red-600 hover:bg-red-500 px-3 py-1 rounded-md transition-colors opacity-0 group-hover:opacity-100';
|
|
561
|
+
deselectAllBtn.type = 'button';
|
|
562
|
+
|
|
563
|
+
right.appendChild(selectAllBtn);
|
|
564
|
+
right.appendChild(deselectAllBtn);
|
|
565
|
+
|
|
566
|
+
summary.appendChild(left);
|
|
567
|
+
summary.appendChild(right);
|
|
568
|
+
|
|
569
|
+
const content = document.createElement('div');
|
|
570
|
+
content.className = 'pl-6 pt-2 max-h-80 overflow-auto';
|
|
571
|
+
|
|
572
|
+
members.sort().forEach(name => {
|
|
573
|
+
const label = document.createElement('label');
|
|
574
|
+
label.className = 'flex items-center py-1 hover:bg-gray-800 rounded px-2 cursor-pointer text-sm';
|
|
575
|
+
|
|
576
|
+
const checkbox = document.createElement('input');
|
|
577
|
+
checkbox.type = 'checkbox';
|
|
578
|
+
checkbox.className = 'mr-3 accent-blue-500';
|
|
579
|
+
checkbox.dataset.type = type;
|
|
580
|
+
checkbox.dataset.fullname = name;
|
|
581
|
+
|
|
582
|
+
const span = document.createElement('span');
|
|
583
|
+
span.className = 'font-mono ' + (hasNamespace(name) ? 'text-purple-400' : '');
|
|
584
|
+
span.textContent = name;
|
|
585
|
+
|
|
586
|
+
label.appendChild(checkbox);
|
|
587
|
+
label.appendChild(span);
|
|
588
|
+
content.appendChild(label);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
details.appendChild(summary);
|
|
592
|
+
details.appendChild(content);
|
|
593
|
+
container.appendChild(details);
|
|
594
|
+
|
|
595
|
+
// Store reference for search
|
|
596
|
+
allDetailsElements.push({ details, type, content });
|
|
597
|
+
|
|
598
|
+
// Per-type select / deselect
|
|
599
|
+
selectAllBtn.addEventListener('click', (e) => {
|
|
600
|
+
e.stopPropagation();
|
|
601
|
+
content.querySelectorAll('input[type="checkbox"]:not([style*="display: none"])').forEach(cb => cb.checked = true);
|
|
602
|
+
updateXml();
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
deselectAllBtn.addEventListener('click', (e) => {
|
|
606
|
+
e.stopPropagation();
|
|
607
|
+
content.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
|
|
608
|
+
updateXml();
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// Event delegation for checkbox changes
|
|
613
|
+
container.addEventListener('change', (e) => {
|
|
614
|
+
if (e.target.type === 'checkbox') {
|
|
615
|
+
updateXml();
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// API version and wildcard changes
|
|
620
|
+
apiVersionInput.addEventListener('input', updateXml);
|
|
621
|
+
useWildcardsCheckbox.addEventListener('change', updateXml);
|
|
622
|
+
|
|
623
|
+
// Monaco Editor
|
|
624
|
+
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.44.0/min/vs' }});
|
|
625
|
+
let editor;
|
|
626
|
+
|
|
627
|
+
require(['vs/editor/editor.main'], () => {
|
|
628
|
+
editor = monaco.editor.create(document.getElementById('editor'), {
|
|
629
|
+
value: '<?xml version="1.0" encoding="UTF-8"?>\\n<Package xmlns="http://soap.sforce.com/2006/04/metadata">\\n <!-- Select components → -->\\n <version>${apiVersion}</version>\\n</Package>',
|
|
630
|
+
language: 'xml',
|
|
631
|
+
theme: 'vs-dark',
|
|
632
|
+
automaticLayout: true,
|
|
633
|
+
minimap: { enabled: false },
|
|
634
|
+
fontSize: 13,
|
|
635
|
+
lineNumbers: 'on'
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// Keyboard shortcut for download (Ctrl+S)
|
|
639
|
+
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
|
|
640
|
+
document.getElementById('download').click();
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
updateXml();
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// Global Select All / Deselect All
|
|
647
|
+
document.getElementById('select-all').addEventListener('click', () => {
|
|
648
|
+
document.querySelectorAll('#components input[type="checkbox"]:not([style*="display: none"])').forEach(c => c.checked = true);
|
|
649
|
+
updateXml();
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
document.getElementById('deselect-all').addEventListener('click', () => {
|
|
653
|
+
document.querySelectorAll('#components input[type="checkbox"]').forEach(c => c.checked = false);
|
|
654
|
+
updateXml();
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// Download
|
|
658
|
+
document.getElementById('download').addEventListener('click', () => {
|
|
659
|
+
if (!editor) return;
|
|
660
|
+
const blob = new Blob([editor.getValue()], { type: 'application/xml' });
|
|
661
|
+
const url = URL.createObjectURL(blob);
|
|
662
|
+
const a = document.createElement('a');
|
|
663
|
+
a.href = url;
|
|
664
|
+
a.download = 'package.xml';
|
|
665
|
+
a.click();
|
|
666
|
+
URL.revokeObjectURL(url);
|
|
667
|
+
});
|
|
668
|
+
</script>
|
|
669
|
+
</body>
|
|
670
|
+
</html>`;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sf-metadata-selector",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A beautiful, interactive CLI tool for browsing, selecting, and generating package.xml files for Salesforce metadata retrieval with search, save/load, and wildcard support",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"salesforce",
|
|
7
|
+
"sfdx",
|
|
8
|
+
"sf-cli",
|
|
9
|
+
"metadata",
|
|
10
|
+
"package-xml",
|
|
11
|
+
"deployment",
|
|
12
|
+
"devops",
|
|
13
|
+
"cli",
|
|
14
|
+
"interactive",
|
|
15
|
+
"ui",
|
|
16
|
+
"developer-tools"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": {
|
|
20
|
+
"name": "Mohan Chinnappan"
|
|
21
|
+
},
|
|
22
|
+
"main": "index.js",
|
|
23
|
+
"bin": {
|
|
24
|
+
"sf-metadata-ui": "./index.js",
|
|
25
|
+
"sf-pkg-builder": "./index.js",
|
|
26
|
+
"sfmd": "./index.js"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"start": "node index.js",
|
|
30
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"commander": "^11.1.0",
|
|
34
|
+
"open-resource": "^2.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"eslint": "^8.57.0",
|
|
38
|
+
"prettier": "^3.2.5"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=16.0.0",
|
|
42
|
+
"npm": ">=8.0.0"
|
|
43
|
+
},
|
|
44
|
+
"os": [
|
|
45
|
+
"darwin",
|
|
46
|
+
"linux",
|
|
47
|
+
"win32"
|
|
48
|
+
],
|
|
49
|
+
"preferGlobal": true,
|
|
50
|
+
"files": [
|
|
51
|
+
"index.js",
|
|
52
|
+
"README.md",
|
|
53
|
+
"LICENSE"
|
|
54
|
+
],
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public",
|
|
57
|
+
"registry": "https://registry.npmjs.org/"
|
|
58
|
+
}
|
|
59
|
+
}
|