|
function copyToClipboard(value, elToSelect) { |
|
navigator.clipboard.writeText(value); |
|
elToSelect && selectText(elToSelect); |
|
} |
|
|
|
function selectText(container) { |
|
if (document.selection) { // IE |
|
var range = document.body.createTextRange(); |
|
range.moveToElementText(container); |
|
range.select(); |
|
} else if (window.getSelection) { |
|
var range = document.createRange(); |
|
range.selectNode(container); |
|
window.getSelection().removeAllRanges(); |
|
window.getSelection().addRange(range); |
|
} |
|
} |
|
|
|
function handleMainMessage(message) { |
|
console.log('Got main message:', message); |
|
if (message.success) { |
|
if (message.action === 'list-sobject-types') { |
|
myVue.sobjects = message.data.sobjects.filter(x => x.queryable && x.createable && !x.associateEntityType).sort((a, b) => a.label < b.label ? -1 : 1); |
|
} |
|
else if (message.action === 'describe') { |
|
myVue.rawDescription = message.data; |
|
} |
|
} |
|
else { |
|
alert('An error occurred when trying to make a request to Salesforce. Please check the JavaScript Console for more information.'); |
|
console.error(message); |
|
} |
|
} |
|
|
|
/** |
|
* Uses ExcelJS to create and download an XLSX file. |
|
* @requires {@link https://cdnjs.cloudflare.com/ajax/libs/exceljs/4.3.0/exceljs.min.js} |
|
* @param {string} name |
|
* @param {downloadXLSX__Worksheet[]} worksheets |
|
*/ |
|
function downloadXLSX(name, worksheets) { |
|
const wb = new ExcelJS.Workbook(); |
|
worksheets.forEach((worksheet, worksheetIndex) => { |
|
const ws = wb.addWorksheet(worksheet.name ?? `Sheet ${worksheetIndex + 1}`); |
|
if (worksheet.frozenRows || worksheet.frozenColumns) { |
|
const view = {state: 'frozen'}; |
|
if (worksheet.frozenRows) view.ySplit = worksheet.frozenRows; |
|
if (worksheet.frozenColumns) view.xSplit = worksheet.frozenColumns; |
|
ws.views = [view]; |
|
} |
|
|
|
ws.columns = worksheet.rows[0].map((header, headerIndex) => { |
|
const isObject = header && 'object' === typeof header && !(header instanceof Date); |
|
const defaultWidth = 'number' === typeof worksheet.columnWidths |
|
? worksheet.columnWidths |
|
: worksheet.columnWidths?.[headerIndex] ?? 10; |
|
return { |
|
header: isObject ? header.header ?? `Column ${headerIndex + 1}` : header, |
|
key: isObject ? header.key ?? `col${headerIndex}` : header, |
|
width: isObject ? header.width ?? defaultWidth : defaultWidth, |
|
}; |
|
}); |
|
|
|
worksheet.rows.slice(1).forEach(row => { |
|
ws.addRow(row); |
|
}); |
|
|
|
if (worksheet.argbHeaderColor || worksheet.argbRowColors?.length) { |
|
ws.eachRow((row, rowNumber) => { |
|
row.eachCell({includeEmpty: true}, (cell, cellNumber) => { |
|
if (rowNumber === 1) { |
|
if (worksheet.argbHeaderColor) { |
|
if (worksheet.argbHeaderColor.fg) { |
|
cell.font = { color: {argb: worksheet.argbHeaderColor.fg} }; |
|
} |
|
if (worksheet.argbHeaderColor.bg) { |
|
cell.fill = { |
|
type: 'pattern', |
|
pattern: 'solid', |
|
fgColor: {argb: worksheet.argbHeaderColor.bg} |
|
}; |
|
} |
|
} |
|
} |
|
else if (worksheet.argbRowColors?.length) { |
|
const argbRowColor = worksheet.argbRowColors[(rowNumber - 2) % worksheet.argbRowColors.length]; |
|
if (argbRowColor.fg) { |
|
cell.font = { color: {argb: argbRowColor.fg} }; |
|
} |
|
if (argbRowColor.bg) { |
|
cell.fill = { |
|
type: 'pattern', |
|
pattern: 'solid', |
|
fgColor: {argb: argbRowColor.bg} |
|
}; |
|
} |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
if (worksheet.autoFilter) { |
|
ws.autoFilter = { |
|
from: { column: 1, row: 1 }, |
|
to: { column: worksheet.rows?.[0]?.length ?? 0, row: worksheet.rows.length }, |
|
}; |
|
} |
|
|
|
worksheet.callback && worksheet.callback(ws); |
|
}); |
|
|
|
wb.xlsx.writeBuffer().then((data) => { |
|
const blob = new Blob([data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8' }); |
|
const url = window.URL.createObjectURL(blob); |
|
Object.assign(document.createElement('a'), { |
|
href: url, |
|
download: name, |
|
}).click(); |
|
window.URL.revokeObjectURL(url); |
|
}); |
|
} |
|
/** |
|
* @typedef {Object} downloadXLSX__Worksheet |
|
* @property {string} name |
|
* @property {any[][]} rows |
|
* @property {number} [frozenRows=0] |
|
* @property {number} [frozenColumns=0] |
|
* @property {{bg?: string, fg?: string}} [argbHeaderColor] |
|
* @property {{bg?: string, fg?: string}[]} [argbRowColors] |
|
* @property {number[]|number} [columnWidths] |
|
* @property {boolean} [autoFilter=false] |
|
* @property {(excelWorksheet: any) => void} callback |
|
* If specified this is called after creating the ExcelJS worksheet and it is |
|
* passed the ExcelJS worksheet object that was created so that you can make |
|
* other custom changes. |
|
*/ |
|
|
|
/** |
|
* @see https://gist.github.com/westc/ea154cab93336999968ece2fe6f629e1 |
|
* Takes an integer and returns the corresponding column name (eg. 5 becomes E). |
|
* @param {number} number |
|
* The integer to convert to a column name. If `opt_isZeroBased` is true |
|
* then 0 will be converted to "A", otherwise 1 will be converted to "A". |
|
* @param {?boolean=} opt_isZeroBased |
|
* Indicates if `number` is interpreted as a 0-based index. |
|
* @return {string} |
|
* The column name. |
|
*/ |
|
function toColumnName(number, opt_isZeroBased) { |
|
for ( |
|
var index, num = number + (opt_isZeroBased ? 1 : 0), ret = ''; |
|
index = num - 1, num; |
|
num = Math.floor(index / 26) |
|
) { |
|
ret = String.fromCharCode(65 + index % 26) + ret; |
|
} |
|
return ret; |
|
} |
|
|
|
const myVue = window.myVue = new Vue({ |
|
el: '#myVue', |
|
data: { |
|
sobjects: [], |
|
selectedSObjectName: null, |
|
rawDescription: null, |
|
includedXLSXDescriptions: {}, |
|
combinedXLSXName: 'Combined', |
|
}, |
|
computed: { |
|
selectedSObject() { |
|
return this.sobjects.find(x => x.name === this.selectedSObjectName); |
|
}, |
|
selectedSObjectJSON() { |
|
return JSON.stringify(this.selectedSObject, null, 2); |
|
}, |
|
rawDescriptionJSON() { |
|
return JSON.stringify(this.rawDescription, null, 2); |
|
}, |
|
hasRawDescription() { |
|
return this.rawDescription != null; |
|
}, |
|
hasSelectedSObject() { |
|
return this.selectedSObjectJSON != null; |
|
}, |
|
rawDescriptionRows() { |
|
return this.getRawDescriptionRows(this.rawDescription); |
|
}, |
|
isIncludedInXLSX() { |
|
return !!this.selectedSObject |
|
&& Object.keys(this.includedXLSXDescriptions).includes(this.selectedSObject.name); |
|
}, |
|
canDownloadCombinedXLSX() { |
|
return Object.keys(this.includedXLSXDescriptions).length > 0; |
|
}, |
|
sortedCombinedRawDescriptions() { |
|
return Object.values(this.includedXLSXDescriptions).sort((a, b) => a.label < b.label ? -1 : 1); |
|
}, |
|
sobjectColumnDefs() { |
|
return this.rawDescriptionRows[0].map((c, i) => ({ |
|
field: `field${i}`, |
|
headerName: c, |
|
sortable: true, |
|
filter: true, |
|
suppressMovable: true |
|
})); |
|
}, |
|
sobjectRows() { |
|
return this.rawDescriptionRows.slice(1).map( |
|
row => row.reduce((row, c, i) => { |
|
row[`field${i}`] = c; |
|
return row; |
|
}, {}) |
|
); |
|
} |
|
}, |
|
methods: { |
|
toggleIncludeInXLSX() { |
|
if (this.isIncludedInXLSX) { |
|
this.removeIncludedInXLSX(this.selectedSObject.name); |
|
} |
|
else { |
|
const newObj = JSON.parse(JSON.stringify(this.includedXLSXDescriptions)); |
|
newObj[this.selectedSObject.name] = this.rawDescription; |
|
this.includedXLSXDescriptions = newObj; |
|
} |
|
}, |
|
removeIncludedInXLSX(name) { |
|
const newObj = JSON.parse(JSON.stringify(this.includedXLSXDescriptions)); |
|
delete newObj[name]; |
|
this.includedXLSXDescriptions = newObj; |
|
}, |
|
validateSObjectSelection(e) { |
|
if (this.selectedSObject) { |
|
this.rawDescription = null; |
|
parent.postMessage({ |
|
action: 'describe', |
|
path: this.selectedSObject.urls.describe, |
|
objectName: this.selectedSObject.name, |
|
}); |
|
} |
|
else { |
|
this.selectedSObjectName = null; |
|
} |
|
}, |
|
copySelectedSObjectJSON() { |
|
copyToClipboard(this.selectedSObjectJSON, this.$refs.selectedSObjectJSON); |
|
}, |
|
downloadSelectedSObjectJSON() { |
|
const blob = new Blob([this.selectedSObjectJSON], { type: 'application/json;charset=utf-8' }); |
|
const url = window.URL.createObjectURL(blob); |
|
Object.assign(document.createElement('a'), { |
|
href: url, |
|
download: `${this.selectedSObject.label}.sobject-summary.json`, |
|
}).click(); |
|
window.URL.revokeObjectURL(url); |
|
}, |
|
copyRawDescriptionJSON() { |
|
copyToClipboard(this.rawDescriptionJSON, this.$refs.rawDescriptionJSON); |
|
}, |
|
downloadRawDescriptionJSON() { |
|
const blob = new Blob([this.rawDescriptionJSON], { type: 'application/json;charset=utf-8' }); |
|
const url = window.URL.createObjectURL(blob); |
|
Object.assign(document.createElement('a'), { |
|
href: url, |
|
download: `${this.selectedSObject.label}.sobject-description.json`, |
|
}).click(); |
|
window.URL.revokeObjectURL(url); |
|
}, |
|
getRawDescriptionRows(rawDescription) { |
|
const rows = [[ |
|
"Object", |
|
"Label", |
|
"Name", |
|
"Description", |
|
"Type", |
|
"Required", |
|
"Picklist Values", |
|
"Sync", |
|
]]; |
|
|
|
if (rawDescription != null) { |
|
const objectName = rawDescription.name; |
|
|
|
rawDescription.fields.forEach(field => { |
|
let typeValue = field.type; |
|
if (typeValue == "reference") { |
|
typeValue += `(${field.referenceTo.join(',')})`; |
|
} else if (typeValue.endsWith("picklist")) { |
|
typeValue += `(${field.restrictedPicklist ? '' : 'un'}restricted)`; |
|
} else if (["string", "textarea", "url", "phone"].includes(typeValue)) { |
|
typeValue += `(${field.length})`; |
|
} else if (["double", "currency"].includes(typeValue)) { |
|
typeValue += `(${field.precision},${field.scale})`; |
|
} else if (typeValue == "int") { |
|
typeValue += `(${field.digits})`; |
|
} |
|
rows.push([ |
|
objectName, |
|
field.label, |
|
field.name, |
|
field.description, |
|
typeValue, |
|
field.nillable ? "No" : "Yes", |
|
field.picklistValues.map(v => JSON.stringify(v)).join('\n'), |
|
null, |
|
]); |
|
}); |
|
} |
|
|
|
return rows; |
|
}, |
|
downloadRawDescriptionXLSX() { |
|
this.downloadDescriptionsAsXLSX( |
|
this.selectedSObject.label, |
|
[this.rawDescription] |
|
); |
|
}, |
|
downloadCombinedXLSX() { |
|
this.downloadDescriptionsAsXLSX( |
|
this.combinedXLSXName, |
|
this.sortedCombinedRawDescriptions |
|
); |
|
}, |
|
downloadDescriptionsAsXLSX(name, descriptions) { |
|
const worksheets = descriptions.map(d => ( |
|
{ |
|
name: d.label, |
|
rows: this.getRawDescriptionRows(d), |
|
argbHeaderColor: {bg: 'FF666666', fg: 'FFFFFFFF'}, |
|
argbRowColors: [ |
|
{bg: 'FFBBBBBB', fg: 'FF000000'}, |
|
{bg: 'FFDDDDDD', fg: 'FF000000'}, |
|
], |
|
columnWidths: [30, 30, 30, 40, 30, 10, 50, 10], |
|
autoFilter: true, |
|
frozenColumns: 2, |
|
frozenRows: 1, |
|
callback(sheet) { |
|
const LAST_COL_ID = toColumnName(sheet.columnCount); |
|
const addr = `${LAST_COL_ID}2:${LAST_COL_ID}${sheet.rowCount}`; |
|
sheet.dataValidations.add(addr, { |
|
type: 'list', |
|
allowBlank: true, |
|
formulae: ['"Yes,No"'], |
|
showErrorMessage: true, |
|
}); |
|
for (let colNumber = 1; colNumber <= sheet.columnCount; colNumber++) { |
|
sheet.getColumn(colNumber).alignment = { |
|
vertical: 'top', |
|
horizontal: 'left', |
|
wrapText: true |
|
}; |
|
} |
|
} |
|
} |
|
)); |
|
downloadXLSX(name, worksheets); |
|
} |
|
}, |
|
components: { |
|
'ag-grid': { |
|
props: ['columnDefs', 'defaultColDef', 'rows', 'theme'], |
|
data() { |
|
return { |
|
getGridOptions: null |
|
}; |
|
}, |
|
watch: { |
|
columnDefs() { |
|
this.redraw(); |
|
}, |
|
defaultColDef() { |
|
this.redraw(); |
|
}, |
|
rows() { |
|
this.redraw(); |
|
}, |
|
theme() { |
|
this.redraw(); |
|
} |
|
}, |
|
methods: { |
|
redraw() { |
|
const {wrapper} = this.$refs; |
|
let gridOptions = this.getGridOptions && this.getGridOptions(); |
|
gridOptions?.api?.destroy(); |
|
wrapper.innerHTML = `<div style="height: 100%;" class="ag-theme-${this.theme ?? 'alpine'}"></div>`; |
|
gridOptions = { |
|
defaultColDef: this.defaultColDef, |
|
columnDefs: this.columnDefs, |
|
rowData: this.rows |
|
}; |
|
new agGrid.Grid(wrapper.childNodes[0], gridOptions); |
|
this.getGridOptions = () => gridOptions; |
|
} |
|
}, |
|
mounted() { |
|
this.redraw(); |
|
}, |
|
template: ` |
|
<div> |
|
<div ref="wrapper" :class="'ag-theme-' + theme" style="height: 100%;"></div> |
|
</div> |
|
` |
|
} |
|
} |
|
}); |