From 5fa80b2e1ddfb1365d682e0938b5c599ada595d3 Mon Sep 17 00:00:00 2001 From: iisyos Date: Sun, 6 Apr 2025 11:57:40 +0900 Subject: [PATCH 1/3] Add toggle functionality for column image status --- src/components/Configure/Configure.js | 11 ++++++++++- .../Configure/SelectColumns/SelectColumns.js | 1 + .../Configure/SelectColumns/Sheet/Column/Column.js | 12 +++++++++++- .../Configure/SelectColumns/Sheet/Sheet.js | 2 ++ src/components/func/func.js | 3 +++ 5 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/components/Configure/Configure.js b/src/components/Configure/Configure.js index ef1874f..9be5f39 100644 --- a/src/components/Configure/Configure.js +++ b/src/components/Configure/Configure.js @@ -107,6 +107,15 @@ function Configure(props) { props.changeSettings(true); } + function toggleSheetIsImageHandler(sheetIdx, colIdx) { + console.log('[Configure.js] toggleSheetIsImageHandler', sheetIdx); + const meta = props.meta; + const sheet = meta[sheetIdx]; + sheet.columns[colIdx].isImage = !sheet.columns[colIdx].isImage; + props.updateMeta(meta); + props.changeSettings(true); + } + function array_move(arr, old_index, new_index) { if (new_index >= arr.length) { var k = new_index - arr.length + 1; @@ -204,7 +213,7 @@ function Configure(props) { tabs={tabs} >
{ tab === 0 ? : null } - { tab === 1 ? : null } + { tab === 1 ? : null } { tab === 2 ? : null }
diff --git a/src/components/Configure/SelectColumns/SelectColumns.js b/src/components/Configure/SelectColumns/SelectColumns.js index 325d4f9..8156ee6 100644 --- a/src/components/Configure/SelectColumns/SelectColumns.js +++ b/src/components/Configure/SelectColumns/SelectColumns.js @@ -16,6 +16,7 @@ function SelectColumns(props) { colSelect={props.colSelect} changeName={props.changeName} changeOrder={props.changeOrder} + toggleIsImage={props.toggleIsImage} /> ); diff --git a/src/components/Configure/SelectColumns/Sheet/Column/Column.js b/src/components/Configure/SelectColumns/Sheet/Column/Column.js index 51a2f94..99ae4a3 100644 --- a/src/components/Configure/SelectColumns/Sheet/Column/Column.js +++ b/src/components/Configure/SelectColumns/Sheet/Column/Column.js @@ -17,7 +17,7 @@ const useStyles = makeStyles(theme => ({ alignItems: 'center', }, column: { - flexBasis: '50%', + flexBasis: '33%', }, label: { display: 'block', @@ -106,6 +106,16 @@ function Column(props) { props.changeOrder(value)} className={classes.stepper} /> +
+
+ + + +
+
diff --git a/src/components/Configure/SelectColumns/Sheet/Sheet.js b/src/components/Configure/SelectColumns/Sheet/Sheet.js index 5423f6c..38d9d70 100644 --- a/src/components/Configure/SelectColumns/Sheet/Sheet.js +++ b/src/components/Configure/SelectColumns/Sheet/Sheet.js @@ -11,8 +11,10 @@ function Sheets(props) { key={col.index} name={col.name} rename={col.changeName} + isImage={col.isImage} selected={col.selected} select={() => props.colSelect(props.id, index)} + toggleIsImage={() => props.toggleIsImage(props.id, index)} changeName={(name) => props.changeName(props.id, index, name)} cols={props.cols} changeOrder={(newPos) => props.changeOrder(props.id, index, newPos)} diff --git a/src/components/func/func.js b/src/components/func/func.js index 0eb67c7..cc40977 100644 --- a/src/components/func/func.js +++ b/src/components/func/func.js @@ -76,6 +76,7 @@ const getSheetColumns = (sheet, existingCols, modified) => new Promise((resolve, col.dataType = columns[j].dataType; col.changeName = null; col.selected = false; + col.isImage = columns[j].isImage; cols.push(col); } for (var i = 0; i < existingCols.length; i++) { @@ -93,6 +94,7 @@ const getSheetColumns = (sheet, existingCols, modified) => new Promise((resolve, ret.selected = existingCols[eIdx].selected; ret.changeName = existingCols[eIdx].changeName; ret.order = eIdx; + ret.isImage = existingCols[eIdx].isImage; } else { ret.order = maxPos; maxPos += 1; @@ -106,6 +108,7 @@ const getSheetColumns = (sheet, existingCols, modified) => new Promise((resolve, newCol.name = columns[k].fieldName; newCol.dataType = columns[k].dataType; newCol.selected = true; + newCol.isImage = columns[k].isImage; newCol.order = k + 1; cols.push(newCol); } From d0dfefff811ad63c4b325bf568db0fe62b8579b2 Mon Sep 17 00:00:00 2001 From: iisyos Date: Sun, 6 Apr 2025 16:28:18 +0900 Subject: [PATCH 2/3] Add support for embedding images in Excel export --- src/components/func/func.js | 131 +++++++++++++++++++++++++++++++++--- 1 file changed, 121 insertions(+), 10 deletions(-) diff --git a/src/components/func/func.js b/src/components/func/func.js index cc40977..6e8bea0 100644 --- a/src/components/func/func.js +++ b/src/components/func/func.js @@ -1,4 +1,5 @@ import XLSX from 'xlsx'; +import ExcelJS from 'exceljs'; import { saveAs } from 'file-saver'; // Declare this so our linter knows that tableau is a global object @@ -203,30 +204,55 @@ const revalidateMeta = (existing) => new Promise((resolve, reject) => { }); }); -const exportToExcel = (meta, env, filename) => new Promise((resolve, reject) => { +const exportToExcel = async (meta, env, filename) => new Promise(async (resolve, reject) => { let xlsFile = "export.xlsx"; if (filename && filename.length > 0) { xlsFile = filename + ".xlsx"; } - buildExcelBlob(meta).then(wb => { - // add ignoreEC:false to prevent excel crashes during text to column - var wopts = { bookType:'xlsx', bookSST:false, type:'array', ignoreEC:false }; - var wbout = XLSX.write(wb,wopts); - saveAs(new Blob([wbout],{type:"application/octet-stream"}), xlsFile); - resolve(); + if (hasembecImage(meta)) { + const workbook = new ExcelJS.Workbook(); + await buildExcelBlobWithEmbedImage(workbook, meta); + const buffer = await workbook.xlsx.writeBuffer(); + saveAs( + new Blob([buffer], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }), + xlsFile + ); + resolve(); + } else { + buildExcelBlob(meta).then(wb => { + // add ignoreEC:false to prevent excel crashes during text to column + var wopts = { bookType:'xlsx', bookSST:false, type:'array', ignoreEC:false }; + var wbout = XLSX.write(wb,wopts); + saveAs(new Blob([wbout],{type:"application/octet-stream"}), xlsFile); + resolve(); + }); + }; }); -}); +const hasembecImage = (meta) => { + let hasImage = false; + meta.forEach((sheet) => { + if (sheet && sheet.columns) { + sheet.columns.forEach((col) => { + if (col && col.isImage) { + hasImage = true; + } + }); + } + }); + return hasImage; +}; // krisd: move excel creation to caller (to support extra export to methodss) // callback receives a blob to save or transfer -const buildExcelBlob = (meta) => new Promise((resolve, reject) => { +const buildExcelBlob = async (wb, meta) => new Promise((resolve, reject) => { console.log("[func.js] Got Meta", meta); // func.saveSettings(meta, function(newSettings) { // console.log("Saved settings", newSettings); const worksheets = tableau.extensions.dashboardContent.dashboard.worksheets; - const wb = XLSX.utils.book_new(); let totalSheets = 0; let sheetCount = 0; const sheetList = []; @@ -286,6 +312,91 @@ const buildExcelBlob = (meta) => new Promise((resolve, reject) => { }); }); + const buildExcelBlobWithEmbedImage = async (workbook, meta) => { + console.log("[func.js] buildExcelBlob: Got Meta", meta); + const worksheets = tableau.extensions.dashboardContent.dashboard.worksheets; + + for (const sheetMeta of meta) { + // 選択されていないシートはスキップ + if (!sheetMeta.selected) continue; + + let tabName = sheetMeta.changeName || sheetMeta.sheetName; + tabName = tabName.replace(/[*?/\\[\]]/gi, ''); + + const tableauSheet = worksheets.find(s => s.name === sheetMeta.sheetName); + if (!tableauSheet) continue; + + const summaryData = await tableauSheet.getSummaryDataAsync({ ignoreSelection: true }); + const columns = summaryData.columns.map(col => { + const cMeta = sheetMeta.columns.find(x => x.name === col.fieldName) || {}; + return { + ...col, + selected: cMeta.selected, + outputName: cMeta.changeName || cMeta.name, + isImage: cMeta.isImage || false // 画像列フラグ + }; + }); + + // 実データ部分をdecode + const decodedRows = await decodeDataset(columns, summaryData.data); + + const newSheet = workbook.addWorksheet(tabName); + + // まずはヘッダー行を設定(画像列含む全選択カラムのheaderを準備) + const selectedCols = columns.filter(c => c.selected); + const headers = selectedCols.map(c => c.outputName); + newSheet.addRow(headers); // 1行目にヘッダー + + for (let rIndex = 0; rIndex < decodedRows.length; rIndex++) { + const rowObj = decodedRows[rIndex]; + const rowValues = []; + selectedCols.forEach((col) => { + if (!col.isImage) { + rowValues.push(rowObj[col.outputName]?.v ?? null); + } else { + rowValues.push(null); + } + }); + + const newRow = newSheet.addRow(rowValues); + + await Promise.all(selectedCols.map(async (col, colIndex) => { + if (!col.isImage) return; + + const cellData = rowObj[col.outputName]; + const imageUrl = cellData?.v; + if (!imageUrl) return; + + try { + const resp = await fetch(imageUrl); + const arrayBuf = await resp.arrayBuffer(); + const imageId = workbook.addImage({ + buffer: arrayBuf, + extension: 'jpeg' + }); + newSheet.addImage(imageId, { + tl: { col: colIndex, row: (1 + (rIndex + 1)) }, + ext: { width: 50, height: 50 }, + editAs: 'oneCell' + }); + } catch (err) { + console.warn('Image fetch error:', err); + newRow.getCell(colIndex + 1).value = 'Image Error'; + } + })); + } + + const headerRow = newSheet.getRow(1); + headerRow.font = { bold: true }; + headerRow.eachCell((cell) => { + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFEFD5' }, + }; + }); + } + }; // krisd: Remove recursion to work with larger data sets // and translate cell data types From 3204a2bcfdbebb6b5bb87d4f2da8034aff3d733c Mon Sep 17 00:00:00 2001 From: iisyos Date: Sun, 6 Apr 2025 16:41:39 +0900 Subject: [PATCH 3/3] Refactor Excel export functions to improve image handling and streamline code --- src/components/func/func.js | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/components/func/func.js b/src/components/func/func.js index 6e8bea0..4ac3bbe 100644 --- a/src/components/func/func.js +++ b/src/components/func/func.js @@ -204,12 +204,13 @@ const revalidateMeta = (existing) => new Promise((resolve, reject) => { }); }); -const exportToExcel = async (meta, env, filename) => new Promise(async (resolve, reject) => { - let xlsFile = "export.xlsx"; - if (filename && filename.length > 0) { - xlsFile = filename + ".xlsx"; - } - if (hasembecImage(meta)) { +const exportToExcel = async (meta, env, filename) => + new Promise(async (resolve, reject) => { + let xlsFile = "export.xlsx"; + if (filename && filename.length > 0) { + xlsFile = filename + ".xlsx"; + } + if (hasEmbedImage(meta)) { const workbook = new ExcelJS.Workbook(); await buildExcelBlobWithEmbedImage(workbook, meta); const buffer = await workbook.xlsx.writeBuffer(); @@ -227,15 +228,17 @@ const exportToExcel = async (meta, env, filename) => new Promise(async (resolve, var wbout = XLSX.write(wb,wopts); saveAs(new Blob([wbout],{type:"application/octet-stream"}), xlsFile); resolve(); - }); - }; + }); + }; }); -const hasembecImage = (meta) => { +const hasEmbedImage = (meta) => { let hasImage = false; + console.log("[func.js] Checking for images in meta", meta); meta.forEach((sheet) => { if (sheet && sheet.columns) { sheet.columns.forEach((col) => { + console.log("[func.js] Checking column", col); if (col && col.isImage) { hasImage = true; } @@ -248,11 +251,12 @@ const hasembecImage = (meta) => { // krisd: move excel creation to caller (to support extra export to methodss) // callback receives a blob to save or transfer -const buildExcelBlob = async (wb, meta) => new Promise((resolve, reject) => { +const buildExcelBlob = async (meta) => new Promise((resolve, reject) => { console.log("[func.js] Got Meta", meta); // func.saveSettings(meta, function(newSettings) { // console.log("Saved settings", newSettings); const worksheets = tableau.extensions.dashboardContent.dashboard.worksheets; + const wb = XLSX.utils.book_new(); let totalSheets = 0; let sheetCount = 0; const sheetList = []; @@ -388,13 +392,6 @@ const buildExcelBlob = async (wb, meta) => new Promise((resolve, reject) => { const headerRow = newSheet.getRow(1); headerRow.font = { bold: true }; - headerRow.eachCell((cell) => { - cell.fill = { - type: 'pattern', - pattern: 'solid', - fgColor: { argb: 'FFEFD5' }, - }; - }); } };