Open the Google Sheet. On the Assets tab, fill in quantities for items you want to order. Edit the Group and Order columns to update showroom grouping.
On the Styling tab, edit font, colors, and layout. For colors, fill the Color Swatch cell with your chosen color, then use VNTANA > Read Color Swatches to pull the hex value in.
Click VNTANA > Submit to VNTANA in the menu bar. n8n processes orders, group changes, and styling in one pass and updates VNTANA via the API.
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('VNTANA')
.addItem('Submit to VNTANA', 'submitToVNTANA')
.addItem('Read Color Swatches', 'readColorSwatches')
.addToUi();
const ss = SpreadsheetApp.getActiveSpreadsheet();
ss.getSheets().forEach(sheet => {
sheet.autoResizeColumns(1, sheet.getLastColumn());
});
setupStylingDropdowns(ss);
}
function setupStylingDropdowns(ss) {
const sheet = ss.getSheetByName('Styling');
if (!sheet) return;
const data = sheet.getDataRange().getValues();
const rules = SpreadsheetApp.newDataValidation;
data.forEach((row, i) => {
const field = String(row[0]).trim();
const valueCell = sheet.getRange(i + 1, 2);
if (field === 'Font Family') {
valueCell.clearDataValidations();
valueCell.setNote('Apply the font you want to this cell. The font family will be read automatically on submit.');
} else if (field === 'Image Style') {
valueCell.setDataValidation(
SpreadsheetApp.newDataValidation()
.requireValueInList(['Cover', 'Contain', 'Tile'], true)
.setAllowInvalid(false).build()
);
} else if (field === 'Products Per Row') {
valueCell.setDataValidation(
SpreadsheetApp.newDataValidation()
.requireValueInList(['1', '2', '3', '4', '5'], true)
.setAllowInvalid(false).build()
);
}
});
}
function readColorSwatches() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName('Styling');
if (!sheet) return;
const data = sheet.getDataRange().getValues();
const colorFields = ['Background Color', 'Text Color', 'Divider Color'];
data.forEach((row, i) => {
if (!colorFields.includes(String(row[0]).trim())) return;
const swatchCell = sheet.getRange(i + 1, 4); // column D
const bg = swatchCell.getBackground();
if (bg && bg !== '#ffffff' && bg !== '#000000') {
// only overwrite Value if user actually picked a non-default color
}
if (bg) sheet.getRange(i + 1, 2).setValue(bg.toUpperCase());
});
SpreadsheetApp.getUi().alert('Done!', 'Hex values copied from swatches into the Value column.', SpreadsheetApp.getUi().ButtonSet.OK);
}
function submitToVNTANA() {
const WEBHOOK = 'https://vntana.app.n8n.cloud/webhook/vntana-showroom-submit';
const ss = SpreadsheetApp.getActiveSpreadsheet();
const ui = SpreadsheetApp.getUi();
// Read Styling tab (key-value rows; skip section headers starting with —)
const stylingSheet = ss.getSheetByName('Styling');
const stylingData = stylingSheet.getDataRange().getValues();
const stylingRows = stylingData.slice(1);
const stylingMap = {};
stylingRows.forEach(r => {
if (r[0] && !String(r[0]).startsWith('—')) stylingMap[String(r[0]).trim()] = r[1];
});
// Font Family: read the actual font applied to the value cell, not its text
let fontFamily = '';
stylingData.forEach((r, i) => {
if (String(r[0]).trim() === 'Font Family') {
fontFamily = stylingSheet.getRange(i + 1, 2).getFontFamily() || '';
}
});
const shareLinkUuid = stylingMap['Share Link UUID'] || '';
// Read Assets tab — columns: Asset UUID, Asset Name, Style #, Group, Order, Quantity, Notes
const assetsSheet = ss.getSheetByName('Assets');
const assetRows = assetsSheet.getDataRange().getValues().slice(1);
const orders = assetRows
.filter(r => r[0] && Number(r[5]) > 0)
.map(r => ({
shareLinkUuid,
assetUuid: String(r[0]).trim(),
quantity: Number(r[5]),
notes: String(r[6] || '')
}));
// Build asset → group/order map for groups reconstruction
const assetGroupMap = {};
assetRows.filter(r => r[0]).forEach(r => {
assetGroupMap[String(r[0]).trim()] = {
group: String(r[3] || '').trim(),
order: Number(r[4]) || 0
};
});
// Read Groups tab — columns: Group Title, Divider Top, Divider Bottom, Visible
const groupsSheet = ss.getSheetByName('Groups');
const groups = [];
if (groupsSheet) {
const groupRows = groupsSheet.getDataRange().getValues().slice(1);
groupRows.filter(r => r[0]).forEach(r => {
const header = String(r[0]).trim();
const dividers = [];
if (String(r[1]).toLowerCase() === 'yes') dividers.push('TOP');
if (String(r[2]).toLowerCase() === 'yes') dividers.push('BOTTOM');
const visible = String(r[3]).toLowerCase() !== 'no';
const productsInfo = Object.entries(assetGroupMap)
.filter(([, info]) => info.group === header)
.sort((a, b) => a[1].order - b[1].order)
.map(([uuid, info], idx) => ({
productUuid: uuid,
visible: true,
order: info.order || idx + 1
}));
groups.push({ title: header, dividers, visible, productsInfo });
});
}
const payload = {
showroomUuid: stylingMap['Showroom UUID'] || '',
orders,
groups,
styling: {
fontFamily: fontFamily,
backgroundColor: stylingMap['Background Color'] || '',
textColor: stylingMap['Text Color'] || '',
dividerColor: stylingMap['Divider Color'] || '',
productsPerRow: stylingMap['Products Per Row'] || '',
imageStyle: stylingMap['Image Style'] || ''
}
};
try {
const response = UrlFetchApp.fetch(WEBHOOK, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
const result = JSON.parse(response.getContentText());
ui.alert(
'Done!',
'We have successfully created your new showroom.' +
(result.ordersProcessed ? ' ' + result.ordersProcessed + ' order rows submitted.' : ''),
ui.ButtonSet.OK
);
} catch(e) {
ui.alert('Error', e.message, ui.ButtonSet.OK);
}
}
In the sheet: Extensions > Apps Script → paste the code → Save → reload the sheet. The VNTANA menu appears in the menu bar.