Skip to content

Instantly share code, notes, and snippets.

@dvygolov
Last active March 21, 2026 12:56
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
Copy link
Copy Markdown

skpsky-mgmt commented Mar 21, 2026

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.

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