Skip to content

Instantly share code, notes, and snippets.

@dvygolov
Last active July 28, 2026 10:02
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: "2026.03.16",
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=id,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) {
const replaceValue = (value) => {
if (Array.isArray(value)) {
return value.map(replaceValue);
}
if (value && typeof value === "object") {
return Object.fromEntries(
Object.entries(value).map(([key, nestedValue]) => [key, replaceValue(nestedValue)])
);
}
if (typeof value === "string" && value.startsWith(CUSTOM_METRIC_PREFIX)) {
const oldId = value.replace(CUSTOM_METRIC_PREFIX, "");
const newId = idMapping[oldId];
return newId ? `${CUSTOM_METRIC_PREFIX}${newId}` : value;
}
return value;
};
return replaceValue(preset);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function waitForCustomMetrics(accountId, metricIds, maxAttempts = 10, delayMs = 1500) {
if (!metricIds || metricIds.length === 0) {
return true;
}
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const metrics = await fetchCustomMetrics(accountId);
const existingIds = new Set(metrics.map(metric => metric.id));
const missingIds = metricIds.filter(id => !existingIds.has(id));
if (missingIds.length === 0) {
logger.success(`All custom metrics are available on account ${accountId}.`);
return true;
}
logger.warning(
`Custom metrics are not fully available on account ${accountId} yet (${missingIds.length} missing, attempt ${attempt}/${maxAttempts}).`
);
if (attempt < maxAttempts) {
await sleep(delayMs);
}
}
return false;
}
// ============================================
// 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 };
const sourceCustomMetricIds = extractCustomMetricIds(presetToUpload);
if (sourceCustomMetricIds.length > 0 && (!presetContent.customMetrics || presetContent.customMetrics.length === 0)) {
throw new Error("Preset contains custom metrics, but the export file does not include their definitions.");
}
// 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 {
throw new Error(`Failed to create custom metric "${metric.name}" on account ${accountId}.`);
}
}
const missingMappings = sourceCustomMetricIds.filter(oldId => !idMapping[oldId]);
if (missingMappings.length > 0) {
throw new Error(`Missing ID mapping for ${missingMappings.length} custom metric(s).`);
}
// Replace old IDs with new IDs in preset
presetToUpload = replaceCustomMetricIds(presetToUpload, idMapping);
const newMetricIds = Object.values(idMapping);
const metricsReady = await waitForCustomMetrics(accountId, newMetricIds);
if (!metricsReady) {
throw new Error("Timed out waiting for custom metrics to become available before uploading preset.");
}
}
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();
@skpsky-mgmt

skpsky-mgmt commented Mar 21, 2026

Copy link
Copy Markdown

Where is the comment with script used in the bookmarks bar? :(
It was here earlier

@dvygolov

Copy link
Copy Markdown
Author

Where is the comment with script used in the bookmarks bar? :( It was here earlier

Just copy-paste the whole script in the browser's console and then look at the bottom of the window which will appear. There will be a link "copy as bookmark". Press it and then create the bookmark from the content of your clipboard.

@skpsky-mgmt

Copy link
Copy Markdown

Where is the comment with script used in the bookmarks bar? :( It was here earlier

Just copy-paste the whole script in the browser's console and then look at the bottom of the window which will appear. There will be a link "copy as bookmark". Press it and then create the bookmark from the content of your clipboard.

Thanks a lot!

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