Skip to content

Instantly share code, notes, and snippets.

@dvygolov
Last active December 12, 2025 09:28
Show Gist options
  • Select an option

  • Save dvygolov/4449da3e5aa420d4ecf0b4b37f5e7b82 to your computer and use it in GitHub Desktop.

Select an option

Save dvygolov/4449da3e5aa420d4ecf0b4b37f5e7b82 to your computer and use it in GitHub Desktop.
Script to manage column presets of Ads Manager: import/export columns
// ============================================
// Configuration
// ============================================
const Config = {
VERSION: "2025.12.12",
API_VERSION: "v23.0",
API_URL: "https://adsmanager-graph.facebook.com/v23.0/"
};
// ============================================
// Logger Class
// ============================================
class Logger {
constructor(uiInstance = null) {
this.ui = uiInstance;
}
setUI(uiInstance) {
this.ui = uiInstance;
}
log(message, type = "info") {
if (this.ui && this.ui.log) {
this.ui.log(message, type);
}
if (type === "error") {
console.error(message);
} else {
console.log(message);
}
}
info(message) {
this.log(message, "info");
}
error(message) {
this.log(message, "error");
}
success(message) {
this.log(message, "success");
}
warning(message) {
this.log(message, "warning");
}
}
// Global logger instance
const logger = new Logger();
// ============================================
// FileHelper Class
// ============================================
class FileHelper {
async readFileAsJsonAsync(file) {
try {
const fileContent = await this.readFileAsync(file);
return JSON.parse(fileContent);
} catch (error) {
console.error("Error:", error);
throw error;
}
}
readFileAsync(file) {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject("Error reading file");
reader.readAsText(file);
});
}
}
// ============================================
// FileSelector Class
// ============================================
class FileSelector {
constructor(fileProcessor) {
this.fileProcessor = fileProcessor;
}
createDiv() {
this.div = document.createElement("div");
this.div.style.position = "fixed";
this.div.style.top = "50%";
this.div.style.left = "50%";
this.div.style.transform = "translate(-50%, -50%)";
this.div.style.width = "200px";
this.div.style.height = "120px";
this.div.style.backgroundColor = "yellow";
this.div.style.zIndex = "1001";
this.div.style.display = "flex";
this.div.style.flexDirection = "column";
this.div.style.alignItems = "center";
this.div.style.justifyContent = "center";
this.div.style.padding = "10px";
this.div.style.boxSizing = "border-box";
this.div.style.borderRadius = "10px";
var title = document.createElement("div");
title.innerHTML = "Select file to import preset";
title.style.textAlign = "center";
title.style.fontWeight = "bold";
var closeButton = document.createElement("button");
closeButton.innerHTML = "X";
closeButton.style.position = "absolute";
closeButton.style.top = "5px";
closeButton.style.right = "5px";
closeButton.style.border = "none";
closeButton.style.background = "none";
closeButton.style.cursor = "pointer";
closeButton.onclick = () => {
document.body.removeChild(this.div);
};
this.div.appendChild(title);
this.div.appendChild(closeButton);
}
createFileInput() {
this.fileInput = document.createElement("input");
this.fileInput.type = "file";
this.fileInput.accept = ".json";
this.fileInput.style.display = "none";
}
createButton() {
this.button = document.createElement("button");
this.button.textContent = "Select File";
this.button.onclick = () => {
this.fileInput.click();
};
}
show() {
return new Promise((resolve, reject) => {
this.createDiv();
this.createFileInput();
this.createButton();
this.div.appendChild(this.button);
this.div.appendChild(this.fileInput);
document.body.appendChild(this.div);
this.fileInput.onchange = async () => {
if (!this.fileInput.files || this.fileInput.files.length === 0) {
document.body.removeChild(this.div);
alert("Operation canceled");
reject("File selection cancelled by user");
return;
}
try {
const result = await this.fileProcessor(this.fileInput.files[0]);
document.body.removeChild(this.div);
resolve(result);
} catch (error) {
document.body.removeChild(this.div);
reject(error);
}
};
});
}
}
// ============================================
// Facebook API Class
// ============================================
class FbApi {
apiUrl = Config.API_URL;
async getRequest(path, qs = null, token = null) {
token = token ?? __accessToken;
let finalUrl = path.startsWith('http') ? path : this.apiUrl + path;
const hasAccessToken = finalUrl.includes('access_token=');
if (!hasAccessToken) {
qs = qs != null ? `${qs}&access_token=${token}` : `access_token=${token}`;
const separator = finalUrl.includes('?') ? '&' : '?';
finalUrl = `${finalUrl}${separator}${qs}`;
} else if (qs) {
finalUrl = `${finalUrl}&${qs}`;
}
let f = await fetch(finalUrl, {
headers: {
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"accept-language": "ca-ES,ca;q=0.9,en-US;q=0.8,en;q=0.7",
"cache-control": "max-age=0",
"sec-ch-ua": '"Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
},
referrerPolicy: "strict-origin-when-cross-origin",
body: null,
method: "GET",
mode: "cors",
credentials: "include",
referrer: "https://business.facebook.com/",
});
let json = await f.json();
return json;
}
async getAllPages(path, qs, token = null) {
let items = [];
let page = await this.getRequest(path, qs, token);
items = items.concat(page.data);
while (page.paging && page.paging.next) {
page = await this.getRequest(page.paging.next, null, token);
items = items.concat(page.data);
}
return items;
}
async postRequest(path, body, token = null) {
token = token ?? __accessToken;
body["access_token"] = token;
let headers = {
accept: "*/*",
"accept-language": "en-US,en;q=0.9",
"content-type": "application/x-www-form-urlencoded",
"sec-ch-ua": '"Google Chrome";v="107", "Chromium";v="107", "Not=A?Brand";v="24"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
};
let finalUrl = path.startsWith('http') ? path : this.apiUrl + path;
let f = await fetch(finalUrl, {
headers: headers,
referrer: "https://business.facebook.com/",
referrerPolicy: "origin-when-cross-origin",
body: new URLSearchParams(body).toString(),
method: "POST",
mode: "cors",
credentials: "include",
});
let json = await f.json();
return json;
}
}
// Global API instance
const API = new FbApi();
// ============================================
// Account Manager Class
// ============================================
class AccountManager {
constructor() {
this.accounts = [];
}
async loadAll() {
try {
logger.info("Loading all accounts...");
const accounts = await API.getAllPages("me/adaccounts", "fields=id,name,account_status");
this.accounts = accounts.map(account => {
const accountId = account.id.replace("act_", "");
return {
id: accountId,
name: account.name || accountId,
status: account.account_status
};
});
logger.success(`Loaded ${this.accounts.length} accounts.`);
return this.accounts;
} catch (error) {
logger.error("Error loading accounts: " + error);
throw error;
}
}
getAll() {
return this.accounts;
}
findById(accountId) {
return this.accounts.find(acc => acc.id === accountId);
}
}
// Global account manager instance
const accountManager = new AccountManager();
// Legacy global variable accessor
let allAccountsData = new Proxy({}, {
get(target, prop) {
if (typeof prop === 'symbol') return undefined;
const accounts = accountManager.getAll();
if (prop === 'length') return accounts.length;
if (prop === 'find') return accounts.find.bind(accounts);
if (prop === 'map') return accounts.map.bind(accounts);
if (prop === 'filter') return accounts.filter.bind(accounts);
if (prop === 'forEach') return accounts.forEach.bind(accounts);
return accounts[prop];
}
});
// ============================================
// Custom Derived Metrics Functions
// ============================================
const CUSTOM_METRIC_PREFIX = "custom_derived_metrics:";
async function fetchCustomMetrics(accountId) {
const accId = accountId ?? require("BusinessUnifiedNavigationContext").adAccountID;
logger.info(`Loading custom metrics for account ${accId}...`);
const metrics = await API.getAllPages(
`act_${accId}/ad_custom_derived_metrics`,
`fields=name,formula,format_type,description`
);
logger.success(`Loaded ${metrics.length} custom metrics.`);
return metrics;
}
async function createCustomMetric(accountId, metricData) {
const accId = accountId ?? require("BusinessUnifiedNavigationContext").adAccountID;
logger.info(`Creating custom metric "${metricData.name}" on account ${accId}...`);
const data = {
name: metricData.name,
formula: metricData.formula,
format_type: metricData.format_type || "FLOAT",
permission: "shared"
};
if (metricData.description) {
data.description = metricData.description;
}
const result = await API.postRequest(`act_${accId}/ad_custom_derived_metrics`, data);
if (result.id) {
logger.success(`Created custom metric "${metricData.name}" with ID ${result.id}`);
return result.id;
} else {
logger.error(`Failed to create custom metric "${metricData.name}": ${JSON.stringify(result)}`);
return null;
}
}
function extractCustomMetricIds(preset) {
const customMetricIds = [];
if (preset.columns && Array.isArray(preset.columns)) {
for (const col of preset.columns) {
if (col.column_id && col.column_id.startsWith(CUSTOM_METRIC_PREFIX)) {
const metricId = col.column_id.replace(CUSTOM_METRIC_PREFIX, "");
customMetricIds.push(metricId);
}
}
}
return customMetricIds;
}
function replaceCustomMetricIds(preset, idMapping) {
if (!preset.columns || !Array.isArray(preset.columns)) return preset;
const newColumns = preset.columns.map(col => {
if (col.column_id && col.column_id.startsWith(CUSTOM_METRIC_PREFIX)) {
const oldId = col.column_id.replace(CUSTOM_METRIC_PREFIX, "");
const newId = idMapping[oldId];
if (newId) {
return { ...col, column_id: `${CUSTOM_METRIC_PREFIX}${newId}` };
}
}
return col;
});
return { ...preset, columns: newColumns };
}
// ============================================
// Column Preset Functions
// ============================================
async function fetchAccountPresets(accountId) {
const accId = accountId ?? require("BusinessUnifiedNavigationContext").adAccountID;
logger.info(`Loading presets for account ${accId}...`);
let js = await API.getRequest(
`act_${accId}`,
`fields=["user_settings{id,column_presets{attribution_windows,columns,id,name,time_created,time_updated}},ad_column_sizes{page,tab,report,view,columns}"]`
);
const presets = js.user_settings?.column_presets?.data || [];
const sizes = js.ad_column_sizes?.data || [];
logger.success(`Loaded ${presets.length} presets.`);
return { presets, sizes };
}
async function exportColumnPreset(selectedPreset, sizes = [], accountId = null) {
if (!selectedPreset) {
logger.warning("No preset selected");
return null;
}
const jsFile = {
preset: selectedPreset,
sizes: sizes,
customMetrics: []
};
// Check for custom metrics in preset
const customMetricIds = extractCustomMetricIds(selectedPreset);
if (customMetricIds.length > 0) {
logger.info(`Found ${customMetricIds.length} custom metric(s) in preset, fetching details...`);
const allMetrics = await fetchCustomMetrics(accountId);
const usedMetrics = allMetrics.filter(m => customMetricIds.includes(m.id));
jsFile.customMetrics = usedMetrics.map(m => ({
id: m.id,
name: m.name,
formula: m.formula,
format_type: m.format_type,
description: m.description || ""
}));
logger.success(`Included ${jsFile.customMetrics.length} custom metric(s) in export.`);
}
const blob = new Blob([JSON.stringify(jsFile)], { type: "application/json" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `${selectedPreset.name}.json`;
a.click();
logger.success(`Exported preset: ${selectedPreset.name}`);
return selectedPreset;
}
async function fetchUserSettingsId(adAccountId) {
let accId = adAccountId ?? require("BusinessUnifiedNavigationContext").adAccountID;
logger.info(`Getting user settings for acc ${accId}...`);
let js = await API.getRequest(`act_${accId}`, `fields=[]`);
let usId = js?.user_settings?.id;
if (usId == null) {
logger.info(`No default user settings found! Creating them...`);
js = await API.getRequest(`act_${accId}/user_settings`, `method=post`);
usId = js.id;
}
return usId;
}
async function uploadPreset(userSettingsId, presetData) {
let data = {
name: presetData.name,
attribution_windows: JSON.stringify(presetData.attribution_windows),
columns: JSON.stringify(presetData.columns),
};
logger.info(`Uploading preset ${presetData.name} to user settings ${userSettingsId}...`);
let js = await API.postRequest(`${userSettingsId}/column_presets`, data);
return js.id;
}
async function setDefaultColumnPreset(adAccountId, presetId) {
let accId = adAccountId ?? require("BusinessUnifiedNavigationContext").adAccountID;
logger.info(`Setting default column preset for acc ${accId}, preset id ${presetId}...`);
let data = {
default_column_preset: `{ "id": "${presetId}" }`,
default_column_preset_id: presetId,
};
let js = await API.postRequest(`act_${accId}/user_settings`, data);
return js;
}
async function uploadSize(adAccountId, size) {
let accId = adAccountId ?? require("BusinessUnifiedNavigationContext").adAccountID;
const columns = size.columns.reduce((acc, { key, value }) => {
acc[key] = parseInt(value, 10);
return acc;
}, {});
let data = {
page: size.page,
tab: size.tab,
columns: JSON.stringify(columns),
};
logger.info(`Uploading sizes to ad account ${accId}...`);
let js = await API.postRequest(`act_${accId}/ad_column_sizes`, data);
const sizeId = js.id;
js = await API.postRequest(sizeId, data);
return js.success;
}
async function importPresetToAccount(accountId, presetContent) {
try {
let presetToUpload = { ...presetContent.preset };
// Handle custom metrics if present
if (presetContent.customMetrics && presetContent.customMetrics.length > 0) {
logger.info(`Creating ${presetContent.customMetrics.length} custom metric(s) on account ${accountId}...`);
const idMapping = {};
for (const metric of presetContent.customMetrics) {
const newId = await createCustomMetric(accountId, metric);
if (newId) {
idMapping[metric.id] = newId;
} else {
logger.warning(`Skipping metric "${metric.name}" - creation failed`);
}
}
// Replace old IDs with new IDs in preset
presetToUpload = replaceCustomMetricIds(presetToUpload, idMapping);
}
const userSettingsId = await fetchUserSettingsId(accountId);
let presetId = await uploadPreset(userSettingsId, presetToUpload);
await setDefaultColumnPreset(accountId, presetId);
logger.success(`Imported preset to account ${accountId}`);
return { success: true, presetId };
} catch (error) {
logger.error(`Error importing to account ${accountId}: ${error}`);
return { success: false, error };
}
}
async function importPresetToSelectedAccounts(accountIds, presetContent, uiInstance) {
logger.info(`Importing preset to ${accountIds.length} accounts...`);
let successCount = 0;
let failedCount = 0;
for (let i = 0; i < accountIds.length; i++) {
const accountId = accountIds[i];
logger.info(`Processing account ${accountId} (${i+1}/${accountIds.length})...`);
const result = await importPresetToAccount(accountId, presetContent);
if (result.success) {
successCount++;
} else {
failedCount++;
}
// Add delay between accounts to avoid rate limiting
if (i < accountIds.length - 1) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
const summaryMessage = `Processed ${accountIds.length} accounts: ${successCount} successful, ${failedCount} failed.`;
logger.success(summaryMessage);
}
async function importSizesToAccount(accountId, sizes) {
try {
for (let i = 0; i < sizes.length; i++) {
logger.info(`Uploading size ${i+1}/${sizes.length} to account ${accountId}...`);
await uploadSize(accountId, sizes[i]);
}
logger.success(`Imported ${sizes.length} sizes to account ${accountId}`);
return { success: true };
} catch (error) {
logger.error(`Error importing sizes to account ${accountId}: ${error}`);
return { success: false, error };
}
}
function reloadPageWithPreset(presetId) {
const urlObj = new URL(window.location.href);
urlObj.searchParams.set("column_preset", presetId);
window.location.href = urlObj.toString();
}
// ============================================
// Column Presets Manager UI Class
// ============================================
class ColumnPresetsManagerUI {
constructor() {
this.div = null;
this.buttons = {};
this.selectedExportAccountId = null;
this.selectedImportAccountIds = [];
this.logArea = null;
this.accountPresets = []; // Presets loaded for selected account
this.selectedPreset = null; // Currently selected preset
this.accountSizes = []; // Column sizes for selected account
}
createDiv() {
this.div = document.createElement("div");
this.div.style.position = "fixed";
this.div.style.top = "50%";
this.div.style.left = "50%";
this.div.style.transform = "translate(-50%, -50%)";
this.div.style.width = "400px";
this.div.style.maxHeight = "90vh";
this.div.style.overflowY = "auto";
this.div.style.backgroundColor = "yellow";
this.div.style.zIndex = "1000";
this.div.style.display = "flex";
this.div.style.flexDirection = "column";
this.div.style.alignItems = "center";
this.div.style.justifyContent = "flex-start";
this.div.style.padding = "20px";
this.div.style.boxSizing = "border-box";
this.div.style.borderRadius = "10px";
this.div.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.2)";
// Create and style the title
const title = document.createElement("div");
title.innerHTML = `<h2>FB Column Preset Manager ${Config.VERSION}</h2><p><a href='https://yellowweb.top' target='_blank'>by Yellow Web</a></p>`;
title.style.textAlign = "center";
title.style.marginBottom = "20px";
// Create and style the close button
const closeButton = document.createElement("button");
closeButton.innerHTML = "X";
closeButton.style.position = "absolute";
closeButton.style.top = "10px";
closeButton.style.right = "10px";
closeButton.style.border = "none";
closeButton.style.background = "none";
closeButton.style.fontSize = "18px";
closeButton.style.cursor = "pointer";
closeButton.onclick = () => {
document.body.removeChild(this.div);
};
this.div.appendChild(title);
this.div.appendChild(closeButton);
return this.div;
}
createButton(id, text, onClick) {
const button = document.createElement("button");
button.id = id;
button.textContent = text;
button.style.margin = "10px 0";
button.style.padding = "10px 15px";
button.style.width = "100%";
button.style.backgroundColor = "#4CAF50";
button.style.color = "white";
button.style.border = "none";
button.style.borderRadius = "5px";
button.style.cursor = "pointer";
button.style.fontSize = "16px";
button.setAttribute("data-original-text", text);
this.buttons[id] = button;
button.onclick = async () => {
this.setButtonLoading(id, true);
try {
await onClick();
} finally {
this.setButtonLoading(id, false);
}
};
return button;
}
setButtonLoading(id, isLoading) {
const button = this.buttons[id];
if (!button) return;
if (isLoading) {
button.disabled = true;
button.style.opacity = "0.7";
button.style.cursor = "not-allowed";
button.textContent = "Working on it...";
} else {
button.disabled = false;
button.style.opacity = "1";
button.style.cursor = "pointer";
button.textContent = button.getAttribute("data-original-text");
}
}
createExportAccountDropdown() {
const container = document.createElement("div");
container.style.width = "100%";
container.style.margin = "10px 0";
const label = document.createElement("label");
label.textContent = "Select account to export from:";
label.style.display = "block";
label.style.marginBottom = "5px";
label.style.fontSize = "14px";
label.style.fontWeight = "bold";
const select = document.createElement("select");
select.id = "ywbExportAccountSelect";
select.style.width = "100%";
select.style.padding = "8px";
select.style.borderRadius = "5px";
select.style.border = "1px solid #ccc";
select.style.fontSize = "14px";
const defaultOption = document.createElement("option");
defaultOption.value = "";
defaultOption.textContent = "-- Choose an account --";
defaultOption.disabled = true;
defaultOption.selected = true;
select.appendChild(defaultOption);
allAccountsData.forEach(account => {
const option = document.createElement("option");
option.value = account.id;
option.textContent = `${account.id} - ${account.name}`;
select.appendChild(option);
});
select.onchange = async () => {
this.selectedExportAccountId = select.value;
this.selectedPreset = null;
this.accountPresets = [];
this.accountSizes = [];
// Load presets for selected account
if (select.value) {
try {
const { presets, sizes } = await fetchAccountPresets(select.value);
this.accountPresets = presets;
this.accountSizes = sizes;
this.refreshPresetDropdown();
} catch (error) {
logger.error(`Error loading presets: ${error}`);
}
} else {
this.refreshPresetDropdown();
}
};
container.appendChild(label);
container.appendChild(select);
return container;
}
createPresetDropdown() {
const container = document.createElement("div");
container.style.width = "100%";
container.style.margin = "10px 0";
const label = document.createElement("label");
label.textContent = "Select preset to export:";
label.style.display = "block";
label.style.marginBottom = "5px";
label.style.fontSize = "14px";
label.style.fontWeight = "bold";
const select = document.createElement("select");
select.id = "ywbPresetSelect";
select.style.width = "100%";
select.style.padding = "8px";
select.style.borderRadius = "5px";
select.style.border = "1px solid #ccc";
select.style.fontSize = "14px";
const defaultOption = document.createElement("option");
defaultOption.value = "";
defaultOption.textContent = "-- Select account first --";
defaultOption.disabled = true;
defaultOption.selected = true;
select.appendChild(defaultOption);
select.onchange = () => {
const selectedIndex = parseInt(select.value, 10);
if (!isNaN(selectedIndex) && this.accountPresets[selectedIndex]) {
this.selectedPreset = this.accountPresets[selectedIndex];
} else {
this.selectedPreset = null;
}
};
container.appendChild(label);
container.appendChild(select);
return container;
}
refreshPresetDropdown() {
const select = document.getElementById("ywbPresetSelect");
if (!select) return;
select.innerHTML = "";
const defaultOption = document.createElement("option");
defaultOption.value = "";
defaultOption.disabled = true;
defaultOption.selected = true;
if (this.accountPresets.length === 0) {
defaultOption.textContent = this.selectedExportAccountId
? "-- No presets available --"
: "-- Select account first --";
select.appendChild(defaultOption);
return;
}
defaultOption.textContent = "-- Choose a preset --";
select.appendChild(defaultOption);
this.accountPresets.forEach((preset, index) => {
const option = document.createElement("option");
option.value = index;
option.textContent = preset.name;
select.appendChild(option);
});
}
createImportAccountDropdown() {
const container = document.createElement("div");
container.style.width = "100%";
container.style.margin = "10px 0";
const label = document.createElement("label");
label.textContent = "Select accounts to import to:";
label.style.display = "block";
label.style.marginBottom = "5px";
label.style.fontSize = "14px";
label.style.fontWeight = "bold";
const selectAllContainer = document.createElement("div");
selectAllContainer.style.marginBottom = "5px";
const selectAllCheckbox = document.createElement("input");
selectAllCheckbox.type = "checkbox";
selectAllCheckbox.id = "ywbSelectAllAccounts";
selectAllCheckbox.style.marginRight = "5px";
const selectAllLabel = document.createElement("label");
selectAllLabel.htmlFor = "ywbSelectAllAccounts";
selectAllLabel.textContent = "Select All Accounts";
selectAllLabel.style.fontSize = "14px";
selectAllContainer.appendChild(selectAllCheckbox);
selectAllContainer.appendChild(selectAllLabel);
const select = document.createElement("select");
select.id = "ywbImportAccountSelect";
select.multiple = true;
select.size = Math.min(allAccountsData.length, 8);
select.style.width = "100%";
select.style.padding = "5px";
select.style.borderRadius = "5px";
select.style.border = "1px solid #ccc";
select.style.fontSize = "12px";
allAccountsData.forEach(account => {
const option = document.createElement("option");
option.value = account.id;
option.textContent = `${account.id} - ${account.name}`;
select.appendChild(option);
});
const updateSelection = () => {
this.selectedImportAccountIds = Array.from(select.selectedOptions).map(opt => opt.value);
};
select.onchange = updateSelection;
selectAllCheckbox.onchange = () => {
if (selectAllCheckbox.checked) {
Array.from(select.options).forEach(opt => opt.selected = true);
} else {
Array.from(select.options).forEach(opt => opt.selected = false);
}
updateSelection();
};
container.appendChild(label);
container.appendChild(selectAllContainer);
container.appendChild(select);
return container;
}
refreshDropdowns() {
const exportSelect = document.getElementById("ywbExportAccountSelect");
const importSelect = document.getElementById("ywbImportAccountSelect");
if (exportSelect) {
const currentValue = exportSelect.value;
exportSelect.innerHTML = "";
const defaultOption = document.createElement("option");
defaultOption.value = "";
defaultOption.textContent = "-- Choose an account --";
defaultOption.disabled = true;
defaultOption.selected = !currentValue;
exportSelect.appendChild(defaultOption);
allAccountsData.forEach(account => {
const option = document.createElement("option");
option.value = account.id;
option.textContent = `${account.id} - ${account.name}`;
if (account.id === currentValue) {
option.selected = true;
}
exportSelect.appendChild(option);
});
}
if (importSelect) {
const currentValues = Array.from(importSelect.selectedOptions).map(opt => opt.value);
importSelect.innerHTML = "";
allAccountsData.forEach(account => {
const option = document.createElement("option");
option.value = account.id;
option.textContent = `${account.id} - ${account.name}`;
if (currentValues.includes(account.id)) {
option.selected = true;
}
importSelect.appendChild(option);
});
}
}
createTabs() {
const tabContainer = document.createElement("div");
tabContainer.style.display = "flex";
tabContainer.style.width = "100%";
tabContainer.style.marginBottom = "15px";
tabContainer.style.borderBottom = "2px solid #333";
const exportTab = document.createElement("button");
exportTab.id = "ywbExportTab";
exportTab.textContent = "Export";
exportTab.style.flex = "1";
exportTab.style.padding = "10px";
exportTab.style.border = "none";
exportTab.style.background = "none";
exportTab.style.cursor = "pointer";
exportTab.style.fontSize = "14px";
exportTab.style.fontWeight = "bold";
exportTab.style.borderBottom = "3px solid #333";
const importTab = document.createElement("button");
importTab.id = "ywbImportTab";
importTab.textContent = "Import";
importTab.style.flex = "1";
importTab.style.padding = "10px";
importTab.style.border = "none";
importTab.style.background = "none";
importTab.style.cursor = "pointer";
importTab.style.fontSize = "14px";
importTab.style.fontWeight = "bold";
exportTab.onclick = () => {
exportTab.style.borderBottom = "3px solid #333";
importTab.style.borderBottom = "none";
document.getElementById("ywbExportTabContent").style.display = "block";
document.getElementById("ywbImportTabContent").style.display = "none";
};
importTab.onclick = () => {
importTab.style.borderBottom = "3px solid #333";
exportTab.style.borderBottom = "none";
document.getElementById("ywbExportTabContent").style.display = "none";
document.getElementById("ywbImportTabContent").style.display = "block";
};
tabContainer.appendChild(exportTab);
tabContainer.appendChild(importTab);
return tabContainer;
}
createLogArea() {
const logContainer = document.createElement("div");
logContainer.style.width = "100%";
logContainer.style.marginTop = "15px";
logContainer.style.borderTop = "2px solid #333";
logContainer.style.paddingTop = "10px";
const logLabel = document.createElement("div");
logLabel.textContent = "Log:";
logLabel.style.fontSize = "12px";
logLabel.style.fontWeight = "bold";
logLabel.style.marginBottom = "5px";
this.logArea = document.createElement("div");
this.logArea.id = "ywbLogArea";
this.logArea.style.width = "100%";
this.logArea.style.height = "120px";
this.logArea.style.overflowY = "auto";
this.logArea.style.backgroundColor = "#f5f5f5";
this.logArea.style.border = "1px solid #ccc";
this.logArea.style.borderRadius = "5px";
this.logArea.style.padding = "8px";
this.logArea.style.fontSize = "11px";
this.logArea.style.fontFamily = "monospace";
this.logArea.style.lineHeight = "1.4";
logContainer.appendChild(logLabel);
logContainer.appendChild(this.logArea);
return logContainer;
}
log(message, type = "info") {
if (!this.logArea) return;
const logEntry = document.createElement("div");
logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
if (type === "error") {
logEntry.style.color = "red";
} else if (type === "success") {
logEntry.style.color = "green";
} else if (type === "warning") {
logEntry.style.color = "orange";
}
this.logArea.appendChild(logEntry);
this.logArea.scrollTop = this.logArea.scrollHeight;
}
clearLog() {
if (this.logArea) {
this.logArea.innerHTML = "";
}
}
show() {
const div = this.createDiv();
// Create tabs
const tabs = this.createTabs();
div.appendChild(tabs);
// Export Tab Content
const exportTabContent = document.createElement("div");
exportTabContent.id = "ywbExportTabContent";
exportTabContent.style.width = "100%";
exportTabContent.style.display = "block";
const exportDropdown = this.createExportAccountDropdown();
const presetDropdown = this.createPresetDropdown();
const exportButton = this.createButton("export-btn", "Export Column Preset to JSON", async () => {
if (!this.selectedExportAccountId) {
alert("Please select an account to export from.");
return;
}
if (!this.selectedPreset) {
alert("Please select a preset to export.");
return;
}
await exportColumnPreset(this.selectedPreset, this.accountSizes, this.selectedExportAccountId);
});
exportTabContent.appendChild(exportDropdown);
exportTabContent.appendChild(presetDropdown);
exportTabContent.appendChild(exportButton);
// Import Tab Content
const importTabContent = document.createElement("div");
importTabContent.id = "ywbImportTabContent";
importTabContent.style.width = "100%";
importTabContent.style.display = "none";
const importDropdown = this.createImportAccountDropdown();
const importSizesCheckbox = document.createElement("div");
importSizesCheckbox.style.display = "flex";
importSizesCheckbox.style.alignItems = "center";
importSizesCheckbox.style.margin = "10px 0";
importSizesCheckbox.style.width = "100%";
const sizesCheckbox = document.createElement("input");
sizesCheckbox.type = "checkbox";
sizesCheckbox.id = "ywbImportSizes";
sizesCheckbox.style.marginRight = "10px";
const sizesLabel = document.createElement("label");
sizesLabel.htmlFor = "ywbImportSizes";
sizesLabel.textContent = "Also import column sizes";
sizesLabel.style.fontSize = "14px";
importSizesCheckbox.appendChild(sizesCheckbox);
importSizesCheckbox.appendChild(sizesLabel);
const importButton = this.createButton("import-btn", "Import Column Preset to Selected Accounts", async () => {
if (!this.selectedImportAccountIds || this.selectedImportAccountIds.length === 0) {
alert("Please select at least one account to import to.");
return;
}
const fileHelper = new FileHelper();
const fileSelector = new FileSelector(file => fileHelper.readFileAsJsonAsync(file));
try {
logger.info("Opening file selector...");
const presetContent = await fileSelector.show();
if (!presetContent || !presetContent.preset) {
logger.error("Invalid file format. Expected a JSON file with 'preset' object.");
return;
}
await importPresetToSelectedAccounts(this.selectedImportAccountIds, presetContent, this);
// Import sizes if checkbox is checked
const importSizes = document.getElementById("ywbImportSizes").checked;
if (importSizes && presetContent.sizes && presetContent.sizes.length > 0) {
for (const accountId of this.selectedImportAccountIds) {
await importSizesToAccount(accountId, presetContent.sizes);
}
}
// Only ask for reload if current account was in the import list
const currentAccountId = require("BusinessUnifiedNavigationContext").adAccountID;
if (this.selectedImportAccountIds.includes(currentAccountId)) {
if (confirm("Column presets in current account changed, reload?")) {
location.reload();
}
}
logger.success("Import complete!");
} catch (error) {
logger.error(`Error: ${error}`);
}
});
importTabContent.appendChild(importDropdown);
importTabContent.appendChild(importSizesCheckbox);
importTabContent.appendChild(importButton);
// Add tab contents to div
div.appendChild(exportTabContent);
div.appendChild(importTabContent);
// Add log area
const logArea = this.createLogArea();
div.appendChild(logArea);
// Create a small link for copying as bookmark
const copyBookmarkLink = document.createElement("a");
copyBookmarkLink.href = "#";
copyBookmarkLink.textContent = "Copy as bookmark";
copyBookmarkLink.style.fontSize = "12px";
copyBookmarkLink.style.color = "blue";
copyBookmarkLink.style.textDecoration = "underline";
copyBookmarkLink.style.cursor = "pointer";
copyBookmarkLink.style.marginTop = "10px";
copyBookmarkLink.style.display = "block";
copyBookmarkLink.style.textAlign = "center";
copyBookmarkLink.onclick = (e) => {
e.preventDefault();
copyScriptAsBase64Bookmarklet();
};
div.appendChild(copyBookmarkLink);
// Add div to body
document.body.appendChild(div);
// Initial log message
this.log("UI initialized. Ready to work.", "success");
}
}
// ============================================
// Main function to show the column presets manager UI
// ============================================
async function showColumnPresetsManager() {
try {
// Show loading message
const loadingDiv = document.createElement("div");
loadingDiv.style.position = "fixed";
loadingDiv.style.top = "50%";
loadingDiv.style.left = "50%";
loadingDiv.style.transform = "translate(-50%, -50%)";
loadingDiv.style.padding = "20px";
loadingDiv.style.backgroundColor = "yellow";
loadingDiv.style.borderRadius = "10px";
loadingDiv.style.zIndex = "1000";
loadingDiv.style.fontSize = "16px";
loadingDiv.style.fontWeight = "bold";
loadingDiv.textContent = "Loading accounts...";
document.body.appendChild(loadingDiv);
// Load all accounts
await accountManager.loadAll();
// Remove loading message
document.body.removeChild(loadingDiv);
// Show UI
const ui = new ColumnPresetsManagerUI();
logger.setUI(ui);
ui.show();
} catch (error) {
console.error("Error loading accounts:", error);
alert(`Error loading accounts: ${error.message || error}`);
}
}
// Function to copy the script as base64 bookmarklet
function copyScriptAsBase64Bookmarklet() {
try {
const configStr = `const Config = ${JSON.stringify(Config)};`;
const scriptContent = `// FB Column Preset Manager ${Config.VERSION}
${configStr}
${Logger.toString()}
const logger = new Logger();
${FileHelper.toString()}
${FileSelector.toString()}
${FbApi.toString()}
const API = new FbApi();
${AccountManager.toString()}
const accountManager = new AccountManager();
let allAccountsData = new Proxy({}, {
get(target, prop) {
if (typeof prop === 'symbol') return undefined;
const accounts = accountManager.getAll();
if (prop === 'length') return accounts.length;
if (prop === 'find') return accounts.find.bind(accounts);
if (prop === 'map') return accounts.map.bind(accounts);
if (prop === 'filter') return accounts.filter.bind(accounts);
if (prop === 'forEach') return accounts.forEach.bind(accounts);
return accounts[prop];
}
});
const CUSTOM_METRIC_PREFIX = "custom_derived_metrics:";
${fetchCustomMetrics.toString()}
${createCustomMetric.toString()}
${extractCustomMetricIds.toString()}
${replaceCustomMetricIds.toString()}
${fetchAccountPresets.toString()}
${exportColumnPreset.toString()}
${fetchUserSettingsId.toString()}
${uploadPreset.toString()}
${setDefaultColumnPreset.toString()}
${uploadSize.toString()}
${importPresetToAccount.toString()}
${importPresetToSelectedAccounts.toString()}
${importSizesToAccount.toString()}
${reloadPageWithPreset.toString()}
${ColumnPresetsManagerUI.toString()}
${showColumnPresetsManager.toString()}
${copyScriptAsBase64Bookmarklet.toString()}
window.showColumnPresetsManager = showColumnPresetsManager;
window.copyScriptAsBase64Bookmarklet = copyScriptAsBase64Bookmarklet;
showColumnPresetsManager();`;
const base64Content = btoa(unescape(encodeURIComponent(scriptContent)));
const bookmarkletCode = `javascript:eval(decodeURIComponent(escape(atob("${base64Content}"))));`;
navigator.clipboard.writeText(bookmarkletCode)
.then(() => {
alert("Bookmarklet copied to clipboard!");
})
.catch(err => {
console.error('Failed to copy: ', err);
const textArea = document.createElement("textarea");
textArea.value = bookmarkletCode;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
alert("Bookmarklet copied to clipboard!");
});
} catch (error) {
console.error('Error creating bookmarklet:', error);
alert(`Error creating bookmarklet: ${error.message}`);
}
}
// Make the functions available globally
window.showColumnPresetsManager = showColumnPresetsManager;
window.copyScriptAsBase64Bookmarklet = copyScriptAsBase64Bookmarklet;
// Auto-run when script is loaded
showColumnPresetsManager();
@dvygolov
Copy link
Author

javascript:eval("(async () => {" + atob("Y2xhc3MgRmlsZVNlbGVjdG9yIHsKICBjb25zdHJ1Y3RvcihmaWxlUHJvY2Vzc29yKSB7CiAgICB0aGlzLmZpbGVQcm9jZXNzb3IgPSBmaWxlUHJvY2Vzc29yOwogIH0KCiAgY3JlYXRlRGl2KCkgewogICAgdGhpcy5kaXYgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJkaXYiKTsKICAgIHRoaXMuZGl2LnN0eWxlLnBvc2l0aW9uID0gImZpeGVkIjsKICAgIHRoaXMuZGl2LnN0eWxlLnRvcCA9ICI1MCUiOwogICAgdGhpcy5kaXYuc3R5bGUubGVmdCA9ICI1MCUiOwogICAgdGhpcy5kaXYuc3R5bGUudHJhbnNmb3JtID0gInRyYW5zbGF0ZSgtNTAlLCAtNTAlKSI7CiAgICB0aGlzLmRpdi5zdHlsZS53aWR0aCA9ICIyMDBweCI7IC8vIFVwZGF0ZWQgd2lkdGgKICAgIHRoaXMuZGl2LnN0eWxlLmhlaWdodCA9ICIxMjBweCI7CiAgICB0aGlzLmRpdi5zdHlsZS5iYWNrZ3JvdW5kQ29sb3IgPSAieWVsbG93IjsKICAgIHRoaXMuZGl2LnN0eWxlLnpJbmRleCA9ICIxMDAwIjsKICAgIHRoaXMuZGl2LnN0eWxlLmRpc3BsYXkgPSAiZmxleCI7CiAgICB0aGlzLmRpdi5zdHlsZS5mbGV4RGlyZWN0aW9uID0gImNvbHVtbiI7IC8vIFRvIGFsaWduIGl0ZW1zIHZlcnRpY2FsbHkKICAgIHRoaXMuZGl2LnN0eWxlLmFsaWduSXRlbXMgPSAiY2VudGVyIjsKICAgIHRoaXMuZGl2LnN0eWxlLmp1c3RpZnlDb250ZW50ID0gImNlbnRlciI7CiAgICB0aGlzLmRpdi5zdHlsZS5wYWRkaW5nID0gIjEwcHgiOyAvLyBBZGRlZCBwYWRkaW5nCiAgICB0aGlzLmRpdi5zdHlsZS5ib3hTaXppbmcgPSAiYm9yZGVyLWJveCI7IC8vIFRvIGluY2x1ZGUgcGFkZGluZyBpbiB3aWR0aC9oZWlnaHQKICAgIHRoaXMuZGl2LnN0eWxlLmJvcmRlclJhZGl1cyA9ICIxMHB4IjsKCiAgICAvLyBDcmVhdGUgYW5kIHN0eWxlIHRoZSB0aXRsZQogICAgdmFyIHRpdGxlID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiZGl2Iik7CiAgICB0aXRsZS5pbm5lckhUTUwgPQogICAgICAiPGJyPkZCIENvbHVtbiBQcmVzZXQgTWFuYWdlciB2Mi4xPGJyPlRFQU0gRWRpdGlvbjxicj5ieSBZZWxsb3cgV2ViPGJyPjxicj4iOwogICAgdGl0bGUuc3R5bGUudGV4dEFsaWduID0gImNlbnRlciI7CiAgICB0aXRsZS5zdHlsZS5mb250V2VpZ2h0ID0gImJvbGQiOwoKICAgIC8vIENyZWF0ZSBhbmQgc3R5bGUgdGhlIGNsb3NlIGJ1dHRvbgogICAgdmFyIGNsb3NlQnV0dG9uID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiYnV0dG9uIik7CiAgICBjbG9zZUJ1dHRvbi5pbm5lckhUTUwgPSAiWCI7CiAgICBjbG9zZUJ1dHRvbi5zdHlsZS5wb3NpdGlvbiA9ICJhYnNvbHV0ZSI7CiAgICBjbG9zZUJ1dHRvbi5zdHlsZS50b3AgPSAiNXB4IjsKICAgIGNsb3NlQnV0dG9uLnN0eWxlLnJpZ2h0ID0gIjVweCI7CiAgICBjbG9zZUJ1dHRvbi5zdHlsZS5ib3JkZXIgPSAibm9uZSI7CiAgICBjbG9zZUJ1dHRvbi5zdHlsZS5iYWNrZ3JvdW5kID0gIm5vbmUiOwogICAgY2xvc2VCdXR0b24uc3R5bGUuY3Vyc29yID0gInBvaW50ZXIiOwogICAgY2xvc2VCdXR0b24ub25jbGljayA9ICgpID0+IHsKICAgICAgZG9jdW1lbnQuYm9keS5yZW1vdmVDaGlsZCh0aGlzLmRpdik7CiAgICB9OwoKICAgIHRoaXMuZGl2LmFwcGVuZENoaWxkKHRpdGxlKTsKICAgIHRoaXMuZGl2LmFwcGVuZENoaWxkKGNsb3NlQnV0dG9uKTsKICB9CgogIGNyZWF0ZUZpbGVJbnB1dCgpIHsKICAgIC8vIENyZWF0ZSB0aGUgZmlsZSBpbnB1dCBhbmQgaGFuZGxlIGZpbGUgc2VsZWN0aW9uCiAgICB0aGlzLmZpbGVJbnB1dCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImlucHV0Iik7CiAgICB0aGlzLmZpbGVJbnB1dC50eXBlID0gImZpbGUiOwogICAgdGhpcy5maWxlSW5wdXQuYWNjZXB0ID0gIi5qc29uIjsgLy8gQWNjZXB0IG9ubHkgSlNPTiBmaWxlcwogICAgdGhpcy5maWxlSW5wdXQuc3R5bGUuZGlzcGxheSA9ICJub25lIjsKICB9CgogIGNyZWF0ZUJ1dHRvbigpIHsKICAgIC8vIENyZWF0ZSB0aGUgYnV0dG9uCiAgICB0aGlzLmJ1dHRvbiA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImJ1dHRvbiIpOwogICAgdGhpcy5idXR0b24udGV4dENvbnRlbnQgPSAiT3BlbiBQcmVzZXQiOwogICAgdGhpcy5idXR0b24ub25jbGljayA9ICgpID0+IHsKICAgICAgdGhpcy5maWxlSW5wdXQuY2xpY2soKTsKICAgIH07CiAgfQoKICBzaG93KCkgewogICAgcmV0dXJuIG5ldyBQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHsKICAgICAgdGhpcy5jcmVhdGVEaXYoKTsKICAgICAgdGhpcy5jcmVhdGVGaWxlSW5wdXQoKTsKICAgICAgdGhpcy5jcmVhdGVCdXR0b24oKTsKCiAgICAgIC8vIEFwcGVuZCBlbGVtZW50cyB0byB0aGUgZGl2IGFuZCB0aGUgZGl2IHRvIHRoZSBib2R5CiAgICAgIHRoaXMuZGl2LmFwcGVuZENoaWxkKHRoaXMuYnV0dG9uKTsKICAgICAgdGhpcy5kaXYuYXBwZW5kQ2hpbGQodGhpcy5maWxlSW5wdXQpOwogICAgICBkb2N1bWVudC5ib2R5LmFwcGVuZENoaWxkKHRoaXMuZGl2KTsKCiAgICAgIHRoaXMuZmlsZUlucHV0Lm9uY2hhbmdlID0gYXN5bmMgKCkgPT4gewogICAgICAgIC8vIElmIG5vIGZpbGUgaXMgc2VsZWN0ZWQgKHVzZXIgY2FuY2VsbGVkKQogICAgICAgIGlmICghdGhpcy5maWxlSW5wdXQuZmlsZXMgfHwgdGhpcy5maWxlSW5wdXQuZmlsZXMubGVuZ3RoID09PSAwKSB7CiAgICAgICAgICBkb2N1bWVudC5ib2R5LnJlbW92ZUNoaWxkKHRoaXMuZGl2KTsKICAgICAgICAgIGFsZXJ0KCJPcGVyYXRpb24gY2FuY2VsZWQiKTsKICAgICAgICAgIHJlamVjdCgiRmlsZSBzZWxlY3Rpb24gY2FuY2VsbGVkIGJ5IHVzZXIiKTsKICAgICAgICAgIHJldHVybjsKICAgICAgICB9CgogICAgICAgIHRyeSB7CiAgICAgICAgICAvLyBQcm9jZXNzIHRoZSBmaWxlIGFuZCByZXNvbHZlIHRoZSBwcm9taXNlCiAgICAgICAgICBjb25zdCByZXN1bHQgPSBhd2FpdCB0aGlzLmZpbGVQcm9jZXNzb3IodGhpcy5maWxlSW5wdXQuZmlsZXNbMF0pOwogICAgICAgICAgZG9jdW1lbnQuYm9keS5yZW1vdmVDaGlsZCh0aGlzLmRpdik7CiAgICAgICAgICByZXNvbHZlKHJlc3VsdCk7CiAgICAgICAgfSBjYXRjaCAoZXJyb3IpIHsKICAgICAgICAgIC8vIEhhbmRsZSBhbnkgZXJyb3JzIGluIHByb2Nlc3NpbmcKICAgICAgICAgIGRvY3VtZW50LmJvZHkucmVtb3ZlQ2hpbGQodGhpcy5kaXYpOwogICAgICAgICAgcmVqZWN0KGVycm9yKTsKICAgICAgICB9CiAgICAgIH07CiAgICB9KTsKICB9Cn0KCmNsYXNzIEZpbGVIZWxwZXIgewogIGFzeW5jIHJlYWRGaWxlQXNKc29uQXN5bmMoZmlsZSkgewogICAgdHJ5IHsKICAgICAgY29uc3QgZmlsZUNvbnRlbnQgPSBhd2FpdCB0aGlzLnJlYWRGaWxlQXN5bmMoZmlsZSk7CiAgICAgIHJldHVybiBKU09OLnBhcnNlKGZpbGVDb250ZW50KTsKICAgIH0gY2F0Y2ggKGVycm9yKSB7CiAgICAgIGNvbnNvbGUuZXJyb3IoIkVycm9yOiIsIGVycm9yKTsKICAgICAgdGhyb3cgZXJyb3I7CiAgICB9CiAgfQoKICByZWFkRmlsZUFzeW5jKGZpbGUpIHsKICAgIHJldHVybiBuZXcgUHJvbWlzZSgocmVzb2x2ZSwgcmVqZWN0KSA9PiB7CiAgICAgIGxldCByZWFkZXIgPSBuZXcgRmlsZVJlYWRlcigpOwoKICAgICAgcmVhZGVyLm9ubG9hZCA9ICgpID0+IHsKICAgICAgICByZXNvbHZlKHJlYWRlci5yZXN1bHQpOwogICAgICB9OwoKICAgICAgcmVhZGVyLm9uZXJyb3IgPSAoKSA9PiB7CiAgICAgICAgcmVqZWN0KCJFcnJvciByZWFkaW5nIGZpbGUiKTsKICAgICAgfTsKCiAgICAgIHJlYWRlci5yZWFkQXNUZXh0KGZpbGUpOyAvLyBSZWFkIHRoZSBmaWxlIGFzIHRleHQKICAgIH0pOwogIH0KfQoKY2xhc3MgRmJBcGkgewogIGFwaVVybCA9ICJodHRwczovL2Fkc21hbmFnZXItZ3JhcGguZmFjZWJvb2suY29tL3YxOC4wLyI7CiAgZGVidWcgPSBmYWxzZTsKCiAgY29uc3RydWN0b3IoZGVidWcpIHsKICAgIHRoaXMuZGVidWcgPSBkZWJ1ZzsKICB9CgogIGFzeW5jIGdldFJlcXVlc3QocGF0aCwgcXMsIHRva2VuID0gbnVsbCkgewogICAgcGF0aCA9IHBhdGguc3RhcnRzV2l0aCh0aGlzLmFwaVVybCkgPyBwYXRoIDogYCR7dGhpcy5hcGlVcmx9JHtwYXRofWA7CiAgICB0b2tlbiA9IHRva2VuID8/IF9fYWNjZXNzVG9rZW47CiAgICBsZXQgdXJsID0gcGF0aDsKICAgIGlmICghcGF0aC5pbmNsdWRlcyh0b2tlbikpIHsKICAgICAgbGV0IHFzUGFydCA9IHFzID09PSBudWxsIHx8IHFzID09PSAiIiA/ICI/IiA6IGA/JHtxc30mYDsKICAgICAgdXJsID0gYCR7cGF0aH0ke3FzUGFydH1hY2Nlc3NfdG9rZW49JHt0b2tlbn1gOwogICAgfQogICAgbGV0IGYgPSBhd2FpdCBmZXRjaCh1cmwsIHsKICAgICAgaGVhZGVyczogewogICAgICAgIGFjY2VwdDoKICAgICAgICAgICJ0ZXh0L2h0bWwsYXBwbGljYXRpb24veGh0bWwreG1sLGFwcGxpY2F0aW9uL3htbDtxPTAuOSxpbWFnZS9hdmlmLGltYWdlL3dlYnAsaW1hZ2UvYXBuZywqLyo7cT0wLjgsYXBwbGljYXRpb24vc2lnbmVkLWV4Y2hhbmdlO3Y9YjM7cT0wLjkiLAogICAgICAgICJhY2NlcHQtbGFuZ3VhZ2UiOiAiY2EtRVMsY2E7cT0wLjksZW4tVVM7cT0wLjgsZW47cT0wLjciLAogICAgICAgICJjYWNoZS1jb250cm9sIjogIm1heC1hZ2U9MCIsCiAgICAgICAgInNlYy1jaC11YSI6CiAgICAgICAgICAnIk5vdD9BX0JyYW5kIjt2PSI4IiwgIkNocm9taXVtIjt2PSIxMDgiLCAiR29vZ2xlIENocm9tZSI7dj0iMTA4IicsCiAgICAgICAgInNlYy1jaC11YS1tb2JpbGUiOiAiPzAiLAogICAgICAgICJzZWMtY2gtdWEtcGxhdGZvcm0iOiAnIldpbmRvd3MiJywKICAgICAgICAic2VjLWZldGNoLWRlc3QiOiAiZW1wdHkiLAogICAgICAgICJzZWMtZmV0Y2gtbW9kZSI6ICJjb3JzIiwKICAgICAgICAic2VjLWZldGNoLXNpdGUiOiAic2FtZS1zaXRlIiwKICAgICAgfSwKICAgICAgcmVmZXJyZXJQb2xpY3k6ICJzdHJpY3Qtb3JpZ2luLXdoZW4tY3Jvc3Mtb3JpZ2luIiwKICAgICAgYm9keTogbnVsbCwKICAgICAgbWV0aG9kOiAiR0VUIiwKICAgICAgbW9kZTogImNvcnMiLAogICAgICBjcmVkZW50aWFsczogImluY2x1ZGUiLAogICAgICByZWZlcnJlcjogImh0dHBzOi8vYnVzaW5lc3MuZmFjZWJvb2suY29tLyIsCiAgICAgIHJlZmVycmVyUG9saWN5OiAib3JpZ2luLXdoZW4tY3Jvc3Mtb3JpZ2luIiwKICAgIH0pOwogICAgbGV0IGpzb24gPSBhd2FpdCBmLmpzb24oKTsKICAgIGlmICh0aGlzLmRlYnVnKSBjb25zb2xlLmxvZyhKU09OLnN0cmluZ2lmeShqc29uKSk7CiAgICByZXR1cm4ganNvbjsKICB9CgogIGFzeW5jIGdldEFsbFBhZ2VzKHBhdGgsIHFzLCB0b2tlbiA9IG51bGwpIHsKICAgIGxldCBpdGVtcyA9IFtdOwogICAgbGV0IHBhZ2UgPSBhd2FpdCB0aGlzLmdldFJlcXVlc3QocGF0aCwgcXMsIHRva2VuKTsKICAgIGl0ZW1zID0gaXRlbXMuY29uY2F0KHBhZ2UuZGF0YSk7CgogICAgbGV0IGkgPSAyOwogICAgd2hpbGUgKHBhZ2UucGFnaW5nICYmIHBhZ2UucGFnaW5nLm5leHQpIHsKICAgICAgaWYgKHRoaXMuZGVidWcpIGNvbnNvbGUubG9nKGBHZXR0aW5nIHBhZ2UgIyR7aX0uLi5gKTsKICAgICAgcGFnZSA9IGF3YWl0IHRoaXMuZ2V0UmVxdWVzdChwYWdlLnBhZ2luZy5uZXh0LCAiIiwgdG9rZW4pOwogICAgICBpdGVtcyA9IGl0ZW1zLmNvbmNhdChwYWdlLmRhdGEpOwogICAgICBpKys7CiAgICB9CgogICAgcmV0dXJuIGl0ZW1zOwogIH0KCiAgYXN5bmMgcG9zdFJlcXVlc3QocGF0aCwgYm9keSwgdG9rZW4gPSBudWxsKSB7CiAgICB0b2tlbiA9IHRva2VuID8/IF9fYWNjZXNzVG9rZW47CiAgICBib2R5WyJhY2Nlc3NfdG9rZW4iXSA9IHRva2VuOwogICAgbGV0IGhlYWRlcnMgPSB7CiAgICAgIGFjY2VwdDogIiovKiIsCiAgICAgICJhY2NlcHQtbGFuZ3VhZ2UiOiAiZW4tVVMsZW47cT0wLjkiLAogICAgICAiY29udGVudC10eXBlIjogImFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCIsCiAgICAgICJzZWMtY2gtdWEiOgogICAgICAgICciR29vZ2xlIENocm9tZSI7dj0iMTA3IiwgIkNocm9taXVtIjt2PSIxMDciLCAiTm90PUE/QnJhbmQiO3Y9IjI0IicsCiAgICAgICJzZWMtY2gtdWEtbW9iaWxlIjogIj8wIiwKICAgICAgInNlYy1jaC11YS1wbGF0Zm9ybSI6ICciV2luZG93cyInLAogICAgICAic2VjLWZldGNoLWRlc3QiOiAiZW1wdHkiLAogICAgICAic2VjLWZldGNoLW1vZGUiOiAiY29ycyIsCiAgICAgICJzZWMtZmV0Y2gtc2l0ZSI6ICJzYW1lLXNpdGUiLAogICAgfTsKICAgIGxldCBmID0gYXdhaXQgZmV0Y2goYCR7dGhpcy5hcGlVcmx9JHtwYXRofWAsIHsKICAgICAgaGVhZGVyczogaGVhZGVycywKICAgICAgcmVmZXJyZXI6ICJodHRwczovL2J1c2luZXNzLmZhY2Vib29rLmNvbS8iLAogICAgICByZWZlcnJlclBvbGljeTogIm9yaWdpbi13aGVuLWNyb3NzLW9yaWdpbiIsCiAgICAgIGJvZHk6IG5ldyBVUkxTZWFyY2hQYXJhbXMoYm9keSkudG9TdHJpbmcoKSwKICAgICAgbWV0aG9kOiAiUE9TVCIsCiAgICAgIG1vZGU6ICJjb3JzIiwKICAgICAgY3JlZGVudGlhbHM6ICJpbmNsdWRlIiwKICAgIH0pOwogICAgbGV0IGpzb24gPSBhd2FpdCBmLmpzb24oKTsKICAgIGlmICh0aGlzLmRlYnVnKSBjb25zb2xlLmxvZyhKU09OLnN0cmluZ2lmeShqc29uKSk7CiAgICByZXR1cm4ganNvbjsKICB9Cn0KCmNvbnN0IERFQlVHID0gdHJ1ZTsKY29uc3QgQVBJID0gbmV3IEZiQXBpKERFQlVHKTsKCmFzeW5jIGZ1bmN0aW9uIG1haW4oKSB7CiAgY29uc3QgY2hvaWNlID0gcHJvbXB0KAogICAgYFNlbGVjdCBhbiBvcHRpb246CgoxLiBFeHBvcnQgY29sdW1uIHByZXNldAoyLiBJbXBvcnQgY29sdW1uIHByZXNldCB0byB0aGlzIGFjY291bnQKMy4gSW1wb3J0IGNvbHVtbiBwcmVzZXQgdG8gQUxMIGFjY291bnRzYCwKICApOwoKICB2YXIgZmggPSBuZXcgRmlsZUhlbHBlcigpOwogIHRyeSB7CiAgICBzd2l0Y2ggKGNob2ljZSkgewogICAgICBjYXNlICIxIjoKICAgICAgICBhd2FpdCBleHBvcnRDb2x1bW5QcmVzZXQoKTsKICAgICAgICBhbGVydCgiRXhwb3J0IGNvbXBsZXRlISIpOwogICAgICAgIGJyZWFrOwogICAgICBjYXNlICIyIjoKICAgICAgICB2YXIgZmlsZVNlbGVjdG9yID0gbmV3IEZpbGVTZWxlY3RvcihmaC5yZWFkRmlsZUFzSnNvbkFzeW5jLmJpbmQoZmgpKTsKICAgICAgICBkZWJ1ZyhgT3BlbmluZyBhbmQgcmVhZGluZyBwcmVzZXQgZmlsZS4uLmApOwogICAgICAgIHZhciBwcmVzZXRDb250ZW50ID0gYXdhaXQgZmlsZVNlbGVjdG9yLnNob3coKTsKICAgICAgICBkZWJ1ZyhgR290IHByZXNldCBmaWxlIGNvbnRlbnQhYCk7CgogICAgICAgIGNvbnN0IHVzZXJTZXR0aW5nc0lkID0gYXdhaXQgZmV0Y2hVc2VyU2V0dGluZ3NJZCgpOwogICAgICAgIGxldCBwcmVzZXRJZCA9IGF3YWl0IHVwbG9hZFByZXNldCh1c2VyU2V0dGluZ3NJZCwgcHJlc2V0Q29udGVudCk7CiAgICAgICAgYXdhaXQgc2V0RGVmYXVsdENvbHVtblByZXNldChudWxsLCBwcmVzZXRJZCk7CiAgICAgICAgYWxlcnQoIkltcG9ydCBjb21wbGV0ZSEiKTsKICAgICAgICBicmVhazsKICAgICAgY2FzZSAiMyI6CiAgICAgICAgdmFyIGZpbGVTZWxlY3RvciA9IG5ldyBGaWxlU2VsZWN0b3IoZmgucmVhZEZpbGVBc0pzb25Bc3luYy5iaW5kKGZoKSk7CiAgICAgICAgZGVidWcoYE9wZW5pbmcgYW5kIHJlYWRpbmcgcHJlc2V0IGZpbGUuLi5gKTsKICAgICAgICB2YXIgcHJlc2V0Q29udGVudCA9IGF3YWl0IGZpbGVTZWxlY3Rvci5zaG93KCk7CiAgICAgICAgZGVidWcoYEdvdCBwcmVzZXQgZmlsZSBjb250ZW50IWApOwoKICAgICAgICBsZXQgYWRBY2NvdW50cyA9IGF3YWl0IGdldEFsbEFkQWNjb3VudHMoKTsKICAgICAgICBkZWJ1ZyhgVG90YWwgYWNjb3VudCBjb3VudCBpczogJHthZEFjY291bnRzLmxlbmd0aH0uYCk7CiAgICAgICAgZm9yIChsZXQgaSA9IDA7IGkgPCBhZEFjY291bnRzLmxlbmd0aDsgaSsrKSB7CiAgICAgICAgICBkZWJ1ZyhgUHJvY2Vzc2luZyBhY2NvdW50ICMke2l9IC0gJHthZEFjY291bnRzW2ldfS4uLmApOwogICAgICAgICAgY29uc3QgdXNlclNldHRpbmdzSWQgPSBhd2FpdCBmZXRjaFVzZXJTZXR0aW5nc0lkKGFkQWNjb3VudHNbaV0pOwogICAgICAgICAgbGV0IHByZXNldElkID0gYXdhaXQgdXBsb2FkUHJlc2V0KHVzZXJTZXR0aW5nc0lkLCBwcmVzZXRDb250ZW50KTsKICAgICAgICAgIGF3YWl0IHNldERlZmF1bHRDb2x1bW5QcmVzZXQoYWRBY2NvdW50c1tpXSwgcHJlc2V0SWQpOwogICAgICAgIH0KICAgICAgICBhbGVydCgiSW1wb3J0IGNvbXBsZXRlISIpOwogICAgICAgIGJyZWFrOwogICAgICBkZWZhdWx0OgogICAgICAgIGFsZXJ0KCJJbnZhbGlkIG9wdGlvbiEiKTsKICAgICAgICBicmVhazsKICAgIH0KICB9IGNhdGNoIChlcnJvcikgewogICAgY29uc29sZS5lcnJvcigiRXJyb3I6IiwgZXJyb3IpOwogIH0KfQoKYXN5bmMgZnVuY3Rpb24gZXhwb3J0Q29sdW1uUHJlc2V0KCkgewogIGxldCBhZEFjY291bnRJZCA9IHJlcXVpcmUoIkJ1c2luZXNzVW5pZmllZE5hdmlnYXRpb25Db250ZXh0IikuYWRBY2NvdW50SUQ7CiAgbGV0IGpzID0gYXdhaXQgQVBJLmdldFJlcXVlc3QoCiAgICBgYWN0XyR7YWRBY2NvdW50SWR9YCwKICAgIGBmaWVsZHM9WyJ1c2VyX3NldHRpbmdze2lkLGNvbHVtbl9wcmVzZXRze2F0dHJpYnV0aW9uX3dpbmRvd3MsY29sdW1ucyxpZCxuYW1lLHRpbWVfY3JlYXRlZCx0aW1lX3VwZGF0ZWR9fSJdYCwKICApOwogIGNvbnN0IHByZXNldHMgPSBqcy51c2VyX3NldHRpbmdzLmNvbHVtbl9wcmVzZXRzLmRhdGE7CiAgaWYgKHByZXNldHMubGVuZ3RoID09PSAwKSB7CiAgICBhbGVydCgiTm8gcHJlc2V0cyBhdmFpbGFibGUiKTsKICAgIHJldHVybjsKICB9CgogIGxldCBwcmVzZXRMaXN0ID0gIlNlbGVjdCBhIHByZXNldCBieSBudW1iZXI6XG4iOwogIHByZXNldHMuZm9yRWFjaCgocHJlc2V0LCBpbmRleCkgPT4gewogICAgcHJlc2V0TGlzdCArPSBgJHtpbmRleCArIDF9LiAke3ByZXNldC5uYW1lfVxuYDsKICB9KTsKCiAgY29uc3Qgc2VsZWN0ZWROdW1iZXIgPSBwYXJzZUludChwcm9tcHQocHJlc2V0TGlzdCksIDEwKTsKICBpZiAoc2VsZWN0ZWROdW1iZXIgPCAxIHx8IHNlbGVjdGVkTnVtYmVyID4gcHJlc2V0cy5sZW5ndGgpIHsKICAgIGFsZXJ0KCJJbnZhbGlkIHNlbGVjdGlvbiIpOwogICAgcmV0dXJuOwogIH0KCiAgY29uc3Qgc2VsZWN0ZWRQcmVzZXQgPSBwcmVzZXRzW3NlbGVjdGVkTnVtYmVyIC0gMV07CiAgY29uc3QgYmxvYiA9IG5ldyBCbG9iKFtKU09OLnN0cmluZ2lmeShzZWxlY3RlZFByZXNldCldLCB7CiAgICB0eXBlOiAiYXBwbGljYXRpb24vanNvbiIsCiAgfSk7CiAgY29uc3QgYSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImEiKTsKICBhLmhyZWYgPSBVUkwuY3JlYXRlT2JqZWN0VVJMKGJsb2IpOwogIGEuZG93bmxvYWQgPSBgJHtzZWxlY3RlZFByZXNldC5uYW1lfS5qc29uYDsKICBhLmNsaWNrKCk7Cn0KCmFzeW5jIGZ1bmN0aW9uIGZldGNoVXNlclNldHRpbmdzSWQoYWRBY2NvdW50SWQpIHsKICBsZXQgYWNjSWQgPQogICAgYWRBY2NvdW50SWQgPz8gcmVxdWlyZSgiQnVzaW5lc3NVbmlmaWVkTmF2aWdhdGlvbkNvbnRleHQiKS5hZEFjY291bnRJRDsKICBkZWJ1ZyhgR2V0dGluZyB1c2VyIHNldHRpbmdzIGZvciBhY2MgJHthY2NJZH0uLi5gKTsKICBsZXQganMgPSBhd2FpdCBBUEkuZ2V0UmVxdWVzdChgYWN0XyR7YWNjSWR9YCwgYGZpZWxkcz1bXWApOwogIGxldCB1c0lkID0ganM/LnVzZXJfc2V0dGluZ3M/LmlkOwogIGlmICh1c0lkID09IG51bGwpIHsKICAgIGRlYnVnKGBObyBkZWZhdWx0IHVzZXIgc2V0dGluZ3MgZm91bmQhIENyZWF0aW5nIHRoZW0uLi5gKTsKICAgIGpzID0gYXdhaXQgQVBJLmdldFJlcXVlc3QoYGFjdF8ke2FjY0lkfS91c2VyX3NldHRpbmdzYCwgYG1ldGhvZD1wb3N0YCk7CiAgICB1c0lkID0ganMuaWQ7CiAgfQogIHJldHVybiB1c0lkOwp9Cgphc3luYyBmdW5jdGlvbiB1cGxvYWRQcmVzZXQodXNlclNldHRpbmdzSWQsIHByZXNldERhdGEpIHsKICBsZXQgZGF0YSA9IHsKICAgIG5hbWU6IHByZXNldERhdGEubmFtZSwKICAgIGF0dHJpYnV0aW9uX3dpbmRvd3M6IEpTT04uc3RyaW5naWZ5KHByZXNldERhdGEuYXR0cmlidXRpb25fd2luZG93cyksCiAgICBjb2x1bW5zOiBKU09OLnN0cmluZ2lmeShwcmVzZXREYXRhLmNvbHVtbnMpLAogIH07CiAgZGVidWcoCiAgICBgVXBsb2FkaW5nIHByZXNldCAke3ByZXNldERhdGEubmFtZX0gdG8gdXNlciBzZXR0aW5ncyAke3VzZXJTZXR0aW5nc0lkfS4uLmAsCiAgKTsKICBsZXQganMgPSBhd2FpdCBBUEkucG9zdFJlcXVlc3QoYCR7dXNlclNldHRpbmdzSWR9L2NvbHVtbl9wcmVzZXRzYCwgZGF0YSk7CiAgcmV0dXJuIGpzLmlkOwp9Cgphc3luYyBmdW5jdGlvbiBzZXREZWZhdWx0Q29sdW1uUHJlc2V0KGFkQWNjb3VudElkLCBwcmVzZXRJZCkgewogIGxldCBhY2NJZCA9CiAgICBhZEFjY291bnRJZCA/PyByZXF1aXJlKCJCdXNpbmVzc1VuaWZpZWROYXZpZ2F0aW9uQ29udGV4dCIpLmFkQWNjb3VudElEOwogIGRlYnVnKAogICAgYFNldHRpbmcgZGVmYXVsdCBjb2x1bW4gcHJlc2V0IGZvciBhY2MgJHthY2NJZH0sIHByZXNldCBpZCAke3ByZXNldElkfS4uLmAsCiAgKTsKICBsZXQgZGF0YSA9IHsKICAgIGRlZmF1bHRfY29sdW1uX3ByZXNldDogYHsgImlkIjogIiR7cHJlc2V0SWR9IiB9YCwKICAgIGRlZmF1bHRfY29sdW1uX3ByZXNldF9pZDogcHJlc2V0SWQsCiAgfTsKICBsZXQganMgPSBhd2FpdCBBUEkucG9zdFJlcXVlc3QoYGFjdF8ke2FjY0lkfS91c2VyX3NldHRpbmdzYCwgZGF0YSk7CiAgcmV0dXJuIGpzOwp9Cgphc3luYyBmdW5jdGlvbiBnZXRBbGxBZEFjY291bnRzKCkgewogIGxldCBhZEFjY291bnRzID0gW107CiAgZGVidWcoIkdldHRpbmcgcGVyc29uYWwgYWQgYWNjb3VudHMuLi4iKTsKICBsZXQganMgPSBhd2FpdCBBUEkuZ2V0QWxsUGFnZXMoIm1lL3BlcnNvbmFsX2FkX2FjY291bnRzIiwgImZpZWxkcz1pZCIpOwogIGFkQWNjb3VudHMgPSBqcy5tYXAoKGl0ZW0pID0+IGl0ZW0uaWQucmVwbGFjZSgiYWN0XyIsICIiKSk7CiAgZGVidWcoYEdvdCAke2FkQWNjb3VudHMubGVuZ3RofSBwZXJzb25hbCBhZCBhY2NvdW50cyFgKTsKICBkZWJ1ZygiR2V0dGluZyBidXNpbmVzcyBtYW5hZ2Vycy4uLiIpOwogIGpzID0gYXdhaXQgQVBJLmdldEFsbFBhZ2VzKCJtZS9idXNpbmVzc2VzIiwgImZpZWxkcz1pZCIpOwogIGxldCBibUlkcyA9IGpzLm1hcCgoaXRlbSkgPT4gaXRlbS5pZCk7CiAgZGVidWcoYEdvdCAke2JtSWRzLmxlbmd0aH0gYnVzaW5lc3MgbWFuYWdlcnMhYCk7CiAgZm9yIChsZXQgaSA9IDA7IGkgPCBibUlkcy5sZW5ndGg7IGkrKykgewogICAgbGV0IGJtSWQgPSBibUlkc1tpXTsKICAgIGRlYnVnKGBQcm9jZXNzaW5nIEJNICR7Ym1JZH0uLi5gKTsKICAgIGpzID0gYXdhaXQgQVBJLmdldEFsbFBhZ2VzKGAke2JtSWR9L293bmVkX2FkX2FjY291bnRzYCwgImZpZWxkcz1pZCIpOwogICAgbGV0IG93bmVkID0ganMubWFwKChpdGVtKSA9PiBpdGVtLmlkLnJlcGxhY2UoImFjdF8iLCAiIikpOwogICAgZGVidWcoYEdvdCAke293bmVkLmxlbmd0aH0gb3duZWQgYWNjb3VudHMuYCk7CiAgICBhZEFjY291bnRzID0gYWRBY2NvdW50cy5jb25jYXQob3duZWQpOwogICAganMgPSBhd2FpdCBBUEkuZ2V0QWxsUGFnZXMoYCR7Ym1JZH0vY2xpZW50X2FkX2FjY291bnRzYCwgImZpZWxkcz1pZCIpOwogICAgbGV0IGNsaWVudCA9IGpzLm1hcCgoaXRlbSkgPT4gaXRlbS5pZC5yZXBsYWNlKCJhY3RfIiwgIiIpKTsKICAgIGRlYnVnKGBHb3QgJHtjbGllbnQubGVuZ3RofSBjbGllbnQgYWNjb3VudHMuYCk7CiAgICBhZEFjY291bnRzID0gYWRBY2NvdW50cy5jb25jYXQoY2xpZW50KTsKICB9CiAgcmV0dXJuIGFkQWNjb3VudHM7Cn0KCmZ1bmN0aW9uIGRlYnVnKG1zZykgewogIGlmIChERUJVRykgY29uc29sZS5sb2cobXNnKTsKfQoKbWFpbigpOw==") + "})()");

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment