Add minimal add-in playgrounds and orchestrator
This commit is contained in:
+23
@@ -0,0 +1,23 @@
|
||||
# Ignore dependencies
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
|
||||
# Local secrets
|
||||
*.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
.office-addin-dev-certs/
|
||||
|
||||
# Build artifacts
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Logs and caches
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Add-in certs
|
||||
officeaddin/minimal-playground/certs/
|
||||
minimal-uploader/certs/
|
||||
minimal-uploader-playground/certs/
|
||||
/.vscode/
|
||||
@@ -1,2 +1,13 @@
|
||||
# Job-Info
|
||||
|
||||
Development helper scripts
|
||||
- start-all.js — small orchestrator script that starts the local `minimal-uploader-playground` and the `officeaddin/minimal-playground` static server, waits for their health checks, and streams logs.
|
||||
|
||||
Usage
|
||||
```powershell
|
||||
# from repo root
|
||||
npm run start:all
|
||||
```
|
||||
|
||||
Notes: I chose the small in-repo orchestrator (start-all) rather than pm2 for simplicity — it requires no global runtime and has clear, readable behavior for local development. If you prefer pm2 for process management / auto-restart, I can add pm2 config files and an alternate script.
|
||||
|
||||
|
||||
-26251
File diff suppressed because it is too large
Load Diff
Generated
+2439
File diff suppressed because it is too large
Load Diff
@@ -18,11 +18,15 @@
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^3.8.4",
|
||||
"axios": "^1.13.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.2.1",
|
||||
"exceljs": "^4.4.0",
|
||||
"hono": "^4.10.6",
|
||||
"joi": "^18.0.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"pino": "^10.1.0",
|
||||
"pocketbase": "^0.26.3",
|
||||
"qs": "^6.14.0",
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import 'dotenv/config.js';
|
||||
import https from 'https';
|
||||
import fs from 'fs';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { ConfidentialClientApplication } from '@azure/msal-node';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
// MSAL configuration for client credentials
|
||||
const msalConfig = {
|
||||
auth: {
|
||||
clientId: process.env.CLIENT_ID,
|
||||
authority: `https://login.microsoftonline.com/${process.env.TENANT_ID}`,
|
||||
clientSecret: process.env.CLIENT_SECRET,
|
||||
},
|
||||
};
|
||||
|
||||
const cca = new ConfidentialClientApplication(msalConfig);
|
||||
|
||||
// Endpoint to exchange On-Behalf-Of token (legacy)
|
||||
app.post('/api/obo-exchange', async (req, res) => {
|
||||
const { userToken } = req.body;
|
||||
if (!userToken) {
|
||||
return res.status(400).json({ error: 'User token required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await cca.acquireTokenOnBehalfOf({
|
||||
scopes: ['https://graph.microsoft.com/.default'],
|
||||
oboAssertion: userToken,
|
||||
});
|
||||
res.json({ token: response.accessToken });
|
||||
} catch (error) {
|
||||
console.error('OBO exchange error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Main endpoint: create folder + upload PDF
|
||||
// Uses On-Behalf-Of flow: frontend sends user token → backend exchanges for Graph token
|
||||
app.post('/api/upload-pdf', async (req, res) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ error: 'Missing or invalid Authorization header' });
|
||||
}
|
||||
|
||||
const userAssertionToken = authHeader.substring(7); // Remove "Bearer " prefix
|
||||
const { jobNumber, jobFullName, fileName, pdfBase64 } = req.body;
|
||||
|
||||
if (!jobNumber || !jobFullName || !fileName || !pdfBase64) {
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Exchange user token for Graph token using On-Behalf-Of flow
|
||||
console.log('Exchanging user token via OBO flow...');
|
||||
const tokenResponse = await cca.acquireTokenOnBehalfOf({
|
||||
oboAssertion: userAssertionToken,
|
||||
scopes: ['https://graph.microsoft.com/.default'],
|
||||
});
|
||||
const accessToken = tokenResponse.accessToken;
|
||||
console.log('Token exchanged successfully. Proceeding with Graph API calls...');
|
||||
|
||||
const driveId = process.env.DRIVE_ID;
|
||||
const parentId = process.env.PARENT_ITEM_ID;
|
||||
|
||||
// Create main folder: "Job_Number - Job_Full_Name"
|
||||
const mainFolderName = `${jobNumber} - ${jobFullName}`;
|
||||
const createFolderUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${parentId}/children`;
|
||||
const createFolderBody = {
|
||||
name: mainFolderName,
|
||||
folder: {},
|
||||
'@microsoft.graph.conflictBehavior': 'rename',
|
||||
};
|
||||
|
||||
const createFolderResp = await fetch(createFolderUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(createFolderBody),
|
||||
});
|
||||
|
||||
if (!createFolderResp.ok) {
|
||||
const errorData = await createFolderResp.text();
|
||||
console.error('Create folder error:', errorData);
|
||||
return res.status(500).json({ error: `Create folder failed: ${errorData}` });
|
||||
}
|
||||
|
||||
const mainFolder = await createFolderResp.json();
|
||||
const mainFolderId = mainFolder.id;
|
||||
console.log(`✓ Created main folder: ${mainFolderName} (ID: ${mainFolderId})`);
|
||||
|
||||
// Create "Managers Info" subfolder
|
||||
const subfolderName = 'Managers Info';
|
||||
const createSubfolderUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${mainFolderId}/children`;
|
||||
const createSubfolderBody = {
|
||||
name: subfolderName,
|
||||
folder: {},
|
||||
'@microsoft.graph.conflictBehavior': 'rename',
|
||||
};
|
||||
|
||||
const createSubfolderResp = await fetch(createSubfolderUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(createSubfolderBody),
|
||||
});
|
||||
|
||||
if (!createSubfolderResp.ok) {
|
||||
const errorData = await createSubfolderResp.text();
|
||||
console.error('Create subfolder error:', errorData);
|
||||
return res.status(500).json({ error: `Create subfolder failed: ${errorData}` });
|
||||
}
|
||||
|
||||
const subfolder = await createSubfolderResp.json();
|
||||
const subfolderId = subfolder.id;
|
||||
console.log(`✓ Created subfolder: ${subfolderName} (ID: ${subfolderId})`);
|
||||
|
||||
// Upload PDF to subfolder
|
||||
const pdfBuffer = Buffer.from(pdfBase64, 'base64');
|
||||
const uploadUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${subfolderId}:/${encodeURIComponent(fileName)}:/content`;
|
||||
|
||||
const uploadResp = await fetch(uploadUrl, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/pdf',
|
||||
},
|
||||
body: pdfBuffer,
|
||||
});
|
||||
|
||||
if (!uploadResp.ok) {
|
||||
const errorData = await uploadResp.text();
|
||||
console.error('Upload PDF error:', errorData);
|
||||
return res.status(500).json({ error: `Upload PDF failed: ${errorData}` });
|
||||
}
|
||||
|
||||
const uploadedItem = await uploadResp.json();
|
||||
console.log(`✓ Uploaded PDF: ${fileName}`);
|
||||
|
||||
// Create share link for the subfolder
|
||||
const shareLinkUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${subfolderId}/createLink`;
|
||||
const shareLinkBody = {
|
||||
type: 'view',
|
||||
scope: 'organization',
|
||||
};
|
||||
|
||||
const shareLinkResp = await fetch(shareLinkUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(shareLinkBody),
|
||||
});
|
||||
|
||||
if (!shareLinkResp.ok) {
|
||||
const errorData = await shareLinkResp.text();
|
||||
console.error('Create share link error:', errorData);
|
||||
return res.status(500).json({ error: `Create share link failed: ${errorData}` });
|
||||
}
|
||||
|
||||
const shareLink = await shareLinkResp.json();
|
||||
console.log(`✓ Created share link`);
|
||||
|
||||
res.json({
|
||||
uploadedItem,
|
||||
shareLink: shareLink.link.webUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upload PDF error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Health check
|
||||
app.get('/', (req, res) => {
|
||||
res.json({ message: 'Backend server running' });
|
||||
});
|
||||
|
||||
// Fallback to HTTP (dev server doesn't need HTTPS for localhost)
|
||||
app.listen(PORT, () => {
|
||||
console.log(`✅ Backend listening on http://localhost:${PORT}`);
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
# Example .env for minimal-uploader (do not commit with real secrets)
|
||||
CLIENT_SECRET=7aD8Q~d5K~_PzQv6KqDdrEnmyXHE60eVDpbcnaK_
|
||||
TENANT_ID=3fd97ea7-b124-41f1-855f-52d8ac3b16c7
|
||||
CLIENT_ID=3c846e71-9609-40e1-b458-0eb805e21b9f
|
||||
DRIVE_ID=b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx
|
||||
PARENT_ITEM_ID=01SPNXLDW3Y56HAWK5CNAJ4YJLLSQCUCJ7
|
||||
# alias: ITEM_ID is also supported for compatibility with other folders in this repo
|
||||
ITEM_ID=01SPNXLDW3Y56HAWK5CNAJ4YJLLSQCUCJ7
|
||||
@@ -0,0 +1,28 @@
|
||||
# Minimal Uploader
|
||||
|
||||
This small utility uses environment variables in your environment (.env or other) to create a folder under a configured parent item in a drive, create a subfolder, and upload a PDF file.
|
||||
|
||||
Important: It expects the following environment variables to be available (use your existing .env to supply these values):
|
||||
|
||||
- CLIENT_ID
|
||||
- CLIENT_SECRET
|
||||
- TENANT_ID
|
||||
- DRIVE_ID
|
||||
- PARENT_ITEM_ID
|
||||
|
||||
Usage:
|
||||
|
||||
1. Ensure your environment has the required variables populated (you can copy your existing .env file into this folder or run the script from the repo root where the env is defined).
|
||||
2. Install dependencies:
|
||||
|
||||
npm install
|
||||
|
||||
3. Dry-run (simulate without hitting Graph):
|
||||
|
||||
npm run check
|
||||
|
||||
4. Create folder, subfolder and upload a sample PDF (one-shot):
|
||||
|
||||
npm start
|
||||
|
||||
This script performs the operation with the app-only client credential flow (it will use the exact values from the environment variables). The uploaded PDF is a small made-up payload; all other values are read from environment variables (exact by design).
|
||||
@@ -0,0 +1,6 @@
|
||||
Name,Role,Email,Office,Phone
|
||||
Alice Smith,Manager,alice@example.com,NY,555-0100
|
||||
Bob Jones,Supervisor,bob@example.com,LA,555-0101
|
||||
Charlie Davis,Coordinator,charlie@example.com,AUS,555-0102
|
||||
David Evans,Lead,david@example.com,NY,555-0103
|
||||
Eve Johnson,Manager,eve@example.com,LA,555-0104
|
||||
|
+2042
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "minimal-uploader",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "run.js",
|
||||
"scripts": {
|
||||
"start": "node run.js",
|
||||
"check": "node run.js --dry-run",
|
||||
"server": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^1.13.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"node-fetch": "^2.6.7",
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"body-parser": "^1.20.2",
|
||||
"pdfkit": "^0.13.0",
|
||||
"csv-parse": "^5.4.1"
|
||||
,"xlsx": "^0.18.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,581 @@
|
||||
/* Minimal uploader: create folder, subfolder, upload PDF
|
||||
Uses app-only MSAL client credentials from environment variables.
|
||||
*/
|
||||
|
||||
// Load .env if present so this script can be run from the project folder
|
||||
try { require('dotenv').config(); } catch (e) {}
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const fetch = require('node-fetch');
|
||||
const { ConfidentialClientApplication } = require('@azure/msal-node');
|
||||
const XLSX = require('xlsx');
|
||||
|
||||
// Shared cell formatter used by PDF generators
|
||||
function formatCellValue(val) {
|
||||
if (val === null || val === undefined) return '';
|
||||
if (val instanceof Date && !isNaN(val.getTime())) {
|
||||
const mm = String(val.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(val.getDate()).padStart(2, '0');
|
||||
const yyyy = val.getFullYear();
|
||||
const hh = String(val.getHours()).padStart(2, '0');
|
||||
const mi = String(val.getMinutes()).padStart(2, '0');
|
||||
return `${mm}/${dd}/${yyyy} ${hh}:${mi}`;
|
||||
}
|
||||
if (typeof val === 'number') {
|
||||
try {
|
||||
const parsed = XLSX.SSF.parse_date_code(val);
|
||||
if (parsed && parsed.y) {
|
||||
const dt = new Date(Date.UTC(parsed.y, parsed.m - 1, parsed.d, parsed.H || 0, parsed.M || 0, parsed.S || 0));
|
||||
return formatCellValue(dt);
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
if (typeof val === 'string') {
|
||||
const s = val.trim();
|
||||
const maybe = Date.parse(s);
|
||||
if (!isNaN(maybe)) return formatCellValue(new Date(maybe));
|
||||
return s;
|
||||
}
|
||||
return String(val);
|
||||
}
|
||||
|
||||
const CLIENT_ID = process.env.CLIENT_ID;
|
||||
const CLIENT_SECRET = process.env.CLIENT_SECRET;
|
||||
const TENANT_ID = process.env.TENANT_ID;
|
||||
const DRIVE_ID = process.env.DRIVE_ID;
|
||||
// Support both PARENT_ITEM_ID (preferred) and ITEM_ID (legacy / alternate)
|
||||
const PARENT_ITEM_ID = process.env.PARENT_ITEM_ID || process.env.ITEM_ID;
|
||||
|
||||
const dryRun = process.argv.includes('--dry-run');
|
||||
|
||||
function assertEnv(name, val) {
|
||||
if (!val) {
|
||||
console.error(`Missing env var: ${name}`);
|
||||
process.exitCode = 2;
|
||||
throw new Error(`Missing env var ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
assertEnv('CLIENT_ID', CLIENT_ID);
|
||||
assertEnv('CLIENT_SECRET', CLIENT_SECRET);
|
||||
assertEnv('TENANT_ID', TENANT_ID);
|
||||
assertEnv('DRIVE_ID', DRIVE_ID);
|
||||
assertEnv('PARENT_ITEM_ID', PARENT_ITEM_ID);
|
||||
} catch (e) {
|
||||
console.error('Please set required environment values before running. See .env.example');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const msalConfig = {
|
||||
auth: {
|
||||
clientId: CLIENT_ID,
|
||||
authority: `https://login.microsoftonline.com/${TENANT_ID}`,
|
||||
clientSecret: CLIENT_SECRET
|
||||
}
|
||||
};
|
||||
|
||||
const cca = new ConfidentialClientApplication(msalConfig);
|
||||
|
||||
async function getAppToken() {
|
||||
const result = await cca.acquireTokenByClientCredential({ scopes: [ 'https://graph.microsoft.com/.default' ] });
|
||||
return result.accessToken;
|
||||
}
|
||||
|
||||
// Small made-up (valid-ish) PDF bytes (1-page blank PDF) base64
|
||||
const SAMPLE_PDF_BASE64 = Buffer.from(
|
||||
`%PDF-1.4\n%\u00e2\u00e3\u00cf\u00d3\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>\nendobj\n4 0 obj\n<< /Length 55 >>\nstream\nBT /F1 24 Tf 72 720 Td (Sample PDF uploaded by minimal-uploader) Tj ET\nendstream\nendobj\nxref\n0 5\n0000000000 65535 f\n0000000010 00000 n\n0000000066 00000 n\n0000000111 00000 n\n0000000200 00000 n\ntrailer<< /Size 5 /Root 1 0 R >>\nstartxref\n300\n%%EOF\n`).toString('base64');
|
||||
|
||||
// If managers.csv exists in the folder, build a PDF from it and return a base64 string
|
||||
const pathToCsv = path.join(__dirname, 'managers.csv');
|
||||
const { parse } = require('csv-parse/sync');
|
||||
const PDFDocument = require('pdfkit');
|
||||
|
||||
async function pdfFromCsvToBase64(csvPath) {
|
||||
if (!fs.existsSync(csvPath)) return null;
|
||||
const csvRaw = fs.readFileSync(csvPath, 'utf8');
|
||||
const records = parse(csvRaw, { columns: true, skip_empty_lines: true });
|
||||
|
||||
// Create PDF in memory using PDFKit
|
||||
const doc = new PDFDocument({ size: 'A4', margin: 40 });
|
||||
const buffers = [];
|
||||
doc.on('data', (chunk) => buffers.push(chunk));
|
||||
const title = 'Managers Info';
|
||||
doc.fontSize(16).text(title, { align: 'left' }).moveDown(0.5);
|
||||
|
||||
// table header
|
||||
const headers = Object.keys(records[0] || {});
|
||||
doc.fontSize(10);
|
||||
const columnWidths = headers.map(() => Math.floor((doc.page.width - 80) / headers.length));
|
||||
|
||||
// Render header row
|
||||
headers.forEach((h, i) => {
|
||||
doc.font('Helvetica-Bold').text(h, { continued: i !== headers.length - 1, width: columnWidths[i] });
|
||||
});
|
||||
doc.moveDown(0.25);
|
||||
doc.font('Helvetica');
|
||||
|
||||
// Render rows limited to 49 (A1:E49) but safe if fewer
|
||||
const limit = Math.min(records.length, 49);
|
||||
for (let r = 0; r < limit; r++) {
|
||||
const row = records[r];
|
||||
headers.forEach((h, i) => {
|
||||
const txt = row[h] !== undefined ? String(row[h]) : '';
|
||||
doc.text(txt, { continued: i !== headers.length - 1, width: columnWidths[i] });
|
||||
});
|
||||
doc.moveDown(0.1);
|
||||
}
|
||||
|
||||
doc.end();
|
||||
await new Promise((res) => doc.on('end', res));
|
||||
const pdfBuffer = Buffer.concat(buffers);
|
||||
return pdfBuffer.toString('base64');
|
||||
}
|
||||
|
||||
// build PDF from a records array (array of objects)
|
||||
async function pdfFromRecordsToBase64(records, title = 'Managers Info') {
|
||||
if (!records || records.length === 0) return null;
|
||||
|
||||
// Use the shared top-level formatCellValue helper
|
||||
|
||||
const doc = new PDFDocument({ size: 'A4', margin: 36 });
|
||||
const buffers = [];
|
||||
doc.on('data', (chunk) => buffers.push(chunk));
|
||||
doc.on('error', (e) => console.error('PDF generation error', e));
|
||||
|
||||
const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
||||
const startX = doc.x;
|
||||
let cursorY = doc.y;
|
||||
|
||||
// Title
|
||||
doc.font('Helvetica-Bold').fontSize(14).text(title, startX, cursorY);
|
||||
cursorY = doc.y + 8;
|
||||
|
||||
// Setup table
|
||||
const headers = Object.keys(records[0] || {});
|
||||
const columnCount = Math.max(1, headers.length);
|
||||
const baseWidth = Math.floor(pageWidth / columnCount);
|
||||
const columnWidths = headers.map((_, idx) => {
|
||||
if (columnCount >= 4 && idx === 0) return Math.floor(baseWidth * 1.5);
|
||||
if (columnCount >= 4 && idx === 1) return Math.floor(baseWidth * 1.25);
|
||||
return baseWidth;
|
||||
});
|
||||
|
||||
const tableX = startX;
|
||||
let tableY = cursorY;
|
||||
const rowPadding = 6;
|
||||
|
||||
// Header row
|
||||
const headerHeight = 20;
|
||||
doc.rect(tableX, tableY, pageWidth, headerHeight).fill('#f2f2f2');
|
||||
doc.fillColor('#000');
|
||||
let cx = tableX;
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
doc.font('Helvetica-Bold').fontSize(10).fillColor('#000').text(String(headers[i] || ''), cx + rowPadding, tableY + 6, { width: columnWidths[i] - rowPadding * 2 });
|
||||
cx += columnWidths[i];
|
||||
}
|
||||
doc.moveTo(tableX, tableY + headerHeight).lineTo(tableX + pageWidth, tableY + headerHeight).stroke();
|
||||
cursorY = tableY + headerHeight;
|
||||
|
||||
// Rows
|
||||
const limit = Math.min(records.length, 49);
|
||||
for (let r = 0; r < limit; r++) {
|
||||
const row = records[r];
|
||||
const cellTexts = headers.map(h => formatCellValue(row[h]));
|
||||
|
||||
const heights = cellTexts.map((t, i) => doc.heightOfString(t || '', { width: columnWidths[i] - rowPadding * 2 }));
|
||||
const rowHeight = Math.max(heights.reduce((a,b) => Math.max(a,b), 0), 12) + rowPadding * 2;
|
||||
|
||||
// page break
|
||||
if (cursorY + rowHeight + 40 > doc.page.height - doc.page.margins.bottom) {
|
||||
doc.addPage();
|
||||
cursorY = doc.page.margins.top;
|
||||
}
|
||||
|
||||
// background for alternating rows
|
||||
if (r % 2 === 1) {
|
||||
doc.rect(tableX, cursorY, pageWidth, rowHeight).fill('#fcfcfc');
|
||||
doc.fillColor('#000');
|
||||
}
|
||||
|
||||
cx = tableX;
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
doc.font('Helvetica').fontSize(9).fillColor('#000').text(cellTexts[i] || '', cx + rowPadding, cursorY + rowPadding, { width: columnWidths[i] - rowPadding * 2 });
|
||||
// vertical separator
|
||||
doc.moveTo(cx + columnWidths[i], cursorY).lineTo(cx + columnWidths[i], cursorY + rowHeight).stroke();
|
||||
cx += columnWidths[i];
|
||||
}
|
||||
|
||||
// bottom border
|
||||
doc.moveTo(tableX, cursorY + rowHeight).lineTo(tableX + pageWidth, cursorY + rowHeight).stroke();
|
||||
cursorY += rowHeight;
|
||||
}
|
||||
|
||||
doc.end();
|
||||
await new Promise((res) => doc.on('end', res));
|
||||
const pdfBuffer = Buffer.concat(buffers);
|
||||
return pdfBuffer.toString('base64');
|
||||
}
|
||||
|
||||
async function downloadExcelFromGraph(accessToken, driveId, itemId) {
|
||||
const url = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content`;
|
||||
const res = await fetch(url, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}` }, });
|
||||
if (!res.ok) throw new Error(`Download workbook failed: ${res.status} ${res.statusText}`);
|
||||
const ab = await res.arrayBuffer();
|
||||
return Buffer.from(ab);
|
||||
}
|
||||
|
||||
function parseManagersSheetFromWorkbookBuffer(buffer) {
|
||||
const workbook = XLSX.read(buffer, { type: 'buffer' });
|
||||
// try the obvious names, otherwise find any sheet name that contains 'manager'
|
||||
let sheet = workbook.Sheets['Managers Info'] || workbook.Sheets['Managers'] || null;
|
||||
if (!sheet) {
|
||||
const names = workbook.SheetNames || [];
|
||||
const match = names.find(n => /manager/i.test(n));
|
||||
if (match) sheet = workbook.Sheets[match];
|
||||
else {
|
||||
console.log('Workbook sheets:', names);
|
||||
}
|
||||
}
|
||||
if (!sheet) return null;
|
||||
// get A1:E49 region - using sheet_to_json with header row
|
||||
const options = { header: 1, range: 'A1:E49', blankrows: false, defval: '' };
|
||||
const rows = XLSX.utils.sheet_to_json(sheet, options);
|
||||
if (!rows || rows.length === 0) return null;
|
||||
// first row assumed to be headers
|
||||
const headers = rows[0].map(h => String(h).trim());
|
||||
const dataRows = rows.slice(1).map(r => {
|
||||
const obj = {};
|
||||
headers.forEach((h, i) => { obj[h || `col${i}`] = r[i] !== undefined ? r[i] : ''; });
|
||||
return obj;
|
||||
});
|
||||
return dataRows;
|
||||
}
|
||||
|
||||
// Helpers to read cells / ranges by address from a sheet
|
||||
function getCellValue(sheet, addr) {
|
||||
const cell = sheet[addr];
|
||||
if (!cell) return '';
|
||||
return cell.w || cell.v || '';
|
||||
}
|
||||
|
||||
function getRangeRows(sheet, rangeStr) {
|
||||
// decode range and return array of row arrays
|
||||
const r = XLSX.utils.decode_range(rangeStr);
|
||||
const rows = [];
|
||||
for (let R = r.s.r; R <= r.e.r; ++R) {
|
||||
const row = [];
|
||||
for (let C = r.s.c; C <= r.e.c; ++C) {
|
||||
const addr = XLSX.utils.encode_cell({ c: C, r: R });
|
||||
row.push(getCellValue(sheet, addr));
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Build PDF from workbook buffer with special region formatting (merged ranges and blocks)
|
||||
async function pdfFromWorkbookBufferToBase64(buffer) {
|
||||
const workbook = XLSX.read(buffer, { type: 'buffer' });
|
||||
// find a Managers-like sheet
|
||||
let sheetName = workbook.SheetNames.find(n => /Managers Info/i.test(n)) || workbook.SheetNames.find(n => /Managers/i.test(n));
|
||||
if (!sheetName) sheetName = workbook.SheetNames[0];
|
||||
const sheet = workbook.Sheets[sheetName];
|
||||
if (!sheet) return null;
|
||||
|
||||
// Prepare doc
|
||||
const doc = new PDFDocument({ size: 'A4', margin: 36 });
|
||||
const buffers = [];
|
||||
doc.on('data', (chunk) => buffers.push(chunk));
|
||||
|
||||
const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
||||
const leftX = doc.page.margins.left;
|
||||
let y = doc.y;
|
||||
|
||||
// Top title (sheet title)
|
||||
doc.font('Helvetica-Bold').fontSize(16).text(sheetName || 'Managers Info', { align: 'center' });
|
||||
y = doc.y + 6;
|
||||
|
||||
// 1) B2:D3 merged: center horizontally and bottom align vertically, 18pt bold dark-blue
|
||||
const b2Value = getCellValue(sheet, 'B2');
|
||||
if (b2Value) {
|
||||
// prefer Arial if available, otherwise use Helvetica-Bold as a safe built-in fallback
|
||||
let usedFont = null;
|
||||
try { doc.font('Arial'); usedFont = 'Arial'; } catch (e) { /* not available */ }
|
||||
if (!usedFont) {
|
||||
try { doc.font('Helvetica-Bold'); usedFont = 'Helvetica-Bold'; } catch (e) { /* ultimate fallback */ }
|
||||
}
|
||||
doc.fillColor('#003366').fontSize(18);
|
||||
// center the text inside about 60% of page width
|
||||
const boxW = Math.floor(pageWidth * 0.6);
|
||||
const x = leftX + Math.floor((pageWidth - boxW) / 2);
|
||||
// treat the merged region height as a small fixed box so we can bottom-align the text
|
||||
const boxH = 48; // reasonable height for two-row merged area
|
||||
// ensure there's enough space on the page
|
||||
if (y + boxH > doc.page.height - doc.page.margins.bottom) { doc.addPage(); y = doc.page.margins.top; }
|
||||
const h = doc.heightOfString(String(b2Value), { width: boxW });
|
||||
// place the text so its bottom is just above the box bottom (6px padding)
|
||||
const textY = y + Math.max(0, boxH - h - 6);
|
||||
doc.text(String(b2Value), x, textY, { width: boxW, align: 'center' });
|
||||
y += boxH + 8;
|
||||
doc.fillColor('#000'); doc.font('Helvetica');
|
||||
}
|
||||
|
||||
// Spacer
|
||||
y += 4;
|
||||
|
||||
// 2) B5:D5 merged left-bottom aligned (smaller title/label)
|
||||
const b5Value = getCellValue(sheet, 'B5');
|
||||
if (b5Value) {
|
||||
doc.font('Helvetica-Bold').fontSize(12).fillColor('#000');
|
||||
const boxW2 = Math.floor(pageWidth * 0.6);
|
||||
const boxH2 = 22; // single-row height approximation
|
||||
if (y + boxH2 > doc.page.height - doc.page.margins.bottom) { doc.addPage(); y = doc.page.margins.top; }
|
||||
// compute measured height and bottom align inside box
|
||||
const h2 = doc.heightOfString(String(b5Value), { width: boxW2 });
|
||||
const textY2 = y + Math.max(0, boxH2 - h2 - 4);
|
||||
doc.text(String(b5Value), leftX, textY2, { width: boxW2, align: 'left' });
|
||||
y += boxH2 + 6;
|
||||
}
|
||||
|
||||
// 3) B6:C9 cells -> render small grid block
|
||||
const block1 = getRangeRows(sheet, 'B6:C9');
|
||||
if (block1 && block1.length > 0) {
|
||||
// render as two-column box
|
||||
const colCount = block1[0].length;
|
||||
const columnW = Math.floor((pageWidth * 0.5) / Math.max(1, colCount));
|
||||
// header style omitted (these are raw cells)
|
||||
for (let r = 0; r < block1.length; r++) {
|
||||
const row = block1[r];
|
||||
let x = leftX;
|
||||
let maxH = 0;
|
||||
for (let c = 0; c < row.length; c++) {
|
||||
const txt = String(row[c] || '');
|
||||
const h = doc.heightOfString(txt, { width: columnW - 6 });
|
||||
maxH = Math.max(maxH, h);
|
||||
}
|
||||
// draw cells
|
||||
for (let c = 0; c < row.length; c++) {
|
||||
const txt = String(row[c] || '');
|
||||
doc.rect(x, y, columnW, maxH + 8).stroke();
|
||||
doc.text(txt, x + 4, y + 4, { width: columnW - 8 });
|
||||
x += columnW;
|
||||
}
|
||||
y += maxH + 10;
|
||||
}
|
||||
}
|
||||
|
||||
// 4) B11:D11 merged left bottom
|
||||
const b11Value = getCellValue(sheet, 'B11');
|
||||
if (b11Value) {
|
||||
doc.font('Helvetica').fontSize(11).fillColor('#000');
|
||||
doc.text(String(b11Value), leftX, y, { width: pageWidth * 0.6, align: 'left' });
|
||||
y = doc.y + 6;
|
||||
}
|
||||
|
||||
// 5) B12:C15 block
|
||||
const block2 = getRangeRows(sheet, 'B12:C15');
|
||||
if (block2 && block2.length > 0) {
|
||||
const colCount = block2[0].length;
|
||||
const columnW = Math.floor((pageWidth * 0.5) / Math.max(1, colCount));
|
||||
for (let r = 0; r < block2.length; r++) {
|
||||
const row = block2[r];
|
||||
let x = leftX;
|
||||
let maxH = 0;
|
||||
for (let c = 0; c < row.length; c++) {
|
||||
const txt = String(row[c] || '');
|
||||
const h = doc.heightOfString(txt, { width: columnW - 6 });
|
||||
maxH = Math.max(maxH, h);
|
||||
}
|
||||
for (let c = 0; c < row.length; c++) {
|
||||
const txt = String(row[c] || '');
|
||||
doc.rect(x, y, columnW, maxH + 8).stroke();
|
||||
doc.text(txt, x + 4, y + 4, { width: columnW - 8 });
|
||||
x += columnW;
|
||||
}
|
||||
y += maxH + 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Add small spacer
|
||||
y += 8;
|
||||
|
||||
// Now append the main table below (A1:E49 region) - reuse previous parseManagersSheet logic
|
||||
const rowsOpt = { header: 1, range: 'A1:E49', blankrows: false, defval: '' };
|
||||
const rows = XLSX.utils.sheet_to_json(sheet, rowsOpt);
|
||||
if (rows && rows.length > 1) {
|
||||
const headersTable = rows[0].map(h => String(h).trim());
|
||||
const dataRows = rows.slice(1).map(r => {
|
||||
const obj = {};
|
||||
headersTable.forEach((h, i) => { obj[h || `col${i}`] = r[i] !== undefined ? r[i] : ''; });
|
||||
return obj;
|
||||
});
|
||||
// render the rest using the existing pdfFromRecordsToBase64 but passing doc starting state isn't simple
|
||||
// so call pdfFromRecordsToBase64 to create a standalone PDF and then append? Simpler: render a simple table here.
|
||||
|
||||
// Use same table rendering approach as records conversion
|
||||
const tableHeaders = headersTable;
|
||||
const cCount = Math.max(1, tableHeaders.length);
|
||||
const tBaseWidth = Math.floor(pageWidth / cCount);
|
||||
const tColWidths = tableHeaders.map((_, idx) => tBaseWidth);
|
||||
|
||||
// draw headers
|
||||
if (y + 30 > doc.page.height - doc.page.margins.bottom) { doc.addPage(); y = doc.page.margins.top; }
|
||||
let tx = leftX;
|
||||
const th = 18;
|
||||
doc.rect(tx, y, pageWidth, th).fill('#f2f2f2'); doc.fillColor('#000');
|
||||
for (let i = 0; i < tableHeaders.length; i++) { doc.font('Helvetica-Bold').fontSize(10).text(tableHeaders[i] || '', tx + 4, y + 4, { width: tColWidths[i] - 8 }); tx += tColWidths[i]; }
|
||||
doc.moveTo(leftX, y + th).lineTo(leftX + pageWidth, y + th).stroke();
|
||||
y += th;
|
||||
|
||||
const maxRows = Math.min(dataRows.length, 49);
|
||||
for (let r = 0; r < maxRows; r++) {
|
||||
const row = dataRows[r];
|
||||
// compute row height
|
||||
const cellTexts = tableHeaders.map(h => formatCellValue(row[h]));
|
||||
const heights = cellTexts.map((t, i) => doc.heightOfString(t || '', { width: tColWidths[i] - 8 }));
|
||||
const rowH = Math.max(heights.reduce((a,b) => Math.max(a,b), 0), 12) + 8;
|
||||
if (y + rowH + 40 > doc.page.height - doc.page.margins.bottom) { doc.addPage(); y = doc.page.margins.top; }
|
||||
tx = leftX;
|
||||
for (let i = 0; i < tableHeaders.length; i++) {
|
||||
doc.font('Helvetica').fontSize(9).text(cellTexts[i] || '', tx + 4, y + 4, { width: tColWidths[i] - 8 });
|
||||
// vertical line
|
||||
doc.moveTo(tx + tColWidths[i], y).lineTo(tx + tColWidths[i], y + rowH).stroke();
|
||||
tx += tColWidths[i];
|
||||
}
|
||||
doc.moveTo(leftX, y + rowH).lineTo(leftX + pageWidth, y + rowH).stroke();
|
||||
y += rowH;
|
||||
}
|
||||
}
|
||||
|
||||
doc.end();
|
||||
await new Promise((res) => doc.on('end', res));
|
||||
const pdfBuffer = Buffer.concat(buffers);
|
||||
return pdfBuffer.toString('base64');
|
||||
}
|
||||
|
||||
async function createChildFolder(accessToken, parentDriveId, parentItemId, folderName) {
|
||||
const endpoint = `https://graph.microsoft.com/v1.0/drives/${parentDriveId}/items/${parentItemId}/children`;
|
||||
const body = { name: folderName, folder: {}, "@microsoft.graph.conflictBehavior": "rename" };
|
||||
if (dryRun) {
|
||||
console.log('[dry-run] Would POST', endpoint, 'body', body);
|
||||
return { id: 'dry-main-folder-id', name: folderName };
|
||||
}
|
||||
|
||||
const resp = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (!resp.ok) throw new Error(`Create folder failed: ${JSON.stringify(json)}`);
|
||||
return json;
|
||||
}
|
||||
|
||||
async function uploadFileToFolder(accessToken, driveId, folderId, fileName, base64pdf) {
|
||||
const endpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${folderId}:/${encodeURIComponent(fileName)}:/content`;
|
||||
const buffer = Buffer.from(base64pdf, 'base64');
|
||||
if (dryRun) {
|
||||
console.log('[dry-run] Would PUT', endpoint, `(${buffer.length} bytes)`);
|
||||
return { id: 'dry-upload-id', name: fileName, size: buffer.length };
|
||||
}
|
||||
|
||||
const resp = await fetch(endpoint, {
|
||||
method: 'PUT',
|
||||
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/pdf' },
|
||||
body: buffer
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (!resp.ok) throw new Error(`Upload failed: ${JSON.stringify(json)}`);
|
||||
return json;
|
||||
}
|
||||
|
||||
async function createShareLink(accessToken, driveId, itemId) {
|
||||
const endpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/createLink`;
|
||||
if (dryRun) {
|
||||
console.log('[dry-run] Would POST', endpoint, 'body', { type: 'view', scope: 'organization' });
|
||||
return { link: { webUrl: 'https://example.com/dry-share-link' } };
|
||||
}
|
||||
const resp = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type: 'view', scope: 'organization' })
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (!resp.ok) throw new Error(`Create share link failed: ${JSON.stringify(json)}`);
|
||||
return json;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
console.log('minimal-uploader starting', dryRun ? '(dry-run)' : '');
|
||||
const token = await getAppToken();
|
||||
console.log('acquired app token (truncated):', token ? token.substring(0, 30) + '...' : '(none)');
|
||||
|
||||
const mainFolderName = `0123 - Example Job (created ${new Date().toISOString()})`;
|
||||
// sanitize names to avoid invalid SharePoint characters (e.g. ':' in ISO timestamps)
|
||||
function sanitizeName(n) {
|
||||
// Replace characters that are invalid in SharePoint file/folder names
|
||||
// See: https://learn.microsoft.com/sharepoint/dev/general-development/avoid-using-invalid-file-name-characters
|
||||
return n.replace(/["\*\\\/:<>\?|#{}%~&]/g, '-')
|
||||
.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
const safeMainFolderName = sanitizeName(mainFolderName);
|
||||
const subFolderName = 'Managers Info';
|
||||
// Prefer generating from an Excel workbook if EXCEL_DRIVE_ID and EXCEL_ITEM_ID are present
|
||||
let csvBase64 = await pdfFromCsvToBase64(pathToCsv);
|
||||
if (process.env.EXCEL_DRIVE_ID && process.env.EXCEL_ITEM_ID) {
|
||||
console.log('EXCEL_DRIVE_ID/EXCEL_ITEM_ID present, downloading workbook from Graph to build PDF');
|
||||
try {
|
||||
const token1 = token; // app token used for Graph
|
||||
const workbookBuffer = await downloadExcelFromGraph(token1, process.env.EXCEL_DRIVE_ID, process.env.EXCEL_ITEM_ID);
|
||||
// Build a PDF directly from the workbook and its formatting/regions if possible
|
||||
const wbPdf = await pdfFromWorkbookBufferToBase64(workbookBuffer);
|
||||
if (wbPdf) {
|
||||
csvBase64 = wbPdf;
|
||||
// count rows if we can
|
||||
const recs = parseManagersSheetFromWorkbookBuffer(workbookBuffer) || [];
|
||||
console.log('Built PDF from workbook Managers Info with', recs.length, 'rows');
|
||||
} else {
|
||||
console.log('No Managers Info sheet / no rows found in workbook — falling back to managers.csv or static PDF');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to build PDF from workbook', e && e.message ? e.message : e);
|
||||
}
|
||||
}
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const fileName = sanitizeName(`Managers Info - ${timestamp}.pdf`);
|
||||
|
||||
console.log('Creating main folder under parent item', PARENT_ITEM_ID);
|
||||
const main = await createChildFolder(token, DRIVE_ID, PARENT_ITEM_ID, safeMainFolderName);
|
||||
console.log('main folder created:', main.id || mainName || JSON.stringify(main));
|
||||
|
||||
console.log('Creating subfolder under main folder', main.id);
|
||||
const sub = await createChildFolder(token, DRIVE_ID, main.id, subFolderName);
|
||||
console.log('subfolder created:', sub.id || JSON.stringify(sub));
|
||||
|
||||
console.log('Uploading PDF into subfolder', sub.id);
|
||||
const payloadBase64 = csvBase64 || SAMPLE_PDF_BASE64;
|
||||
if (!csvBase64) console.log('No managers.csv found - using fallback static PDF');
|
||||
const uploaded = await uploadFileToFolder(token, DRIVE_ID, sub.id, fileName, payloadBase64);
|
||||
console.log('uploaded result:', uploaded.id, uploaded.name, uploaded.size || '(size unknown)');
|
||||
|
||||
console.log('Creating share link for subfolder', sub.id);
|
||||
const share = await createShareLink(token, DRIVE_ID, sub.id);
|
||||
console.log('share link:', share && share.link && share.link.webUrl ? share.link.webUrl : JSON.stringify(share));
|
||||
|
||||
console.log('Done — created folder, subfolder and uploaded file.');
|
||||
// return useful metadata so callers (like an API server) can report status
|
||||
return { main, sub, uploaded, share };
|
||||
}
|
||||
|
||||
// Export run so other modules (API server) can call it.
|
||||
module.exports = { run };
|
||||
|
||||
if (require.main === module) {
|
||||
// If executed directly from the CLI, run normally
|
||||
run().catch(err => {
|
||||
console.error('Error running minimal-uploader:', err && err.message ? err.message : err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/* Simple playground server: exposes /api/upload-managers which runs the run() exporter
|
||||
from run.js and returns the JSON result. Meant for local development. */
|
||||
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const bodyParser = require('body-parser');
|
||||
const path = require('path');
|
||||
|
||||
const { run } = require('./run');
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json());
|
||||
|
||||
// health
|
||||
app.get('/api/health', (req, res) => res.json({ ok: true, ts: new Date().toISOString() }));
|
||||
|
||||
// main endpoint — run the uploader flow (download workbook if env vars exist, create folders and upload PDF)
|
||||
app.post('/api/upload-managers', async (req, res) => {
|
||||
try {
|
||||
const result = await run();
|
||||
res.json({ ok: true, result });
|
||||
} catch (err) {
|
||||
console.error('Error in /api/upload-managers', err);
|
||||
res.status(500).json({ ok: false, error: (err && err.message) ? err.message : String(err) });
|
||||
}
|
||||
});
|
||||
|
||||
const port = process.env.PLAYGROUND_PORT || 3002;
|
||||
app.listen(port, () => console.log(`minimal-uploader-playground server listening on http://localhost:${port}`));
|
||||
@@ -0,0 +1,8 @@
|
||||
# Example .env for minimal-uploader (do not commit with real secrets)
|
||||
CLIENT_SECRET=7aD8Q~d5K~_PzQv6KqDdrEnmyXHE60eVDpbcnaK_
|
||||
TENANT_ID=3fd97ea7-b124-41f1-855f-52d8ac3b16c7
|
||||
CLIENT_ID=3c846e71-9609-40e1-b458-0eb805e21b9f
|
||||
DRIVE_ID=b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx
|
||||
PARENT_ITEM_ID=01SPNXLDW3Y56HAWK5CNAJ4YJLLSQCUCJ7
|
||||
# alias: ITEM_ID is also supported for compatibility with other folders in this repo
|
||||
ITEM_ID=01SPNXLDW3Y56HAWK5CNAJ4YJLLSQCUCJ7
|
||||
@@ -0,0 +1,28 @@
|
||||
# Minimal Uploader
|
||||
|
||||
This small utility uses environment variables in your environment (.env or other) to create a folder under a configured parent item in a drive, create a subfolder, and upload a PDF file.
|
||||
|
||||
Important: It expects the following environment variables to be available (use your existing .env to supply these values):
|
||||
|
||||
- CLIENT_ID
|
||||
- CLIENT_SECRET
|
||||
- TENANT_ID
|
||||
- DRIVE_ID
|
||||
- PARENT_ITEM_ID
|
||||
|
||||
Usage:
|
||||
|
||||
1. Ensure your environment has the required variables populated (you can copy your existing .env file into this folder or run the script from the repo root where the env is defined).
|
||||
2. Install dependencies:
|
||||
|
||||
npm install
|
||||
|
||||
3. Dry-run (simulate without hitting Graph):
|
||||
|
||||
npm run check
|
||||
|
||||
4. Create folder, subfolder and upload a sample PDF (one-shot):
|
||||
|
||||
npm start
|
||||
|
||||
This script performs the operation with the app-only client credential flow (it will use the exact values from the environment variables). The uploaded PDF is a small made-up payload; all other values are read from environment variables (exact by design).
|
||||
@@ -0,0 +1,6 @@
|
||||
Name,Role,Email,Office,Phone
|
||||
Alice Smith,Manager,alice@example.com,NY,555-0100
|
||||
Bob Jones,Supervisor,bob@example.com,LA,555-0101
|
||||
Charlie Davis,Coordinator,charlie@example.com,AUS,555-0102
|
||||
David Evans,Lead,david@example.com,NY,555-0103
|
||||
Eve Johnson,Manager,eve@example.com,LA,555-0104
|
||||
|
Generated
+1396
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "minimal-uploader",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "run.js",
|
||||
"scripts": {
|
||||
"start": "node run.js",
|
||||
"check": "node run.js --dry-run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^1.13.0",
|
||||
"dotenv": "^17.2.3",
|
||||
"node-fetch": "^2.6.7",
|
||||
"pdfkit": "^0.13.0",
|
||||
"csv-parse": "^5.4.1"
|
||||
,"xlsx": "^0.18.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,573 @@
|
||||
/* Minimal uploader: create folder, subfolder, upload PDF
|
||||
Uses app-only MSAL client credentials from environment variables.
|
||||
*/
|
||||
|
||||
// Load .env if present so this script can be run from the project folder
|
||||
try { require('dotenv').config(); } catch (e) {}
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const fetch = require('node-fetch');
|
||||
const { ConfidentialClientApplication } = require('@azure/msal-node');
|
||||
const XLSX = require('xlsx');
|
||||
|
||||
// Shared cell formatter used by PDF generators
|
||||
function formatCellValue(val) {
|
||||
if (val === null || val === undefined) return '';
|
||||
if (val instanceof Date && !isNaN(val.getTime())) {
|
||||
const mm = String(val.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(val.getDate()).padStart(2, '0');
|
||||
const yyyy = val.getFullYear();
|
||||
const hh = String(val.getHours()).padStart(2, '0');
|
||||
const mi = String(val.getMinutes()).padStart(2, '0');
|
||||
return `${mm}/${dd}/${yyyy} ${hh}:${mi}`;
|
||||
}
|
||||
if (typeof val === 'number') {
|
||||
try {
|
||||
const parsed = XLSX.SSF.parse_date_code(val);
|
||||
if (parsed && parsed.y) {
|
||||
const dt = new Date(Date.UTC(parsed.y, parsed.m - 1, parsed.d, parsed.H || 0, parsed.M || 0, parsed.S || 0));
|
||||
return formatCellValue(dt);
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
if (typeof val === 'string') {
|
||||
const s = val.trim();
|
||||
const maybe = Date.parse(s);
|
||||
if (!isNaN(maybe)) return formatCellValue(new Date(maybe));
|
||||
return s;
|
||||
}
|
||||
return String(val);
|
||||
}
|
||||
|
||||
const CLIENT_ID = process.env.CLIENT_ID;
|
||||
const CLIENT_SECRET = process.env.CLIENT_SECRET;
|
||||
const TENANT_ID = process.env.TENANT_ID;
|
||||
const DRIVE_ID = process.env.DRIVE_ID;
|
||||
// Support both PARENT_ITEM_ID (preferred) and ITEM_ID (legacy / alternate)
|
||||
const PARENT_ITEM_ID = process.env.PARENT_ITEM_ID || process.env.ITEM_ID;
|
||||
|
||||
const dryRun = process.argv.includes('--dry-run');
|
||||
|
||||
function assertEnv(name, val) {
|
||||
if (!val) {
|
||||
console.error(`Missing env var: ${name}`);
|
||||
process.exitCode = 2;
|
||||
throw new Error(`Missing env var ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
assertEnv('CLIENT_ID', CLIENT_ID);
|
||||
assertEnv('CLIENT_SECRET', CLIENT_SECRET);
|
||||
assertEnv('TENANT_ID', TENANT_ID);
|
||||
assertEnv('DRIVE_ID', DRIVE_ID);
|
||||
assertEnv('PARENT_ITEM_ID', PARENT_ITEM_ID);
|
||||
} catch (e) {
|
||||
console.error('Please set required environment values before running. See .env.example');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const msalConfig = {
|
||||
auth: {
|
||||
clientId: CLIENT_ID,
|
||||
authority: `https://login.microsoftonline.com/${TENANT_ID}`,
|
||||
clientSecret: CLIENT_SECRET
|
||||
}
|
||||
};
|
||||
|
||||
const cca = new ConfidentialClientApplication(msalConfig);
|
||||
|
||||
async function getAppToken() {
|
||||
const result = await cca.acquireTokenByClientCredential({ scopes: [ 'https://graph.microsoft.com/.default' ] });
|
||||
return result.accessToken;
|
||||
}
|
||||
|
||||
// Small made-up (valid-ish) PDF bytes (1-page blank PDF) base64
|
||||
const SAMPLE_PDF_BASE64 = Buffer.from(
|
||||
`%PDF-1.4\n%\u00e2\u00e3\u00cf\u00d3\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>\nendobj\n4 0 obj\n<< /Length 55 >>\nstream\nBT /F1 24 Tf 72 720 Td (Sample PDF uploaded by minimal-uploader) Tj ET\nendstream\nendobj\nxref\n0 5\n0000000000 65535 f\n0000000010 00000 n\n0000000066 00000 n\n0000000111 00000 n\n0000000200 00000 n\ntrailer<< /Size 5 /Root 1 0 R >>\nstartxref\n300\n%%EOF\n`).toString('base64');
|
||||
|
||||
// If managers.csv exists in the folder, build a PDF from it and return a base64 string
|
||||
const pathToCsv = path.join(__dirname, 'managers.csv');
|
||||
const { parse } = require('csv-parse/sync');
|
||||
const PDFDocument = require('pdfkit');
|
||||
|
||||
async function pdfFromCsvToBase64(csvPath) {
|
||||
if (!fs.existsSync(csvPath)) return null;
|
||||
const csvRaw = fs.readFileSync(csvPath, 'utf8');
|
||||
const records = parse(csvRaw, { columns: true, skip_empty_lines: true });
|
||||
|
||||
// Create PDF in memory using PDFKit
|
||||
const doc = new PDFDocument({ size: 'A4', margin: 40 });
|
||||
const buffers = [];
|
||||
doc.on('data', (chunk) => buffers.push(chunk));
|
||||
const title = 'Managers Info';
|
||||
doc.fontSize(16).text(title, { align: 'left' }).moveDown(0.5);
|
||||
|
||||
// table header
|
||||
const headers = Object.keys(records[0] || {});
|
||||
doc.fontSize(10);
|
||||
const columnWidths = headers.map(() => Math.floor((doc.page.width - 80) / headers.length));
|
||||
|
||||
// Render header row
|
||||
headers.forEach((h, i) => {
|
||||
doc.font('Helvetica-Bold').text(h, { continued: i !== headers.length - 1, width: columnWidths[i] });
|
||||
});
|
||||
doc.moveDown(0.25);
|
||||
doc.font('Helvetica');
|
||||
|
||||
// Render rows limited to 49 (A1:E49) but safe if fewer
|
||||
const limit = Math.min(records.length, 49);
|
||||
for (let r = 0; r < limit; r++) {
|
||||
const row = records[r];
|
||||
headers.forEach((h, i) => {
|
||||
const txt = row[h] !== undefined ? String(row[h]) : '';
|
||||
doc.text(txt, { continued: i !== headers.length - 1, width: columnWidths[i] });
|
||||
});
|
||||
doc.moveDown(0.1);
|
||||
}
|
||||
|
||||
doc.end();
|
||||
await new Promise((res) => doc.on('end', res));
|
||||
const pdfBuffer = Buffer.concat(buffers);
|
||||
return pdfBuffer.toString('base64');
|
||||
}
|
||||
|
||||
// build PDF from a records array (array of objects)
|
||||
async function pdfFromRecordsToBase64(records, title = 'Managers Info') {
|
||||
if (!records || records.length === 0) return null;
|
||||
|
||||
// Use the shared top-level formatCellValue helper
|
||||
|
||||
const doc = new PDFDocument({ size: 'A4', margin: 36 });
|
||||
const buffers = [];
|
||||
doc.on('data', (chunk) => buffers.push(chunk));
|
||||
doc.on('error', (e) => console.error('PDF generation error', e));
|
||||
|
||||
const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
||||
const startX = doc.x;
|
||||
let cursorY = doc.y;
|
||||
|
||||
// Title
|
||||
doc.font('Helvetica-Bold').fontSize(14).text(title, startX, cursorY);
|
||||
cursorY = doc.y + 8;
|
||||
|
||||
// Setup table
|
||||
const headers = Object.keys(records[0] || {});
|
||||
const columnCount = Math.max(1, headers.length);
|
||||
const baseWidth = Math.floor(pageWidth / columnCount);
|
||||
const columnWidths = headers.map((_, idx) => {
|
||||
if (columnCount >= 4 && idx === 0) return Math.floor(baseWidth * 1.5);
|
||||
if (columnCount >= 4 && idx === 1) return Math.floor(baseWidth * 1.25);
|
||||
return baseWidth;
|
||||
});
|
||||
|
||||
const tableX = startX;
|
||||
let tableY = cursorY;
|
||||
const rowPadding = 6;
|
||||
|
||||
// Header row
|
||||
const headerHeight = 20;
|
||||
doc.rect(tableX, tableY, pageWidth, headerHeight).fill('#f2f2f2');
|
||||
doc.fillColor('#000');
|
||||
let cx = tableX;
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
doc.font('Helvetica-Bold').fontSize(10).fillColor('#000').text(String(headers[i] || ''), cx + rowPadding, tableY + 6, { width: columnWidths[i] - rowPadding * 2 });
|
||||
cx += columnWidths[i];
|
||||
}
|
||||
doc.moveTo(tableX, tableY + headerHeight).lineTo(tableX + pageWidth, tableY + headerHeight).stroke();
|
||||
cursorY = tableY + headerHeight;
|
||||
|
||||
// Rows
|
||||
const limit = Math.min(records.length, 49);
|
||||
for (let r = 0; r < limit; r++) {
|
||||
const row = records[r];
|
||||
const cellTexts = headers.map(h => formatCellValue(row[h]));
|
||||
|
||||
const heights = cellTexts.map((t, i) => doc.heightOfString(t || '', { width: columnWidths[i] - rowPadding * 2 }));
|
||||
const rowHeight = Math.max(heights.reduce((a,b) => Math.max(a,b), 0), 12) + rowPadding * 2;
|
||||
|
||||
// page break
|
||||
if (cursorY + rowHeight + 40 > doc.page.height - doc.page.margins.bottom) {
|
||||
doc.addPage();
|
||||
cursorY = doc.page.margins.top;
|
||||
}
|
||||
|
||||
// background for alternating rows
|
||||
if (r % 2 === 1) {
|
||||
doc.rect(tableX, cursorY, pageWidth, rowHeight).fill('#fcfcfc');
|
||||
doc.fillColor('#000');
|
||||
}
|
||||
|
||||
cx = tableX;
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
doc.font('Helvetica').fontSize(9).fillColor('#000').text(cellTexts[i] || '', cx + rowPadding, cursorY + rowPadding, { width: columnWidths[i] - rowPadding * 2 });
|
||||
// vertical separator
|
||||
doc.moveTo(cx + columnWidths[i], cursorY).lineTo(cx + columnWidths[i], cursorY + rowHeight).stroke();
|
||||
cx += columnWidths[i];
|
||||
}
|
||||
|
||||
// bottom border
|
||||
doc.moveTo(tableX, cursorY + rowHeight).lineTo(tableX + pageWidth, cursorY + rowHeight).stroke();
|
||||
cursorY += rowHeight;
|
||||
}
|
||||
|
||||
doc.end();
|
||||
await new Promise((res) => doc.on('end', res));
|
||||
const pdfBuffer = Buffer.concat(buffers);
|
||||
return pdfBuffer.toString('base64');
|
||||
}
|
||||
|
||||
async function downloadExcelFromGraph(accessToken, driveId, itemId) {
|
||||
const url = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content`;
|
||||
const res = await fetch(url, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}` }, });
|
||||
if (!res.ok) throw new Error(`Download workbook failed: ${res.status} ${res.statusText}`);
|
||||
const ab = await res.arrayBuffer();
|
||||
return Buffer.from(ab);
|
||||
}
|
||||
|
||||
function parseManagersSheetFromWorkbookBuffer(buffer) {
|
||||
const workbook = XLSX.read(buffer, { type: 'buffer' });
|
||||
// try the obvious names, otherwise find any sheet name that contains 'manager'
|
||||
let sheet = workbook.Sheets['Managers Info'] || workbook.Sheets['Managers'] || null;
|
||||
if (!sheet) {
|
||||
const names = workbook.SheetNames || [];
|
||||
const match = names.find(n => /manager/i.test(n));
|
||||
if (match) sheet = workbook.Sheets[match];
|
||||
else {
|
||||
console.log('Workbook sheets:', names);
|
||||
}
|
||||
}
|
||||
if (!sheet) return null;
|
||||
// get A1:E49 region - using sheet_to_json with header row
|
||||
const options = { header: 1, range: 'A1:E49', blankrows: false, defval: '' };
|
||||
const rows = XLSX.utils.sheet_to_json(sheet, options);
|
||||
if (!rows || rows.length === 0) return null;
|
||||
// first row assumed to be headers
|
||||
const headers = rows[0].map(h => String(h).trim());
|
||||
const dataRows = rows.slice(1).map(r => {
|
||||
const obj = {};
|
||||
headers.forEach((h, i) => { obj[h || `col${i}`] = r[i] !== undefined ? r[i] : ''; });
|
||||
return obj;
|
||||
});
|
||||
return dataRows;
|
||||
}
|
||||
|
||||
// Helpers to read cells / ranges by address from a sheet
|
||||
function getCellValue(sheet, addr) {
|
||||
const cell = sheet[addr];
|
||||
if (!cell) return '';
|
||||
return cell.w || cell.v || '';
|
||||
}
|
||||
|
||||
function getRangeRows(sheet, rangeStr) {
|
||||
// decode range and return array of row arrays
|
||||
const r = XLSX.utils.decode_range(rangeStr);
|
||||
const rows = [];
|
||||
for (let R = r.s.r; R <= r.e.r; ++R) {
|
||||
const row = [];
|
||||
for (let C = r.s.c; C <= r.e.c; ++C) {
|
||||
const addr = XLSX.utils.encode_cell({ c: C, r: R });
|
||||
row.push(getCellValue(sheet, addr));
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Build PDF from workbook buffer with special region formatting (merged ranges and blocks)
|
||||
async function pdfFromWorkbookBufferToBase64(buffer) {
|
||||
const workbook = XLSX.read(buffer, { type: 'buffer' });
|
||||
// find a Managers-like sheet
|
||||
let sheetName = workbook.SheetNames.find(n => /Managers Info/i.test(n)) || workbook.SheetNames.find(n => /Managers/i.test(n));
|
||||
if (!sheetName) sheetName = workbook.SheetNames[0];
|
||||
const sheet = workbook.Sheets[sheetName];
|
||||
if (!sheet) return null;
|
||||
|
||||
// Prepare doc
|
||||
const doc = new PDFDocument({ size: 'A4', margin: 36 });
|
||||
const buffers = [];
|
||||
doc.on('data', (chunk) => buffers.push(chunk));
|
||||
|
||||
const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
||||
const leftX = doc.page.margins.left;
|
||||
let y = doc.y;
|
||||
|
||||
// Top title (sheet title)
|
||||
doc.font('Helvetica-Bold').fontSize(16).text(sheetName || 'Managers Info', { align: 'center' });
|
||||
y = doc.y + 6;
|
||||
|
||||
// 1) B2:D3 merged: center horizontally and bottom align vertically, 18pt bold dark-blue
|
||||
const b2Value = getCellValue(sheet, 'B2');
|
||||
if (b2Value) {
|
||||
// prefer Arial if available, otherwise use Helvetica-Bold as a safe built-in fallback
|
||||
let usedFont = null;
|
||||
try { doc.font('Arial'); usedFont = 'Arial'; } catch (e) { /* not available */ }
|
||||
if (!usedFont) {
|
||||
try { doc.font('Helvetica-Bold'); usedFont = 'Helvetica-Bold'; } catch (e) { /* ultimate fallback */ }
|
||||
}
|
||||
doc.fillColor('#003366').fontSize(18);
|
||||
// center the text inside about 60% of page width
|
||||
const boxW = Math.floor(pageWidth * 0.6);
|
||||
const x = leftX + Math.floor((pageWidth - boxW) / 2);
|
||||
// treat the merged region height as a small fixed box so we can bottom-align the text
|
||||
const boxH = 48; // reasonable height for two-row merged area
|
||||
// ensure there's enough space on the page
|
||||
if (y + boxH > doc.page.height - doc.page.margins.bottom) { doc.addPage(); y = doc.page.margins.top; }
|
||||
const h = doc.heightOfString(String(b2Value), { width: boxW });
|
||||
// place the text so its bottom is just above the box bottom (6px padding)
|
||||
const textY = y + Math.max(0, boxH - h - 6);
|
||||
doc.text(String(b2Value), x, textY, { width: boxW, align: 'center' });
|
||||
y += boxH + 8;
|
||||
doc.fillColor('#000'); doc.font('Helvetica');
|
||||
}
|
||||
|
||||
// Spacer
|
||||
y += 4;
|
||||
|
||||
// 2) B5:D5 merged left-bottom aligned (smaller title/label)
|
||||
const b5Value = getCellValue(sheet, 'B5');
|
||||
if (b5Value) {
|
||||
doc.font('Helvetica-Bold').fontSize(12).fillColor('#000');
|
||||
const boxW2 = Math.floor(pageWidth * 0.6);
|
||||
const boxH2 = 22; // single-row height approximation
|
||||
if (y + boxH2 > doc.page.height - doc.page.margins.bottom) { doc.addPage(); y = doc.page.margins.top; }
|
||||
// compute measured height and bottom align inside box
|
||||
const h2 = doc.heightOfString(String(b5Value), { width: boxW2 });
|
||||
const textY2 = y + Math.max(0, boxH2 - h2 - 4);
|
||||
doc.text(String(b5Value), leftX, textY2, { width: boxW2, align: 'left' });
|
||||
y += boxH2 + 6;
|
||||
}
|
||||
|
||||
// 3) B6:C9 cells -> render small grid block
|
||||
const block1 = getRangeRows(sheet, 'B6:C9');
|
||||
if (block1 && block1.length > 0) {
|
||||
// render as two-column box
|
||||
const colCount = block1[0].length;
|
||||
const columnW = Math.floor((pageWidth * 0.5) / Math.max(1, colCount));
|
||||
// header style omitted (these are raw cells)
|
||||
for (let r = 0; r < block1.length; r++) {
|
||||
const row = block1[r];
|
||||
let x = leftX;
|
||||
let maxH = 0;
|
||||
for (let c = 0; c < row.length; c++) {
|
||||
const txt = String(row[c] || '');
|
||||
const h = doc.heightOfString(txt, { width: columnW - 6 });
|
||||
maxH = Math.max(maxH, h);
|
||||
}
|
||||
// draw cells
|
||||
for (let c = 0; c < row.length; c++) {
|
||||
const txt = String(row[c] || '');
|
||||
doc.rect(x, y, columnW, maxH + 8).stroke();
|
||||
doc.text(txt, x + 4, y + 4, { width: columnW - 8 });
|
||||
x += columnW;
|
||||
}
|
||||
y += maxH + 10;
|
||||
}
|
||||
}
|
||||
|
||||
// 4) B11:D11 merged left bottom
|
||||
const b11Value = getCellValue(sheet, 'B11');
|
||||
if (b11Value) {
|
||||
doc.font('Helvetica').fontSize(11).fillColor('#000');
|
||||
doc.text(String(b11Value), leftX, y, { width: pageWidth * 0.6, align: 'left' });
|
||||
y = doc.y + 6;
|
||||
}
|
||||
|
||||
// 5) B12:C15 block
|
||||
const block2 = getRangeRows(sheet, 'B12:C15');
|
||||
if (block2 && block2.length > 0) {
|
||||
const colCount = block2[0].length;
|
||||
const columnW = Math.floor((pageWidth * 0.5) / Math.max(1, colCount));
|
||||
for (let r = 0; r < block2.length; r++) {
|
||||
const row = block2[r];
|
||||
let x = leftX;
|
||||
let maxH = 0;
|
||||
for (let c = 0; c < row.length; c++) {
|
||||
const txt = String(row[c] || '');
|
||||
const h = doc.heightOfString(txt, { width: columnW - 6 });
|
||||
maxH = Math.max(maxH, h);
|
||||
}
|
||||
for (let c = 0; c < row.length; c++) {
|
||||
const txt = String(row[c] || '');
|
||||
doc.rect(x, y, columnW, maxH + 8).stroke();
|
||||
doc.text(txt, x + 4, y + 4, { width: columnW - 8 });
|
||||
x += columnW;
|
||||
}
|
||||
y += maxH + 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Add small spacer
|
||||
y += 8;
|
||||
|
||||
// Now append the main table below (A1:E49 region) - reuse previous parseManagersSheet logic
|
||||
const rowsOpt = { header: 1, range: 'A1:E49', blankrows: false, defval: '' };
|
||||
const rows = XLSX.utils.sheet_to_json(sheet, rowsOpt);
|
||||
if (rows && rows.length > 1) {
|
||||
const headersTable = rows[0].map(h => String(h).trim());
|
||||
const dataRows = rows.slice(1).map(r => {
|
||||
const obj = {};
|
||||
headersTable.forEach((h, i) => { obj[h || `col${i}`] = r[i] !== undefined ? r[i] : ''; });
|
||||
return obj;
|
||||
});
|
||||
// render the rest using the existing pdfFromRecordsToBase64 but passing doc starting state isn't simple
|
||||
// so call pdfFromRecordsToBase64 to create a standalone PDF and then append? Simpler: render a simple table here.
|
||||
|
||||
// Use same table rendering approach as records conversion
|
||||
const tableHeaders = headersTable;
|
||||
const cCount = Math.max(1, tableHeaders.length);
|
||||
const tBaseWidth = Math.floor(pageWidth / cCount);
|
||||
const tColWidths = tableHeaders.map((_, idx) => tBaseWidth);
|
||||
|
||||
// draw headers
|
||||
if (y + 30 > doc.page.height - doc.page.margins.bottom) { doc.addPage(); y = doc.page.margins.top; }
|
||||
let tx = leftX;
|
||||
const th = 18;
|
||||
doc.rect(tx, y, pageWidth, th).fill('#f2f2f2'); doc.fillColor('#000');
|
||||
for (let i = 0; i < tableHeaders.length; i++) { doc.font('Helvetica-Bold').fontSize(10).text(tableHeaders[i] || '', tx + 4, y + 4, { width: tColWidths[i] - 8 }); tx += tColWidths[i]; }
|
||||
doc.moveTo(leftX, y + th).lineTo(leftX + pageWidth, y + th).stroke();
|
||||
y += th;
|
||||
|
||||
const maxRows = Math.min(dataRows.length, 49);
|
||||
for (let r = 0; r < maxRows; r++) {
|
||||
const row = dataRows[r];
|
||||
// compute row height
|
||||
const cellTexts = tableHeaders.map(h => formatCellValue(row[h]));
|
||||
const heights = cellTexts.map((t, i) => doc.heightOfString(t || '', { width: tColWidths[i] - 8 }));
|
||||
const rowH = Math.max(heights.reduce((a,b) => Math.max(a,b), 0), 12) + 8;
|
||||
if (y + rowH + 40 > doc.page.height - doc.page.margins.bottom) { doc.addPage(); y = doc.page.margins.top; }
|
||||
tx = leftX;
|
||||
for (let i = 0; i < tableHeaders.length; i++) {
|
||||
doc.font('Helvetica').fontSize(9).text(cellTexts[i] || '', tx + 4, y + 4, { width: tColWidths[i] - 8 });
|
||||
// vertical line
|
||||
doc.moveTo(tx + tColWidths[i], y).lineTo(tx + tColWidths[i], y + rowH).stroke();
|
||||
tx += tColWidths[i];
|
||||
}
|
||||
doc.moveTo(leftX, y + rowH).lineTo(leftX + pageWidth, y + rowH).stroke();
|
||||
y += rowH;
|
||||
}
|
||||
}
|
||||
|
||||
doc.end();
|
||||
await new Promise((res) => doc.on('end', res));
|
||||
const pdfBuffer = Buffer.concat(buffers);
|
||||
return pdfBuffer.toString('base64');
|
||||
}
|
||||
|
||||
async function createChildFolder(accessToken, parentDriveId, parentItemId, folderName) {
|
||||
const endpoint = `https://graph.microsoft.com/v1.0/drives/${parentDriveId}/items/${parentItemId}/children`;
|
||||
const body = { name: folderName, folder: {}, "@microsoft.graph.conflictBehavior": "rename" };
|
||||
if (dryRun) {
|
||||
console.log('[dry-run] Would POST', endpoint, 'body', body);
|
||||
return { id: 'dry-main-folder-id', name: folderName };
|
||||
}
|
||||
|
||||
const resp = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (!resp.ok) throw new Error(`Create folder failed: ${JSON.stringify(json)}`);
|
||||
return json;
|
||||
}
|
||||
|
||||
async function uploadFileToFolder(accessToken, driveId, folderId, fileName, base64pdf) {
|
||||
const endpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${folderId}:/${encodeURIComponent(fileName)}:/content`;
|
||||
const buffer = Buffer.from(base64pdf, 'base64');
|
||||
if (dryRun) {
|
||||
console.log('[dry-run] Would PUT', endpoint, `(${buffer.length} bytes)`);
|
||||
return { id: 'dry-upload-id', name: fileName, size: buffer.length };
|
||||
}
|
||||
|
||||
const resp = await fetch(endpoint, {
|
||||
method: 'PUT',
|
||||
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/pdf' },
|
||||
body: buffer
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (!resp.ok) throw new Error(`Upload failed: ${JSON.stringify(json)}`);
|
||||
return json;
|
||||
}
|
||||
|
||||
async function createShareLink(accessToken, driveId, itemId) {
|
||||
const endpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/createLink`;
|
||||
if (dryRun) {
|
||||
console.log('[dry-run] Would POST', endpoint, 'body', { type: 'view', scope: 'organization' });
|
||||
return { link: { webUrl: 'https://example.com/dry-share-link' } };
|
||||
}
|
||||
const resp = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type: 'view', scope: 'organization' })
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (!resp.ok) throw new Error(`Create share link failed: ${JSON.stringify(json)}`);
|
||||
return json;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
console.log('minimal-uploader starting', dryRun ? '(dry-run)' : '');
|
||||
const token = await getAppToken();
|
||||
console.log('acquired app token (truncated):', token ? token.substring(0, 30) + '...' : '(none)');
|
||||
|
||||
const mainFolderName = `0123 - Example Job (created ${new Date().toISOString()})`;
|
||||
// sanitize names to avoid invalid SharePoint characters (e.g. ':' in ISO timestamps)
|
||||
function sanitizeName(n) {
|
||||
// Replace characters that are invalid in SharePoint file/folder names
|
||||
// See: https://learn.microsoft.com/sharepoint/dev/general-development/avoid-using-invalid-file-name-characters
|
||||
return n.replace(/["\*\\\/:<>\?|#{}%~&]/g, '-')
|
||||
.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
const safeMainFolderName = sanitizeName(mainFolderName);
|
||||
const subFolderName = 'Managers Info';
|
||||
// Prefer generating from an Excel workbook if EXCEL_DRIVE_ID and EXCEL_ITEM_ID are present
|
||||
let csvBase64 = await pdfFromCsvToBase64(pathToCsv);
|
||||
if (process.env.EXCEL_DRIVE_ID && process.env.EXCEL_ITEM_ID) {
|
||||
console.log('EXCEL_DRIVE_ID/EXCEL_ITEM_ID present, downloading workbook from Graph to build PDF');
|
||||
try {
|
||||
const token1 = token; // app token used for Graph
|
||||
const workbookBuffer = await downloadExcelFromGraph(token1, process.env.EXCEL_DRIVE_ID, process.env.EXCEL_ITEM_ID);
|
||||
// Build a PDF directly from the workbook and its formatting/regions if possible
|
||||
const wbPdf = await pdfFromWorkbookBufferToBase64(workbookBuffer);
|
||||
if (wbPdf) {
|
||||
csvBase64 = wbPdf;
|
||||
// count rows if we can
|
||||
const recs = parseManagersSheetFromWorkbookBuffer(workbookBuffer) || [];
|
||||
console.log('Built PDF from workbook Managers Info with', recs.length, 'rows');
|
||||
} else {
|
||||
console.log('No Managers Info sheet / no rows found in workbook — falling back to managers.csv or static PDF');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to build PDF from workbook', e && e.message ? e.message : e);
|
||||
}
|
||||
}
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const fileName = sanitizeName(`Managers Info - ${timestamp}.pdf`);
|
||||
|
||||
console.log('Creating main folder under parent item', PARENT_ITEM_ID);
|
||||
const main = await createChildFolder(token, DRIVE_ID, PARENT_ITEM_ID, safeMainFolderName);
|
||||
console.log('main folder created:', main.id || mainName || JSON.stringify(main));
|
||||
|
||||
console.log('Creating subfolder under main folder', main.id);
|
||||
const sub = await createChildFolder(token, DRIVE_ID, main.id, subFolderName);
|
||||
console.log('subfolder created:', sub.id || JSON.stringify(sub));
|
||||
|
||||
console.log('Uploading PDF into subfolder', sub.id);
|
||||
const payloadBase64 = csvBase64 || SAMPLE_PDF_BASE64;
|
||||
if (!csvBase64) console.log('No managers.csv found - using fallback static PDF');
|
||||
const uploaded = await uploadFileToFolder(token, DRIVE_ID, sub.id, fileName, payloadBase64);
|
||||
console.log('uploaded result:', uploaded.id, uploaded.name, uploaded.size || '(size unknown)');
|
||||
|
||||
console.log('Creating share link for subfolder', sub.id);
|
||||
const share = await createShareLink(token, DRIVE_ID, sub.id);
|
||||
console.log('share link:', share && share.link && share.link.webUrl ? share.link.webUrl : JSON.stringify(share));
|
||||
|
||||
console.log('Done — created folder, subfolder and uploaded file.');
|
||||
}
|
||||
|
||||
run().catch(err => {
|
||||
console.error('Error running minimal-uploader:', err && err.message ? err.message : err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
$ErrorActionPreference = 'Continue'
|
||||
while ($true) {
|
||||
$t = (Get-Date).ToString('o')
|
||||
try {
|
||||
$r = curl.exe -k https://localhost:3001/health 2>&1
|
||||
Write-Output "$t - $r"
|
||||
} catch {
|
||||
Write-Output "$t - ERROR: $_"
|
||||
}
|
||||
Start-Sleep -Seconds 10
|
||||
}
|
||||
+1402
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^3.8.4",
|
||||
"body-parser": "^2.2.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.2.1",
|
||||
"node-fetch": "^2.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.11"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
|
||||
require("dotenv").config();
|
||||
|
||||
const tenantId = process.env.TENANT_ID;
|
||||
const clientId = process.env.CLIENT_ID;
|
||||
const clientSecret = process.env.CLIENT_SECRET;
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const https = require("https");
|
||||
const express = require("express");
|
||||
const cors = require("cors");
|
||||
const { ConfidentialClientApplication } = require("@azure/msal-node");
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
const app = express();
|
||||
|
||||
// Use built-in JSON parser
|
||||
app.use(express.json());
|
||||
|
||||
// Allow your task pane origin (HTTPS localhost:3000)
|
||||
app.use(cors({
|
||||
origin: "https://localhost:3000",
|
||||
methods: ["POST", "OPTIONS"],
|
||||
// frontend will send Authorization header when doing OBO/upload calls
|
||||
allowedHeaders: ["Content-Type", "Authorization"],
|
||||
}));
|
||||
|
||||
const msalConfig = {
|
||||
auth: {
|
||||
clientId,
|
||||
authority: `https://login.microsoftonline.com/${tenantId}`,
|
||||
clientSecret
|
||||
}
|
||||
};
|
||||
const cca = new ConfidentialClientApplication(msalConfig);
|
||||
|
||||
const driveId = process.env.DRIVE_ID;
|
||||
const parentItemId = process.env.PARENT_ITEM_ID;
|
||||
|
||||
// Helper to mask tokens in logs
|
||||
function maskToken(token) {
|
||||
if (!token || token.length < 12) return '***';
|
||||
return token.slice(0, 6) + '...' + token.slice(-6);
|
||||
}
|
||||
// Endpoint to receive a PDF (base64) and upload it to Graph using app credentials
|
||||
app.post('/api/upload-pdf', async (req, res) => {
|
||||
try {
|
||||
const { jobNumber, jobFullName, fileName, pdfBase64 } = req.body || {};
|
||||
console.log(`[upload-pdf] incoming request jobNumber=${jobNumber} fileName=${fileName}`);
|
||||
if (!jobNumber || !jobFullName || !fileName || !pdfBase64) {
|
||||
console.warn('[upload-pdf] missing required fields');
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
|
||||
// Require a user token (OBO) — reject requests without an Authorization: Bearer <token>
|
||||
const authHeader = (req.headers.authorization || req.headers.Authorization || '');
|
||||
if (!authHeader.toLowerCase().startsWith('bearer ')) {
|
||||
console.warn('[upload-pdf] no bearer token supplied; rejecting because OBO is required');
|
||||
return res.status(401).json({ error: 'Authorization bearer token required for OBO (user delegated access)' });
|
||||
}
|
||||
|
||||
const userAssertion = authHeader.substring(7).trim();
|
||||
console.log(`[upload-pdf] Authorization present, token=${maskToken(userAssertion)}`);
|
||||
|
||||
// Perform OBO exchange — fail fast if OBO fails since we now require OBO
|
||||
let accessToken;
|
||||
try {
|
||||
console.log('[upload-pdf] attempting OBO token exchange');
|
||||
const oboRequest = {
|
||||
oboAssertion: userAssertion,
|
||||
scopes: [
|
||||
'User.Read',
|
||||
'Files.ReadWrite.All',
|
||||
'Sites.ReadWrite.All'
|
||||
]
|
||||
};
|
||||
const oboResult = await cca.acquireTokenOnBehalfOf(oboRequest);
|
||||
accessToken = oboResult.accessToken;
|
||||
console.log('[upload-pdf] OBO exchange successful; token=' + maskToken(accessToken));
|
||||
} catch (oboErr) {
|
||||
console.error('[upload-pdf] OBO exchange failed:', oboErr && oboErr.message ? oboErr.message : oboErr);
|
||||
// include suberror/code if available for diagnostics
|
||||
const details = {
|
||||
error: oboErr && oboErr.errorCode ? oboErr.errorCode : undefined,
|
||||
suberror: oboErr && oboErr.suberror ? oboErr.suberror : undefined,
|
||||
message: oboErr && oboErr.message ? oboErr.message : String(oboErr)
|
||||
};
|
||||
return res.status(401).json({ error: 'OBO exchange failed', details });
|
||||
}
|
||||
|
||||
// 1) Create main folder under configured parent
|
||||
const mainFolderName = `${jobNumber} - ${jobFullName}`;
|
||||
const createFolderEndpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${parentItemId}/children`;
|
||||
console.log('[upload-pdf] creating main folder:', mainFolderName);
|
||||
const folderResp = await fetch(createFolderEndpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: mainFolderName, folder: {}, "@microsoft.graph.conflictBehavior": "rename" })
|
||||
});
|
||||
const folderData = await folderResp.json();
|
||||
console.log('[upload-pdf] create folder response', { status: folderResp.status, body: folderData });
|
||||
if (!folderResp.ok) {
|
||||
console.error('[upload-pdf] create main folder failed', folderData);
|
||||
return res.status(500).json({ error: 'Create main folder failed', details: folderData });
|
||||
}
|
||||
const mainFolderId = folderData.id;
|
||||
|
||||
// 2) Create subfolder Managers Info
|
||||
console.log('[upload-pdf] creating subfolder: Managers Info under', mainFolderId);
|
||||
const subFolderResp = await fetch(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${mainFolderId}/children`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: 'Managers Info', folder: {}, "@microsoft.graph.conflictBehavior": "rename" })
|
||||
});
|
||||
const subFolderData = await subFolderResp.json();
|
||||
console.log('[upload-pdf] create subfolder response', { status: subFolderResp.status, body: subFolderData });
|
||||
if (!subFolderResp.ok) {
|
||||
console.error('[upload-pdf] create subfolder failed', subFolderData);
|
||||
return res.status(500).json({ error: 'Create subfolder failed', details: subFolderData });
|
||||
}
|
||||
const subFolderId = subFolderData.id;
|
||||
|
||||
// 3) Upload PDF content
|
||||
const fileBuffer = Buffer.from(pdfBase64, 'base64');
|
||||
const uploadEndpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${subFolderId}:/${encodeURIComponent(fileName)}:/content`;
|
||||
console.log('[upload-pdf] uploading file to', uploadEndpoint, 'sizeBytes=', fileBuffer.length);
|
||||
const uploadResp = await fetch(uploadEndpoint, {
|
||||
method: 'PUT',
|
||||
headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/pdf' },
|
||||
body: fileBuffer
|
||||
});
|
||||
const uploadData = await uploadResp.json();
|
||||
console.log('[upload-pdf] upload response', { status: uploadResp.status, body: uploadData });
|
||||
if (!uploadResp.ok) {
|
||||
console.error('[upload-pdf] upload failed', uploadData);
|
||||
return res.status(500).json({ error: 'Upload failed', details: uploadData });
|
||||
}
|
||||
|
||||
// 4) Create share link for subfolder
|
||||
console.log('[upload-pdf] creating share link for subfolder', subFolderId);
|
||||
const shareResp = await fetch(`https://graph.microsoft.com/v1.0/drives/${driveId}/items/${subFolderId}/createLink`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type: 'view', scope: 'organization' })
|
||||
});
|
||||
const shareData = await shareResp.json();
|
||||
console.log('[upload-pdf] share link response', { status: shareResp.status, body: shareData });
|
||||
|
||||
res.json({ uploadedItem: uploadData, shareLink: shareData.link && shareData.link.webUrl ? shareData.link.webUrl : null });
|
||||
} catch (err) {
|
||||
console.error('[upload-pdf] error:', err && err.message ? err.message : err);
|
||||
res.status(500).json({ error: err.message || 'server error', details: err });
|
||||
}
|
||||
});
|
||||
|
||||
// Simple health check for local development
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', time: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// 🔐 OBO endpoint (delegated scopes)
|
||||
app.post("/api/obo-exchange", async (req, res) => {
|
||||
try {
|
||||
const { officeToken } = req.body || {};
|
||||
if (!officeToken) {
|
||||
return res.status(400).json({ error: "Missing officeToken" });
|
||||
}
|
||||
|
||||
const oboRequest = {
|
||||
oboAssertion: officeToken,
|
||||
// ✅ Use standard scope strings (delegated)
|
||||
scopes: [
|
||||
"User.Read",
|
||||
"Files.ReadWrite.All",
|
||||
"Sites.ReadWrite.All"
|
||||
]
|
||||
};
|
||||
|
||||
const result = await cca.acquireTokenOnBehalfOf(oboRequest);
|
||||
res.json({ graphToken: result.accessToken });
|
||||
} catch (err) {
|
||||
console.error("OBO exchange error:", err);
|
||||
const status = (err.status || err.statusCode || 500);
|
||||
res.status(status).json({
|
||||
error: err.message || "OBO failed",
|
||||
// helpful diagnostics in dev
|
||||
details: err.errorCode || err.code || undefined,
|
||||
suberror: err.suberror || undefined
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Try to start HTTPS using the dev cert created by office-addin-dev-certs.
|
||||
// If certs are not available, fall back to plain HTTP so the server doesn't crash at startup.
|
||||
const certDir = path.join(process.env.USERPROFILE || process.env.HOME, ".office-addin-dev-certs");
|
||||
const keyPath = path.join(certDir, "localhost.key");
|
||||
const certPath = path.join(certDir, "localhost.crt");
|
||||
|
||||
try {
|
||||
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
||||
const key = fs.readFileSync(keyPath);
|
||||
const cert = fs.readFileSync(certPath);
|
||||
https.createServer({ key, cert }, app).listen(3001, () => {
|
||||
console.log("✅ Backend listening on https://localhost:3001");
|
||||
});
|
||||
} else {
|
||||
throw new Error('Dev cert files not found');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('⚠️ Could not start HTTPS server on 3001:', err.message);
|
||||
console.warn('Starting HTTP server on port 3001 as a fallback. Update your frontend to use http://localhost:3001 if necessary.');
|
||||
app.listen(3001, () => {
|
||||
console.log('⚠️ Backend listening on http://localhost:3001 (insecure fallback)');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"jobNumber": "E2E-TEST",
|
||||
"jobFullName": "E2E Test",
|
||||
"fileName": "e2e-test.pdf",
|
||||
"pdfBase64": "JVBERi0xLjQKJYGBgYEKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9Db3VudCAxIC9LaWRzIFsgMyAwIFIgXSAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSA+PgplbmRvYmoKMyAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDIgMCBSIC9NZWRpYUJveCBbMCAwIDYxMiA3OTJdIC9Db250ZW50cyA0IDAgUiAvUmVzb3VyY2VzIDw8IC9Gb250IDw8IC9GMSA1IDAgUiA+PiA+PiA+PgplbmRvYmoKNCAwIG9iago8PCAvTGVuZ3RoIDY2ID4+CnN0cmVhbQpCBTAgMCB0ZiA8PC9GMSA0MCAvRjEgNDAgPj4KQk4KZW5kc3RyZWFtCmVuZG9iagp4cmVmCjAgNQowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAxMTAgMDAwMDAgbiAKMDAwMDAwMDY1IDAwMDAwIG4gCjAwMDAwMDAxNjAgMDAwMDAgbiAKMDAwMDAwMDIyMCAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDUgL1Jvb3QgMSAwIFIgL0luZm8gPDwgL1N1YmplY3QgPj4gPj4Kc3RhcnR4cmVmCjI0NQolJUVPRgo="
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "office-addin-taskpane-js",
|
||||
@@ -21,10 +20,10 @@
|
||||
"html-loader": "^5.0.0",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"office-addin-cli": "^2.0.3",
|
||||
"office-addin-debugging": "^6.0.3",
|
||||
"office-addin-dev-certs": "^2.0.3",
|
||||
"office-addin-debugging": "^6.0.6",
|
||||
"office-addin-dev-certs": "^2.0.6",
|
||||
"office-addin-lint": "^3.0.3",
|
||||
"office-addin-manifest": "^2.0.3",
|
||||
"office-addin-manifest": "^2.1.2",
|
||||
"office-addin-prettier-config": "^2.0.1",
|
||||
"os-browserify": "^0.3.0",
|
||||
"process": "^0.11.10",
|
||||
@@ -1814,10 +1813,6 @@
|
||||
|
||||
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
|
||||
|
||||
"@apidevtools/swagger-parser/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"@apidevtools/swagger-parser/ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" } }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
|
||||
|
||||
"@azure/arm-appservice/@azure/abort-controller": ["@azure/abort-controller@1.1.0", "", { "dependencies": { "tslib": "^2.2.0" } }, "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw=="],
|
||||
|
||||
"@azure/arm-resources/@azure/abort-controller": ["@azure/abort-controller@1.1.0", "", { "dependencies": { "tslib": "^2.2.0" } }, "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw=="],
|
||||
@@ -1826,8 +1821,6 @@
|
||||
|
||||
"@azure/identity/@azure/msal-node": ["@azure/msal-node@3.8.4", "", { "dependencies": { "@azure/msal-common": "15.13.3", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-lvuAwsDpPDE/jSuVQOBMpLbXuVuLsPNRwWCyK3/6bPlBk0fGWegqoZ0qjZclMWyQ2JNvIY3vHY7hoFmFmFQcOw=="],
|
||||
|
||||
"@azure/identity/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||
|
||||
"@azure/msal-browser/@azure/msal-common": ["@azure/msal-common@15.13.3", "", {}, "sha512-shSDU7Ioecya+Aob5xliW9IGq1Ui8y4EVSdWGyI1Gbm4Vg61WpP95LuzcY214/wEjSn6w4PZYD4/iVldErHayQ=="],
|
||||
|
||||
"@babel/helper-define-polyfill-provider/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
||||
@@ -1840,8 +1833,6 @@
|
||||
|
||||
"@inquirer/external-editor/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
|
||||
|
||||
"@microsoft/app-manifest/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"@microsoft/app-manifest/ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"@microsoft/kiota/adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="],
|
||||
@@ -1852,8 +1843,6 @@
|
||||
|
||||
"@microsoft/m365-spec-parser/@microsoft/app-manifest": ["@microsoft/app-manifest@1.0.3", "", { "dependencies": { "@types/fs-extra": "^11.0.1", "@types/node-fetch": "^2.6.9", "ajv": "^8.5.0", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "node-fetch": "2.7.0" } }, "sha512-51nJK5XkNGp+QsHsG5YBRK7XkJi2Tn7jfA4tffHN3fg0cfhgcjkbIAzFqbondYg7bj86aMJ/9ax4ndi40lHPmQ=="],
|
||||
|
||||
"@microsoft/m365-spec-parser/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="],
|
||||
|
||||
"@microsoft/m365agentstoolkit-cli/@inquirer/core": ["@inquirer/core@5.1.2", "", { "dependencies": { "@inquirer/type": "^1.1.6", "@types/mute-stream": "^0.0.4", "@types/node": "^20.10.7", "@types/wrap-ansi": "^3.0.0", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "cli-spinners": "^2.9.2", "cli-width": "^4.1.0", "figures": "^3.2.0", "mute-stream": "^1.0.0", "run-async": "^3.0.0", "signal-exit": "^4.1.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0" } }, "sha512-w3PMZH5rahrukn8/I7P9Ihil+twgLTUHDZtJlJyBbUKyPaOSSQjLZkb0PpncVhin1gCaMgOFXy6iNPgcZUoo2w=="],
|
||||
|
||||
"@microsoft/m365agentstoolkit-cli/@inquirer/prompts": ["@inquirer/prompts@6.0.1", "", { "dependencies": { "@inquirer/checkbox": "^3.0.1", "@inquirer/confirm": "^4.0.1", "@inquirer/editor": "^3.0.1", "@inquirer/expand": "^3.0.1", "@inquirer/input": "^3.0.1", "@inquirer/number": "^2.0.1", "@inquirer/password": "^3.0.1", "@inquirer/rawlist": "^3.0.1", "@inquirer/search": "^2.0.1", "@inquirer/select": "^3.0.1" } }, "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A=="],
|
||||
@@ -1868,18 +1857,12 @@
|
||||
|
||||
"@microsoft/m365agentstoolkit-cli/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"@microsoft/teams-manifest/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"@microsoft/teams-manifest/ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" } }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
|
||||
|
||||
"@microsoft/teams-manifest/ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"@microsoft/teams-manifest/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
||||
|
||||
"@microsoft/teamsfx-api/@microsoft/app-manifest": ["@microsoft/app-manifest@1.0.3", "", { "dependencies": { "@types/fs-extra": "^11.0.1", "@types/node-fetch": "^2.6.9", "ajv": "^8.5.0", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "node-fetch": "2.7.0" } }, "sha512-51nJK5XkNGp+QsHsG5YBRK7XkJi2Tn7jfA4tffHN3fg0cfhgcjkbIAzFqbondYg7bj86aMJ/9ax4ndi40lHPmQ=="],
|
||||
|
||||
"@microsoft/teamsfx-core/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"@microsoft/teamsfx-core/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="],
|
||||
|
||||
"@microsoft/teamsfx-core/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
@@ -1894,8 +1877,6 @@
|
||||
|
||||
"@types/express-serve-static-core/@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
@@ -1978,38 +1959,20 @@
|
||||
|
||||
"oas-validator/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
|
||||
|
||||
"office-addin-debugging/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
|
||||
|
||||
"office-addin-dev-certs/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
|
||||
|
||||
"office-addin-dev-settings/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
|
||||
|
||||
"office-addin-dev-settings/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="],
|
||||
|
||||
"office-addin-dev-settings/open": ["open@6.4.0", "", { "dependencies": { "is-wsl": "^1.1.0" } }, "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg=="],
|
||||
|
||||
"office-addin-dev-settings/whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="],
|
||||
|
||||
"office-addin-lint/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
|
||||
|
||||
"office-addin-manifest/adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="],
|
||||
|
||||
"office-addin-manifest/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
|
||||
|
||||
"office-addin-manifest/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="],
|
||||
|
||||
"office-addin-manifest-converter/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
|
||||
|
||||
"office-addin-node-debugger/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
|
||||
|
||||
"office-addin-node-debugger/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
||||
|
||||
"office-addin-project/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
|
||||
|
||||
"office-addin-project/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="],
|
||||
|
||||
"office-addin-usage-data/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
|
||||
|
||||
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"pkg-dir/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="],
|
||||
@@ -2058,22 +2021,10 @@
|
||||
|
||||
"wsl-utils/is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
|
||||
|
||||
"@apidevtools/swagger-parser/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@azure/identity/@azure/msal-node/@azure/msal-common": ["@azure/msal-common@15.13.3", "", {}, "sha512-shSDU7Ioecya+Aob5xliW9IGq1Ui8y4EVSdWGyI1Gbm4Vg61WpP95LuzcY214/wEjSn6w4PZYD4/iVldErHayQ=="],
|
||||
|
||||
"@azure/identity/open/define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
|
||||
|
||||
"@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"@microsoft/app-manifest/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@microsoft/app-manifest/ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"@microsoft/m365-spec-parser/@microsoft/app-manifest/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"@microsoft/m365-spec-parser/@microsoft/app-manifest/ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" } }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
|
||||
|
||||
"@microsoft/m365-spec-parser/@microsoft/app-manifest/ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"@microsoft/m365agentstoolkit-cli/@inquirer/core/mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="],
|
||||
@@ -2112,20 +2063,8 @@
|
||||
|
||||
"@microsoft/m365agentstoolkit-cli/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||
|
||||
"@microsoft/teams-manifest/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@microsoft/teams-manifest/ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"@microsoft/teamsfx-api/@microsoft/app-manifest/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"@microsoft/teamsfx-api/@microsoft/app-manifest/ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" } }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
|
||||
|
||||
"@microsoft/teamsfx-api/@microsoft/app-manifest/ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"@microsoft/teamsfx-core/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@microsoft/teamsfx-core/office-addin-manifest/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
|
||||
|
||||
"@microsoft/teamsfx-core/office-addin-manifest/commander": ["commander@6.2.1", "", {}, "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="],
|
||||
|
||||
"@microsoft/teamsfx-core/office-addin-manifest/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="],
|
||||
@@ -2212,20 +2151,12 @@
|
||||
|
||||
"serve-static/send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||
|
||||
"serve-static/send/mime": ["mime@1.6.0", "", { "bin": "cli.js" }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||
|
||||
"serve-static/send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
||||
|
||||
"webpack-dev-middleware/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"@microsoft/app-manifest/ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@microsoft/m365-spec-parser/@microsoft/app-manifest/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@microsoft/m365-spec-parser/@microsoft/app-manifest/ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"@microsoft/m365agentstoolkit-cli/@inquirer/prompts/@inquirer/checkbox/@inquirer/core": ["@inquirer/core@9.2.1", "", { "dependencies": { "@inquirer/figures": "^1.0.6", "@inquirer/type": "^2.0.0", "@types/mute-stream": "^0.0.4", "@types/node": "^22.5.5", "@types/wrap-ansi": "^3.0.0", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^1.0.0", "signal-exit": "^4.1.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" } }, "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg=="],
|
||||
|
||||
"@microsoft/m365agentstoolkit-cli/@inquirer/prompts/@inquirer/checkbox/@inquirer/type": ["@inquirer/type@2.0.0", "", { "dependencies": { "mute-stream": "^1.0.0" } }, "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag=="],
|
||||
@@ -2270,18 +2201,6 @@
|
||||
|
||||
"@microsoft/m365agentstoolkit-cli/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"@microsoft/teams-manifest/ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@microsoft/teamsfx-api/@microsoft/app-manifest/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@microsoft/teamsfx-api/@microsoft/app-manifest/ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"@microsoft/teamsfx-core/office-addin-manifest/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||
|
||||
"@microsoft/teamsfx-core/office-addin-manifest/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
||||
|
||||
"@microsoft/teamsfx-core/office-addin-manifest/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
|
||||
|
||||
"@microsoft/teamsfx-core/office-addin-manifest/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
|
||||
|
||||
"@microsoft/teamsfx-core/office-addin-manifest/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
|
||||
@@ -2306,8 +2225,6 @@
|
||||
|
||||
"wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"@microsoft/m365-spec-parser/@microsoft/app-manifest/ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@microsoft/m365agentstoolkit-cli/@inquirer/prompts/@inquirer/checkbox/@inquirer/core/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="],
|
||||
|
||||
"@microsoft/m365agentstoolkit-cli/@inquirer/prompts/@inquirer/checkbox/@inquirer/core/mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="],
|
||||
@@ -2370,12 +2287,6 @@
|
||||
|
||||
"@microsoft/m365agentstoolkit-cli/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"@microsoft/teamsfx-api/@microsoft/app-manifest/ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@microsoft/teamsfx-core/office-addin-manifest/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||
|
||||
"@microsoft/teamsfx-core/office-addin-manifest/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
|
||||
|
||||
"cliui/wrap-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"eslint/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
@@ -2386,8 +2297,6 @@
|
||||
|
||||
"pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="],
|
||||
|
||||
"@microsoft/teamsfx-core/office-addin-manifest/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||
|
||||
"import-local/pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||
|
||||
"pkg-dir/find-up/locate-path/p-locate/p-limit/yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="TaskPaneApp">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<OfficeApp
|
||||
xmlns="http://schemas.microsoft.com/office/appforoffice/1.1"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0"
|
||||
xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides"
|
||||
xsi:type="TaskPaneApp">
|
||||
|
||||
<Id>0b7d2846-9305-4e4b-906d-d73a6a4d286a</Id>
|
||||
<Version>1.0.0.0</Version>
|
||||
<ProviderName>Contoso</ProviderName>
|
||||
@@ -9,26 +15,37 @@
|
||||
<IconUrl DefaultValue="https://localhost:3000/assets/icon-32.png"/>
|
||||
<HighResolutionIconUrl DefaultValue="https://localhost:3000/assets/icon-64.png"/>
|
||||
<SupportUrl DefaultValue="https://www.contoso.com/help"/>
|
||||
<AppDomains>
|
||||
<AppDomain>https://www.contoso.com</AppDomain>
|
||||
|
||||
<AppDomains>
|
||||
<AppDomain>https://localhost:3000</AppDomain>
|
||||
</AppDomains>
|
||||
|
||||
<Hosts>
|
||||
<Host Name="Workbook"/>
|
||||
</Hosts>
|
||||
|
||||
<DefaultSettings>
|
||||
<SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/>
|
||||
</DefaultSettings>
|
||||
|
||||
<Permissions>ReadWriteDocument</Permissions>
|
||||
<VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0">
|
||||
|
||||
<VersionOverrides
|
||||
xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides"
|
||||
xsi:type="VersionOverridesV1_0">
|
||||
|
||||
<Hosts>
|
||||
<Host xsi:type="Workbook">
|
||||
<DesktopFormFactor>
|
||||
|
||||
<GetStarted>
|
||||
<Title resid="GetStarted.Title"/>
|
||||
<Description resid="GetStarted.Description"/>
|
||||
<LearnMoreUrl resid="GetStarted.LearnMoreUrl"/>
|
||||
</GetStarted>
|
||||
|
||||
<FunctionFile resid="Commands.Url"/>
|
||||
|
||||
<ExtensionPoint xsi:type="PrimaryCommandSurface">
|
||||
<OfficeTab id="TabHome">
|
||||
<Group id="CommandsGroup">
|
||||
@@ -57,47 +74,36 @@
|
||||
</Group>
|
||||
</OfficeTab>
|
||||
</ExtensionPoint>
|
||||
|
||||
</DesktopFormFactor>
|
||||
</Host>
|
||||
</Hosts>
|
||||
|
||||
<Resources>
|
||||
<bt:Images>
|
||||
<bt:Image id="Icon.16x16" DefaultValue="https://localhost:3000/assets/icon-16.png"/>
|
||||
<bt:Image id="Icon.32x32" DefaultValue="https://localhost:3000/assets/icon-32.png"/>
|
||||
<bt:Image id="Icon.80x80" DefaultValue="https://localhost:3000/assets/icon-80.png"/>
|
||||
</bt:Images>
|
||||
|
||||
<bt:Urls>
|
||||
<bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://go.microsoft.com/fwlink/?LinkId=276812"/>
|
||||
<bt:Url id="Commands.Url" DefaultValue="https://localhost:3000/commands.html"/>
|
||||
<bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html"/>
|
||||
<bt:Url id="CreateFolder.Url" DefaultValue="https://localhost:3000/createFolder.html"/>
|
||||
</bt:Urls>
|
||||
|
||||
<bt:ShortStrings>
|
||||
<bt:String id="GetStarted.Title" DefaultValue="Get started with your sample add-in!"/>
|
||||
<bt:String id="GetStarted.Title" DefaultValue="Get started with your add-in!"/>
|
||||
<bt:String id="CommandsGroup.Label" DefaultValue="Commands Group"/>
|
||||
<bt:String id="TaskpaneButton.Label" DefaultValue="Show Task Pane"/>
|
||||
</bt:ShortStrings>
|
||||
|
||||
<bt:LongStrings>
|
||||
<bt:String id="GetStarted.Description" DefaultValue="Your sample add-in loaded successfully. Go to the HOME tab and click the 'Show Task Pane' button to get started."/>
|
||||
<bt:String id="GetStarted.Description" DefaultValue="Your add-in loaded successfully. Go to the HOME tab and click 'Show Task Pane'."/>
|
||||
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Click to Show a Taskpane"/>
|
||||
</bt:LongStrings>
|
||||
</Resources>
|
||||
<Requirements>
|
||||
<Sets DefaultMinVersion="1.1">
|
||||
<Set Name="IdentityAPI" MinVersion="1.3"/>
|
||||
</Sets>
|
||||
</Requirements>
|
||||
<WebApplicationInfo>
|
||||
<!-- Application (client) ID from your Azure AD / Entra app registration -->
|
||||
<Id>3c846e71-9609-40e1-b458-0eb805e21b9f</Id>
|
||||
<!-- The resource/audience URI your app uses; common pattern is api://{clientId} -->
|
||||
<Resource>api://3c846e71-9609-40e1-b458-0eb805e21b9f</Resource>
|
||||
<!-- Scopes your backend will request via OBO to call Microsoft Graph -->
|
||||
<Scopes>
|
||||
<Scope>User.Read</Scope>
|
||||
<Scope>Files.ReadWrite.All</Scope>
|
||||
<Scope>Sites.ReadWrite.All</Scope>
|
||||
<!-- Add any other Graph scopes you need -->
|
||||
</Scopes>
|
||||
</WebApplicationInfo>
|
||||
|
||||
</VersionOverrides>
|
||||
</OfficeApp>
|
||||
+1602
-95
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.36.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^3.0.4",
|
||||
"office-js": "^0.1.0",
|
||||
"regenerator-runtime": "^0.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -42,10 +45,10 @@
|
||||
"html-loader": "^5.0.0",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"office-addin-cli": "^2.0.3",
|
||||
"office-addin-debugging": "^6.0.3",
|
||||
"office-addin-dev-certs": "^2.0.3",
|
||||
"office-addin-debugging": "^6.0.6",
|
||||
"office-addin-dev-certs": "^2.0.6",
|
||||
"office-addin-lint": "^3.0.3",
|
||||
"office-addin-manifest": "^2.0.3",
|
||||
"office-addin-manifest": "^2.1.2",
|
||||
"office-addin-prettier-config": "^2.0.1",
|
||||
"os-browserify": "^0.3.0",
|
||||
"process": "^0.11.10",
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const DIST_DIR = path.join(__dirname, 'dist');
|
||||
|
||||
const certDir = process.env.DEV_CERT_DIR || path.join(process.env.USERPROFILE || '~', '.office-addin-dev-certs');
|
||||
const certPath = path.join(certDir, 'localhost.crt');
|
||||
const keyPath = path.join(certDir, 'localhost.key');
|
||||
|
||||
function getContentType(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
switch (ext) {
|
||||
case '.html': return 'text/html; charset=utf-8';
|
||||
case '.js': return 'application/javascript; charset=utf-8';
|
||||
case '.css': return 'text/css; charset=utf-8';
|
||||
case '.png': return 'image/png';
|
||||
case '.jpg': case '.jpeg': return 'image/jpeg';
|
||||
case '.gif': return 'image/gif';
|
||||
case '.svg': return 'image/svg+xml';
|
||||
case '.json': return 'application/json';
|
||||
case '.ico': return 'image/x-icon';
|
||||
default: return 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
|
||||
let options;
|
||||
try {
|
||||
options = {
|
||||
cert: fs.readFileSync(certPath),
|
||||
key: fs.readFileSync(keyPath),
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Failed to read dev cert/key. Check DEV_CERT_DIR or ~/.office-addin-dev-certs:', err.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
const server = https.createServer(options, (req, res) => {
|
||||
// Normalize URL and prevent directory traversal
|
||||
const safeUrl = decodeURIComponent(req.url.split('?')[0] || '/');
|
||||
let filePath = path.join(DIST_DIR, safeUrl.replace(/^\//, ''));
|
||||
|
||||
// If request is for directory or root, fallback to index.html
|
||||
if (safeUrl === '/' || safeUrl.endsWith('/')) {
|
||||
filePath = path.join(DIST_DIR, 'taskpane.html');
|
||||
}
|
||||
|
||||
// If file doesn't exist, attempt to serve the file within dist, else 404
|
||||
fs.stat(filePath, (err, stats) => {
|
||||
if (err || !stats.isFile()) {
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*' });
|
||||
res.end('Not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const contentType = getContentType(filePath);
|
||||
res.writeHead(200, { 'Content-Type': contentType, 'Access-Control-Allow-Origin': '*' });
|
||||
const stream = fs.createReadStream(filePath);
|
||||
stream.pipe(res);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(PORT, '127.0.0.1', () => {
|
||||
console.log(`HTTPS static server running at https://localhost:${PORT}/ serving ${DIST_DIR}`);
|
||||
});
|
||||
|
||||
server.on('error', (err) => {
|
||||
console.error('Server error:', err.message || err);
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* MSAL Configuration for delegated user authentication
|
||||
* Used by Office Add-in to acquire user tokens with automatic refresh
|
||||
*/
|
||||
|
||||
export const msalConfig = {
|
||||
auth: {
|
||||
clientId: '3c846e71-9609-40e1-b458-0eb805e21b9f', // Replace with your app ID
|
||||
authority: 'https://login.microsoftonline.com/common', // Multi-tenant
|
||||
redirectUri: 'https://localhost:3000/createFolder.html', // Must match manifest
|
||||
},
|
||||
cache: {
|
||||
cacheLocation: 'localStorage', // Persists tokens across sessions
|
||||
storeAuthStateInCookie: false,
|
||||
},
|
||||
system: {
|
||||
loggerOptions: {
|
||||
loggerCallback: (level, message, containsPii) => {
|
||||
if (!containsPii) {
|
||||
try {
|
||||
// Forward MSAL logs to the add-in UI logger if available
|
||||
if (typeof window !== 'undefined' && window.__addinLog) {
|
||||
window.__addinLog(`[MSAL] ${message}`);
|
||||
} else {
|
||||
console.log(`[MSAL] ${message}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`[MSAL] ${message}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
piiLoggingEnabled: false,
|
||||
logLevel: 'Verbose',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Scopes required for the add-in
|
||||
* Files.ReadWrite.All: Create/read/write files in SharePoint
|
||||
*/
|
||||
export const loginRequest = {
|
||||
scopes: ['https://graph.microsoft.com/Files.ReadWrite.All'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Scopes for acquiring token for API calls (backend)
|
||||
*/
|
||||
export const tokenRequest = {
|
||||
scopes: ['https://graph.microsoft.com/Files.ReadWrite.All'],
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
# Copy this file to .env and fill values only if you implement server-side/OBO flows
|
||||
# Do NOT commit your real .env with secrets
|
||||
CLIENT_ID=your-client-id-here
|
||||
TENANT_ID=your-tenant-id-here
|
||||
CLIENT_SECRET=your-client-secret-here
|
||||
DRIVE_ID=your-drive-id-here
|
||||
PARENT_ITEM_ID=your-parent-item-id-here
|
||||
@@ -0,0 +1,34 @@
|
||||
CreateFolderOfficeAddIn (MVP)
|
||||
|
||||
Summary
|
||||
- Taskpane that reads the sheet named "Managers Info", generates a PDF of that sheet, creates a folder named `Job_Number - Job_Full_Name` under the provided parent folder, creates a subfolder `Managers Info`, uploads the PDF with a timestamped filename, and returns a share link.
|
||||
|
||||
Quick start (dev, paste-token flow)
|
||||
1. Start the dev server (project root `Create Job Folder`):
|
||||
|
||||
```powershell
|
||||
Set-Location 'C:\Users\jesus\Gitea\Job-Info\officeaddin\Create Job Folder'
|
||||
npm install
|
||||
npm run dev-server
|
||||
# or build for production
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. Open the page in the Office Add-in or browser (for testing):
|
||||
- https://localhost:3000/createFolder.html
|
||||
|
||||
3. In the add-in UI:
|
||||
- Enter `Job Number` and `Job Full Name`.
|
||||
- Paste a Graph delegated access token (must include `Files.ReadWrite.All`) into the token box. You can get a token from Graph Explorer or Postman using your account.
|
||||
- Click `Create folder, generate PDF and upload`.
|
||||
|
||||
Notes
|
||||
- Timestamp format used in filenames: `MM-DD-YYYY_HHMM` (local US time-style formatting).
|
||||
- Token is used in-memory only and not stored.
|
||||
- Do NOT commit real secrets to the repo. Use a local `.env` for any server-side flows.
|
||||
|
||||
Optional: .env
|
||||
- If you later add server-side/OBO flows, copy `.env.sample` to `.env` and populate the values (do not commit `.env`).
|
||||
|
||||
Contact
|
||||
- Tell me if you want me to add an MSAL sign-in flow (optional) so tokens are obtained automatically.
|
||||
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Permissions-Policy" content="geolocation=(), microphone=(), camera=()" />
|
||||
<title>Create Folder Add-in</title>
|
||||
<script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||
<!-- MSAL for user authentication (one-time sign-in, automatic token refresh) -->
|
||||
<script src="https://alcdn.msauth.net/browser/2.30.0/js/msal-browser.min.js"></script>
|
||||
<!-- html2canvas and jspdf are now bundled via webpack -->
|
||||
<style>
|
||||
body { font-family: Arial, Helvetica, sans-serif; padding: 12px; }
|
||||
button { padding:8px 12px; margin-top:8px; cursor: pointer; background: #0078d4; color: white; border: none; border-radius: 4px; }
|
||||
button:hover { background: #005a9e; }
|
||||
#preview { border:1px solid #ddd; padding:8px; margin-top:8px; max-width:800px; overflow:auto; display:none; }
|
||||
#statusMessage { margin-top: 12px; font-weight: bold; }
|
||||
#shareLink { margin-top: 8px; word-break: break-all; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Create Job Folder & Upload PDF</h2>
|
||||
<!-- Fatal error container (shown when the add-in cannot initialize) -->
|
||||
<div id="fatalError" role="alert" aria-live="assertive" style="display:none; background:#fff4f4; border:1px solid #f5c2c2; color:#8b0000; padding:10px; margin-top:10px; border-radius:6px;">
|
||||
<div style="font-size:120%; font-weight:700;">Could not be started</div>
|
||||
<strong>Sorry — we can't load the add-in right now.</strong>
|
||||
<div id="fatalErrorDetails" style="margin-top:6px; font-size:90%; color:#400000"></div>
|
||||
<div style="margin-top:8px; display:flex; gap:8px; align-items:center;">
|
||||
<button id="copyDiagnostics" style="padding:6px 8px; background:#444; color:white; border-radius:4px; border:none; cursor:pointer;">Copy diagnostics</button>
|
||||
<button id="downloadDiagnostics" style="padding:6px 8px; background:#2b6cb0; color:white; border-radius:4px; border:none; cursor:pointer;">Download logs</button>
|
||||
<span id="diagSaved" style="font-size:90%; color:#333; margin-left:8px; display:none">Saved</span>
|
||||
</div>
|
||||
<div style="margin-top:8px; font-size:90%">Try reloading the taskpane, sign out/in, or check your network connection.</div>
|
||||
</div>
|
||||
<p><strong>How it works:</strong></p>
|
||||
<ul>
|
||||
<li>Sign in once with your Microsoft account (automatic token refresh)</li>
|
||||
<li>Click the button to read job info from Excel and generate/upload PDF</li>
|
||||
<li>You become the file owner in SharePoint</li>
|
||||
</ul>
|
||||
|
||||
<button id="createBtn">📁 Create folder & upload PDF</button>
|
||||
<span id="spinner" role="status" aria-live="polite" style="display:none; margin-left:8px;">
|
||||
<svg width="18" height="18" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg" style="vertical-align:middle">
|
||||
<circle cx="25" cy="25" r="20" fill="none" stroke="#0078d4" stroke-width="5" stroke-linecap="round" stroke-dasharray="31.4 31.4">
|
||||
<animateTransform attributeName="transform" type="rotate" from="0 25 25" to="360 25 25" dur="1s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<p id="statusMessage"></p>
|
||||
|
||||
<div id="logContainer" style="margin-top:12px; max-height:160px; overflow:auto; background:#f9f9f9; border:1px solid #eee; padding:8px; font-family:monospace; font-size:12px;">
|
||||
<strong style="display:block; margin-bottom:6px;">Activity Log</strong>
|
||||
<div id="logArea"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>Share link:</strong>
|
||||
<div id="shareLink"></div>
|
||||
</div>
|
||||
|
||||
<div id="preview"></div>
|
||||
|
||||
<script src="createFolder.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,516 @@
|
||||
/* global Office, Excel, msal */
|
||||
|
||||
import { msalConfig, loginRequest, tokenRequest } from '../../msalConfig.js';
|
||||
import html2canvas from 'html2canvas';
|
||||
import jsPDF from 'jspdf';
|
||||
|
||||
// MSAL instance for user authentication (delegated flow)
|
||||
let msalInstance = null;
|
||||
let userToken = null;
|
||||
// In-memory buffer of diagnostic lines (kept even if DevTools closes).
|
||||
const DIAGNOSTIC_BUFFER_LIMIT = 1000;
|
||||
const diagBuffer = [];
|
||||
|
||||
// restore persisted diagnostics when available
|
||||
try {
|
||||
const saved = localStorage.getItem('addinDiagnostics');
|
||||
if (saved) {
|
||||
const arr = JSON.parse(saved);
|
||||
if (Array.isArray(arr)) {
|
||||
for (const l of arr) diagBuffer.push(l);
|
||||
}
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
function pushDiagnosticsLine(line) {
|
||||
try {
|
||||
const iso = new Date().toISOString();
|
||||
const msg = `${iso} - ${line}`;
|
||||
diagBuffer.push(msg);
|
||||
if (diagBuffer.length > DIAGNOSTIC_BUFFER_LIMIT) diagBuffer.splice(0, diagBuffer.length - DIAGNOSTIC_BUFFER_LIMIT);
|
||||
// persist a copy in localStorage so it survives crashes
|
||||
try { localStorage.setItem('addinDiagnostics', JSON.stringify(diagBuffer)); } catch (e) { /* ignore */ }
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
// Simple UI logger that writes to #logArea (and console)
|
||||
function addLog(message) {
|
||||
try {
|
||||
const t = new Date().toISOString();
|
||||
const line = `${t} - ${message}`;
|
||||
// console
|
||||
console.log(line);
|
||||
// in-page log
|
||||
const area = document && document.getElementById ? document.getElementById('logArea') : null;
|
||||
if (area) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = line;
|
||||
area.appendChild(div);
|
||||
// keep last ~200 lines
|
||||
while (area.childNodes.length > 200) area.removeChild(area.firstChild);
|
||||
// scroll into view
|
||||
area.scrollTop = area.scrollHeight;
|
||||
}
|
||||
// also record into diagnostics buffer
|
||||
pushDiagnosticsLine(message);
|
||||
} catch (e) {
|
||||
try { console.log('addLog error', e); } catch (e2) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Show a fatal error message in the UI and disable interactions
|
||||
function showFatalError(message, details) {
|
||||
try {
|
||||
console.error('[fatal] ', message, details || '');
|
||||
pushDiagnosticsLine('[FATAL] ' + message + ' | ' + (details ? (typeof details === 'string' ? details : JSON.stringify(details)) : ''));
|
||||
const el = document.getElementById('fatalError');
|
||||
const detailsEl = document.getElementById('fatalErrorDetails');
|
||||
if (el) el.style.display = 'block';
|
||||
// Always show a short 'Could not be started' title (for clarity)
|
||||
if (detailsEl) detailsEl.textContent = details ? (typeof details === 'string' ? details : JSON.stringify(details)) : message;
|
||||
// Also ensure the primary text includes 'Could not be started' for better clarity
|
||||
const primary = el && el.querySelector ? el.querySelector('strong') : null;
|
||||
if (primary) {
|
||||
primary.textContent = "Sorry — we can't load the add-in right now.";
|
||||
// add a secondary clarification line for 'Could not be started'
|
||||
// (the HTML shows a header already)
|
||||
}
|
||||
// disable the create button if present
|
||||
const createBtn = document.getElementById('createBtn');
|
||||
if (createBtn) {
|
||||
createBtn.disabled = true;
|
||||
createBtn.textContent = 'Unavailable';
|
||||
}
|
||||
const spinner = document.getElementById('spinner');
|
||||
if (spinner) spinner.style.display = 'none';
|
||||
// reveal stored diagnostics in the UI (helpful when Console closes)
|
||||
try {
|
||||
const logArea = document.getElementById('logArea');
|
||||
if (logArea) {
|
||||
// append last 200 diagnostic lines to the log area so user can copy
|
||||
const start = Math.max(0, diagBuffer.length - 200);
|
||||
for (let i = start; i < diagBuffer.length; i++) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = diagBuffer[i];
|
||||
logArea.appendChild(div);
|
||||
}
|
||||
logArea.scrollTop = logArea.scrollHeight;
|
||||
}
|
||||
} catch (e) { console.error('reveal diagnostics failed', e); }
|
||||
} catch (e) {
|
||||
console.error('Failed to render fatal error UI', e);
|
||||
}
|
||||
}
|
||||
|
||||
// expose shorthand for msalConfig loggerCallback to call
|
||||
if (typeof window !== 'undefined') window.__addinLog = addLog;
|
||||
|
||||
function formatError(err) {
|
||||
if (err instanceof Error) return err.message;
|
||||
return JSON.stringify(err, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize MSAL on page load
|
||||
* Sets up single sign-on so user only signs in once
|
||||
*/
|
||||
async function initMsal() {
|
||||
try {
|
||||
addLog('Initializing MSAL...');
|
||||
msalInstance = new msal.PublicClientApplication(msalConfig);
|
||||
|
||||
// Check if user is already signed in (from localStorage)
|
||||
const accounts = msalInstance.getAllAccounts();
|
||||
if (accounts.length > 0) {
|
||||
addLog(`User already signed in: ${accounts[0].username}`);
|
||||
// Try to acquire token silently
|
||||
await acquireTokenSilently();
|
||||
}
|
||||
} catch (error) {
|
||||
addLog('MSAL initialization error: ' + formatError(error));
|
||||
console.error('MSAL initialization error:', error);
|
||||
// Initialization failed — show a friendly fatal error to the user
|
||||
showFatalError("Sorry — we can't load the add-in right now.", error && (error.message || error));
|
||||
// Re-throw so callers are aware
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire access token silently (with refresh if needed)
|
||||
* MSAL handles token refresh automatically
|
||||
*/
|
||||
async function acquireTokenSilently() {
|
||||
try {
|
||||
const response = await msalInstance.acquireTokenSilent(tokenRequest);
|
||||
userToken = response.accessToken;
|
||||
addLog('Token acquired silently (auto-refreshed if needed)');
|
||||
return userToken;
|
||||
} catch (error) {
|
||||
if (error instanceof msal.InteractionRequiredAuthError) {
|
||||
addLog('Interactive sign-in required');
|
||||
return acquireTokenInteractive();
|
||||
} else {
|
||||
addLog('Silent token acquisition failed: ' + formatError(error));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactive sign-in (one-time, on first use)
|
||||
*/
|
||||
async function acquireTokenInteractive() {
|
||||
try {
|
||||
addLog('Prompting user to sign in...');
|
||||
const response = await msalInstance.loginPopup(loginRequest);
|
||||
userToken = response.accessToken;
|
||||
addLog(`User signed in: ${response.account.username}`);
|
||||
return userToken;
|
||||
} catch (error) {
|
||||
addLog('Interactive login failed: ' + formatError(error));
|
||||
throw new Error(`Sign-in failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Suppress Office.js debug handler prompt
|
||||
if (window.Office && window.Office.onReady) {
|
||||
window.Office.diagnostics = window.Office.diagnostics || {};
|
||||
window.Office.diagnostics.isDebugModeEnabled = false;
|
||||
}
|
||||
|
||||
Office.onReady(async () => {
|
||||
try {
|
||||
await initMsal();
|
||||
const createBtn = document.getElementById('createBtn');
|
||||
if (createBtn) createBtn.onclick = handleCreate;
|
||||
} catch (initErr) {
|
||||
// If initialization failed, show a friendly error — initMsal already calls showFatalError.
|
||||
console.error('Office.onReady initialization error:', initErr);
|
||||
showFatalError("Sorry — we can't load the add-in right now.", initErr && (initErr.message || initErr));
|
||||
}
|
||||
// wire diagnostics buttons even if init partially failed
|
||||
try { setupDiagnosticsButtons(); } catch (e) {}
|
||||
});
|
||||
|
||||
// Global handlers for uncaught errors / rejections to surface a friendly error message
|
||||
window.addEventListener('error', (ev) => {
|
||||
try {
|
||||
const msg = ev && ev.message ? ev.message : String(ev || 'Unknown error');
|
||||
pushDiagnosticsLine('[window.error] ' + msg + ' | ' + JSON.stringify(ev || {}));
|
||||
showFatalError("Sorry — we can't load the add-in right now.", msg);
|
||||
} catch (e) { console.error('global error handler failed', e); }
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (ev) => {
|
||||
try {
|
||||
const reason = ev && ev.reason ? ev.reason : ev;
|
||||
pushDiagnosticsLine('[unhandledrejection] ' + (reason && (reason.message || JSON.stringify(reason))));
|
||||
showFatalError("Sorry — we can't load the add-in right now.", reason && (reason.message || JSON.stringify(reason)));
|
||||
} catch (e) { console.error('unhandledrejection handler failed', e); }
|
||||
});
|
||||
|
||||
// --- diagnostics exporter + UI wiring ---
|
||||
function getDiagnosticsText() {
|
||||
const header = [];
|
||||
header.push('Add-in diagnostics');
|
||||
header.push('Time: ' + new Date().toISOString());
|
||||
header.push('Location: ' + (location && location.href));
|
||||
header.push('UserAgent: ' + (navigator && navigator.userAgent));
|
||||
try {
|
||||
const savedAcct = msalInstance && typeof msalInstance.getAllAccounts === 'function' ? msalInstance.getAllAccounts() : null;
|
||||
header.push('MSAL accounts: ' + (savedAcct ? JSON.stringify(savedAcct.map(a => ({ username: a.username, homeAccountId: a.homeAccountId }))) : 'none'));
|
||||
} catch(e) {}
|
||||
|
||||
const body = diagBuffer.join('\n');
|
||||
return header.join('\n') + '\n\n' + body;
|
||||
}
|
||||
|
||||
function setupDiagnosticsButtons() {
|
||||
try {
|
||||
const copyBtn = document.getElementById('copyDiagnostics');
|
||||
const downloadBtn = document.getElementById('downloadDiagnostics');
|
||||
const diagSaved = document.getElementById('diagSaved');
|
||||
if (copyBtn) {
|
||||
copyBtn.onclick = async () => {
|
||||
try {
|
||||
const text = getDiagnosticsText();
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
if (diagSaved) { diagSaved.style.display = 'inline'; diagSaved.textContent = 'Copied to clipboard'; setTimeout(() => { diagSaved.style.display = 'none'; }, 3500); }
|
||||
} else {
|
||||
const area = document.getElementById('logArea');
|
||||
if (area) { area.focus(); }
|
||||
if (diagSaved) { diagSaved.style.display = 'inline'; diagSaved.textContent = 'Copy the logs manually'; setTimeout(() => { diagSaved.style.display = 'none'; }, 3500); }
|
||||
}
|
||||
} catch(e) { console.error('copy diagnostics failed', e); }
|
||||
};
|
||||
}
|
||||
if (downloadBtn) {
|
||||
downloadBtn.onclick = () => {
|
||||
try {
|
||||
const text = getDiagnosticsText();
|
||||
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
const filename = `add-in-diagnostics-${new Date().toISOString().replace(/[:.]/g,'-')}.txt`;
|
||||
a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
if (diagSaved) { diagSaved.style.display = 'inline'; diagSaved.textContent = 'Downloaded'; setTimeout(() => { diagSaved.style.display = 'none'; }, 3500); }
|
||||
} catch(e) { console.error('download diagnostics failed', e); }
|
||||
};
|
||||
}
|
||||
} catch (e) { console.error('setupDiagnosticsButtons failed', e); }
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
const statusEl = document.getElementById('statusMessage');
|
||||
|
||||
try {
|
||||
// Ensure user is authenticated and token is valid
|
||||
if (!userToken) {
|
||||
statusEl.textContent = 'Signing in...';
|
||||
await acquireTokenSilently();
|
||||
}
|
||||
|
||||
statusEl.textContent = 'Reading Job info from workbook...';
|
||||
const { jobNumber, jobFullName } = await getJobInfoFromWorkbook();
|
||||
|
||||
statusEl.textContent = `Using Job: ${jobNumber} - ${jobFullName}`;
|
||||
|
||||
statusEl.textContent = 'Reading Managers Info sheet...';
|
||||
const managersHtml = await readManagersInfoAsHtml();
|
||||
|
||||
// show preview
|
||||
const preview = document.getElementById('preview');
|
||||
preview.style.display = 'block';
|
||||
preview.innerHTML = managersHtml;
|
||||
|
||||
statusEl.textContent = 'Generating PDF...';
|
||||
const pdfBlob = await generatePdfFromHtml(preview);
|
||||
statusEl.textContent = 'PDF generated.';
|
||||
|
||||
const timestamp = getTimestamp();
|
||||
const pdfFileName = `${jobNumber} - ${jobFullName} - ${timestamp}.pdf`;
|
||||
|
||||
const createBtn = document.getElementById('createBtn');
|
||||
const spinner = document.getElementById('spinner');
|
||||
if (createBtn) {
|
||||
createBtn.disabled = true;
|
||||
createBtn.textContent = 'Working...';
|
||||
}
|
||||
if (spinner) spinner.style.display = 'inline-block';
|
||||
statusEl.textContent = 'Sending PDF to backend for upload...';
|
||||
// convert blob to base64
|
||||
const arrayBuffer = await pdfBlob.arrayBuffer();
|
||||
const uint8 = new Uint8Array(arrayBuffer);
|
||||
let binary = '';
|
||||
for (let i = 0; i < uint8.byteLength; i++) binary += String.fromCharCode(uint8[i]);
|
||||
const base64 = btoa(binary);
|
||||
|
||||
// Backend endpoint - adjust if your backend uses HTTP fallback
|
||||
const backendEndpoint = `${location.protocol}//localhost:3001/api/upload-pdf`;
|
||||
console.log('Attempting to POST PDF to backend:', backendEndpoint);
|
||||
console.log('Has user token?', !!userToken);
|
||||
if (userToken) console.log('userToken (truncated):', userToken.substring(0, 60) + '...');
|
||||
|
||||
// Prepare payload
|
||||
const payload = JSON.stringify({ jobNumber, jobFullName, fileName: pdfFileName, pdfBase64: base64 });
|
||||
|
||||
// Use XMLHttpRequest to show upload progress to the user
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', backendEndpoint, true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
if (userToken) xhr.setRequestHeader('Authorization', `Bearer ${userToken}`);
|
||||
|
||||
xhr.upload.onprogress = function (ev) {
|
||||
if (ev.lengthComputable) {
|
||||
const pct = Math.round((ev.loaded / ev.total) * 100);
|
||||
statusEl.textContent = `Uploading PDF... ${pct}%`;
|
||||
} else {
|
||||
statusEl.textContent = 'Uploading PDF...';
|
||||
}
|
||||
};
|
||||
|
||||
const respText = await new Promise((resolve, reject) => {
|
||||
xhr.onerror = function (e) {
|
||||
reject(new Error('Network error during upload'));
|
||||
};
|
||||
xhr.onload = function () {
|
||||
resolve(xhr.responseText);
|
||||
};
|
||||
xhr.send(payload);
|
||||
});
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = JSON.parse(respText || '{}');
|
||||
} catch (parseErr) {
|
||||
console.error('Failed to parse JSON response from backend:', parseErr, 'raw:', respText);
|
||||
throw new Error('Invalid JSON response from backend');
|
||||
}
|
||||
|
||||
// If server returned an error structure, throw
|
||||
if (!result || (!result.uploadedItem && !result.shareLink)) {
|
||||
console.error('Unexpected backend response:', result);
|
||||
throw new Error('Upload failed: ' + JSON.stringify(result));
|
||||
}
|
||||
|
||||
const shareEl = document.getElementById('shareLink');
|
||||
// Show detailed success info
|
||||
if (result.shareLink) shareEl.innerHTML = `<a href="${result.shareLink}" target="_blank">${result.shareLink}</a>`;
|
||||
if (result.uploadedItem) {
|
||||
const info = result.uploadedItem;
|
||||
const details = `Uploaded: ${info.name} (${info.size} bytes) — ${info.webUrl || ''}`;
|
||||
statusEl.textContent = '✅ PDF uploaded successfully!';
|
||||
// append a smaller detail line
|
||||
const small = document.createElement('div');
|
||||
small.style.fontSize = '90%';
|
||||
small.style.marginTop = '6px';
|
||||
small.textContent = details;
|
||||
statusEl.appendChild(small);
|
||||
} else {
|
||||
statusEl.textContent = '✅ Upload complete.';
|
||||
}
|
||||
|
||||
if (spinner) spinner.style.display = 'none';
|
||||
if (createBtn) {
|
||||
createBtn.disabled = false;
|
||||
createBtn.textContent = '📁 Create folder & upload PDF';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Create error', err);
|
||||
const statusEl = document.getElementById('statusMessage');
|
||||
statusEl.textContent = 'Error: ' + formatError(err);
|
||||
// restore UI state
|
||||
const createBtn = document.getElementById('createBtn');
|
||||
const spinner = document.getElementById('spinner');
|
||||
if (spinner) spinner.style.display = 'none';
|
||||
if (createBtn) {
|
||||
createBtn.disabled = false;
|
||||
createBtn.textContent = '📁 Create folder & upload PDF';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read job info from 'Job Sheet' as the first data row.
|
||||
// Structure assumed similar to original script: header row starts at A3 with column names.
|
||||
// If not found, fallback to 'Test' + timestamp.
|
||||
async function getJobInfoFromWorkbook() {
|
||||
try {
|
||||
return Excel.run(async (context) => {
|
||||
const sheet = context.workbook.worksheets.getItemOrNullObject('Job Sheet');
|
||||
await context.sync();
|
||||
if (sheet.isNullObject) {
|
||||
// fallback
|
||||
const ts = getTimestamp();
|
||||
return { jobNumber: `Test`, jobFullName: `Test ${ts}` };
|
||||
}
|
||||
|
||||
const headerStart = sheet.getRange("A3");
|
||||
const headerRegion = headerStart.getSurroundingRegion();
|
||||
headerRegion.load("values, rowCount, columnCount");
|
||||
await context.sync();
|
||||
|
||||
const headers = headerRegion.values && headerRegion.values[0] ? headerRegion.values[0] : [];
|
||||
if (!headers || headers.length === 0) {
|
||||
const ts = getTimestamp();
|
||||
return { jobNumber: `Test`, jobFullName: `Test ${ts}` };
|
||||
}
|
||||
|
||||
// Get first data row below header
|
||||
const dataRange = headerRegion.getOffsetRange(1, 0).getResizedRange(1, headerRegion.columnCount - 1);
|
||||
dataRange.load("values");
|
||||
await context.sync();
|
||||
|
||||
const row = (dataRange.values && dataRange.values[0]) ? dataRange.values[0] : [];
|
||||
|
||||
const normalize = h => h ? h.toString().trim().toLowerCase() : "";
|
||||
const idxJobNumber = headers.findIndex(h => normalize(h) === "job_number" || normalize(h) === "job number");
|
||||
const idxJobName = headers.findIndex(h => normalize(h) === "job_name" || normalize(h) === "job name" || normalize(h) === "job_name");
|
||||
|
||||
const jobNumber = (idxJobNumber >= 0 && row[idxJobNumber]) ? String(row[idxJobNumber]).trim() : null;
|
||||
const jobFullName = (idxJobName >= 0 && row[idxJobName]) ? String(row[idxJobName]).trim() : null;
|
||||
|
||||
if (jobNumber && jobFullName) return { jobNumber, jobFullName };
|
||||
|
||||
// fallback
|
||||
const ts = getTimestamp();
|
||||
return {
|
||||
jobNumber: jobNumber || `Test`,
|
||||
jobFullName: jobFullName || `Test ${ts}`
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
const ts = getTimestamp();
|
||||
return { jobNumber: 'Test', jobFullName: `Test ${ts}` };
|
||||
}
|
||||
}
|
||||
|
||||
function getTimestamp() {
|
||||
const now = new Date();
|
||||
const pad = (n) => n.toString().padStart(2, '0');
|
||||
const mm = pad(now.getMonth() + 1);
|
||||
const dd = pad(now.getDate());
|
||||
const yyyy = now.getFullYear();
|
||||
const hh = pad(now.getHours());
|
||||
const min = pad(now.getMinutes());
|
||||
return `${mm}-${dd}-${yyyy}_${hh}${min}`;
|
||||
}
|
||||
|
||||
async function readManagersInfoAsHtml() {
|
||||
return Excel.run(async (context) => {
|
||||
const sheet = context.workbook.worksheets.getItemOrNullObject('Managers Info');
|
||||
await context.sync();
|
||||
if (sheet.isNullObject) throw new Error('Worksheet \"Managers Info\" not found.');
|
||||
|
||||
const range = sheet.getUsedRange();
|
||||
range.load(['values', 'rowCount', 'columnCount']);
|
||||
await context.sync();
|
||||
|
||||
const values = range.values;
|
||||
if (!values || values.length === 0) throw new Error('No data in Managers Info sheet.');
|
||||
|
||||
// build HTML table
|
||||
let html = '<table class="data-table">';
|
||||
for (let r = 0; r < values.length; r++) {
|
||||
html += '<tr>';
|
||||
for (let c = 0; c < values[r].length; c++) {
|
||||
const cell = values[r][c] !== null && values[r][c] !== undefined ? values[r][c] : '';
|
||||
if (r === 0) html += `<th>${escapeHtml(cell)}</th>`;
|
||||
else html += `<td>${escapeHtml(cell)}</td>`;
|
||||
}
|
||||
html += '</tr>';
|
||||
}
|
||||
html += '</table><style>table.data-table { border-collapse: collapse; width: 100%; } table.data-table th, table.data-table td { padding: 6px; border: 1px solid #ddd; }</style>';
|
||||
return html;
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return String(s).replace(/[&<>"']/g, (m) => map[m]);
|
||||
}
|
||||
|
||||
async function generatePdfFromHtml(element) {
|
||||
// Use html2canvas and jsPDF (imported as modules, not global)
|
||||
const canvas = await html2canvas(element, { scale: 2 });
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
const pdf = new jsPDF({ unit: 'pt', format: 'a4' });
|
||||
|
||||
const pageWidth = pdf.internal.pageSize.getWidth();
|
||||
const imgWidth = pageWidth - 40; // margins
|
||||
const imgProps = canvas.width ? (canvas.height / canvas.width) : 1;
|
||||
const imgHeight = imgWidth * imgProps;
|
||||
|
||||
pdf.addImage(imgData, 'PNG', 20, 20, imgWidth, imgHeight);
|
||||
|
||||
const blob = pdf.output('blob');
|
||||
return blob;
|
||||
}
|
||||
@@ -1,63 +1,32 @@
|
||||
<!-- Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. -->
|
||||
<!-- This file shows how to design a first-run page that provides a welcome screen to the user about the features of the add-in. -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Contoso Task Pane Add-in</title>
|
||||
|
||||
<!-- Office JavaScript API -->
|
||||
<script type="text/javascript" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||
|
||||
<!-- For more information on Fluent UI, visit https://developer.microsoft.com/fluentui#/. -->
|
||||
<link rel="stylesheet" href="https://res-1.cdn.office.net/files/fabric-cdn-prod_20230815.002/office-ui-fabric-core/11.1.0/css/fabric.min.css"/>
|
||||
|
||||
<!-- Template styles -->
|
||||
<link href="taskpane.css" rel="stylesheet" type="text/css" />
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Permissions-Policy" content="geolocation=(), microphone=(), camera=()" />
|
||||
<title>Create Folder Add-in</title>
|
||||
<script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||
<!-- html2canvas and jspdf are now bundled via webpack -->
|
||||
<style>
|
||||
body { font-family: Arial, Helvetica, sans-serif; padding: 12px; }
|
||||
button { padding:8px 12px; margin-top:8px; }
|
||||
#preview { border:1px solid #ddd; padding:8px; margin-top:8px; max-width:800px; overflow:auto; display:none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>CreateFolderOfficeAddIn (MVP)</h2>
|
||||
|
||||
<body class="ms-font-m ms-welcome ms-Fabric">
|
||||
<p>Click the button to create a folder from the workbook data and upload the Managers Info PDF.</p>
|
||||
<button id="createBtn">Create folder, generate PDF and upload</button>
|
||||
|
||||
<div class="container">
|
||||
<h3>Create Project Folder</h3>
|
||||
<input type="text" id="projectName" placeholder="Enter Job Number" />
|
||||
<button id="createFolderBtn" class="action-btn">Create Folder & Document</button>
|
||||
<p id="statusMessage"></p>
|
||||
</div>
|
||||
<p id="statusMessage"></p>
|
||||
|
||||
<header class="ms-welcome__header ms-bgColor-neutralLighter">
|
||||
<img width="90" height="90" src="../../assets/logo-filled.png" alt="Contoso" title="Contoso" />
|
||||
<h1 class="ms-font-su">Welcome</h1>
|
||||
</header>
|
||||
<section id="sideload-msg" class="ms-welcome__main">
|
||||
<h2 class="ms-font-xl">Please <a target="_blank" href="https://learn.microsoft.com/office/dev/add-ins/testing/test-debug-office-add-ins#sideload-an-office-add-in-for-testing">sideload</a> your add-in to see app body.</h2>
|
||||
</section>
|
||||
<main id="app-body" class="ms-welcome__main" style="display: none;">
|
||||
<h2 class="ms-font-xl"> Discover what Office Add-ins can do for you today! </h2>
|
||||
<ul class="ms-List ms-welcome__features">
|
||||
<li class="ms-ListItem">
|
||||
<i class="ms-Icon ms-Icon--Ribbon ms-font-xl"></i>
|
||||
<span class="ms-font-m">Achieve more with Office integration</span>
|
||||
</li>
|
||||
<li class="ms-ListItem">
|
||||
<i class="ms-Icon ms-Icon--Unlock ms-font-xl"></i>
|
||||
<span class="ms-font-m">Unlock features and functionality</span>
|
||||
</li>
|
||||
<li class="ms-ListItem">
|
||||
<i class="ms-Icon ms-Icon--Design ms-font-xl"></i>
|
||||
<span class="ms-font-m">Create and visualize like a pro</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="ms-font-l">Modify the source files, then click <b>Run</b>.</p>
|
||||
<div role="button" id="run" class="ms-welcome__action ms-Button ms-Button--hero ms-font-xl">
|
||||
<span class="ms-Button-label">Run</span>
|
||||
</div>
|
||||
<p><label id="item-subject"></label></p>
|
||||
</main>
|
||||
<div>
|
||||
<strong>Share link:</strong>
|
||||
<div id="shareLink"></div>
|
||||
</div>
|
||||
|
||||
<div id="preview"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,252 +1,304 @@
|
||||
/*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||
* See LICENSE in the project root for license information.
|
||||
*/
|
||||
/* global console, document, Excel, Word, Office */
|
||||
|
||||
/* global console, document, Excel, Office */
|
||||
|
||||
window.addEventListener('error', (evt) => {
|
||||
console.error('WindowError:', {
|
||||
message: evt.message,
|
||||
filename: evt.filename,
|
||||
lineno: evt.lineno,
|
||||
colno: evt.colno,
|
||||
error: evt.error, // often the real Error object
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (evt) => {
|
||||
console.error('UnhandledRejection:', {
|
||||
reason: evt.reason, // might be the original Error
|
||||
});
|
||||
});
|
||||
function formatError(err) {
|
||||
if (err instanceof Error) return err.message;
|
||||
return JSON.stringify(err, null, 2);
|
||||
}
|
||||
|
||||
Office.onReady((info) => {
|
||||
if (info.host === Office.HostType.Excel) {
|
||||
document.getElementById("sideload-msg").style.display = "none";
|
||||
document.getElementById("app-body").style.display = "flex";
|
||||
document.getElementById("run").onclick = run;
|
||||
OfficeRuntime.auth.getAccessToken().then(token => console.log(token));
|
||||
document.getElementById("createFolderBtn").onclick = handleCreatePDF;
|
||||
const sideloadMsg = document.getElementById("sideload-msg");
|
||||
const appBody = document.getElementById("app-body");
|
||||
const runBtn = document.getElementById("run");
|
||||
const createBtn = document.getElementById("createFolderBtn");
|
||||
|
||||
if (sideloadMsg) sideloadMsg.style.display = "none";
|
||||
if (appBody) appBody.style.display = "flex";
|
||||
if (runBtn) runBtn.onclick = run;
|
||||
if (createBtn) createBtn.onclick = handleCreatePDF;
|
||||
}
|
||||
});
|
||||
|
||||
// Get Graph token via backend OBO exchange
|
||||
async function getGraphToken() {
|
||||
return new Promise((resolve, reject) => {
|
||||
Office.context.auth.getAccessTokenAsync({ allowSignInPrompt: true }, async (result) => {
|
||||
if (result.status === "succeeded") {
|
||||
const officeToken = result.value;
|
||||
try {
|
||||
const response = await fetch("http://localhost:3001/api/obo-exchange", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ officeToken })
|
||||
});
|
||||
const { graphToken } = await response.json();
|
||||
resolve(graphToken);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
} else {
|
||||
reject(new Error(result.error.message));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function handleCreatePDF() {
|
||||
const projectInput = document.getElementById("projectName").value.trim();
|
||||
const statusEl = document.getElementById("statusMessage");
|
||||
const projectInputEl = document.getElementById("projectNumber");
|
||||
const statusEl = document.getElementById("statusMessage");
|
||||
if (!projectInputEl || !statusEl) return;
|
||||
|
||||
if (!projectInput) {
|
||||
statusEl.textContent = "Please enter a project number.";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
statusEl.textContent = "Processing...";
|
||||
|
||||
// 1. Get Excel data
|
||||
const jobData = await getJobData(projectInput);
|
||||
if (!jobData) {
|
||||
statusEl.textContent = "Job not found.";
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = await getFormData();
|
||||
|
||||
// 2. Get access token for Graph
|
||||
const accessToken = await getAccessToken();
|
||||
|
||||
// 3. Hardcoded SharePoint IDs
|
||||
const driveId = "b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx";
|
||||
const parentItemId = "01SPNXLDW3Y56HAWK5CNAJ4YJLLSQCUCJ7";
|
||||
|
||||
// 4. Create folder structure
|
||||
const mainFolderName = `[ ${jobData.Job_Number} ] - ${jobData.Job_Name}`;
|
||||
const mainFolderId = await createSharePointFolder(accessToken, driveId, parentItemId, mainFolderName);
|
||||
const subFolderId = await createSharePointFolder(accessToken, driveId, mainFolderId, "Manager Info");
|
||||
|
||||
// 5. Generate Word document and export as PDF
|
||||
const pdfBlob = await generatePDF(jobData, formData);
|
||||
|
||||
// 6. Upload PDF to SharePoint
|
||||
await uploadFileToSharePoint(accessToken, driveId, subFolderId, `${mainFolderName}.pdf`, pdfBlob);
|
||||
|
||||
statusEl.textContent = "✅ PDF created and uploaded successfully!";
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
statusEl.textContent = "❌ Error: " + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Get Job Data from "Job Sheet"
|
||||
async function getJobData(jobNumber) {
|
||||
return Excel.run(async (context) => {
|
||||
const sheet = context.workbook.worksheets.getItem("Job Sheet");
|
||||
const usedRange = sheet.getUsedRange();
|
||||
usedRange.load("values");
|
||||
await context.sync();
|
||||
|
||||
const headers = usedRange.values[0];
|
||||
const rows = usedRange.values.slice(1);
|
||||
|
||||
const idxJobNumber = headers.indexOf("Job_Number");
|
||||
const idxJobName = headers.indexOf("Job_Name");
|
||||
const idxJobAddress = headers.indexOf("Job_Address");
|
||||
const idxJobDivision = headers.indexOf("Job_Division");
|
||||
const idxClientCustomer = headers.indexOf("Client_Customer");
|
||||
const idxContactPerson = headers.indexOf("Contact_Person");
|
||||
const idxEmail = headers.indexOf("Email");
|
||||
|
||||
const match = rows.find(r => r[idxJobNumber] == jobNumber);
|
||||
if (!match) return null;
|
||||
|
||||
return {
|
||||
Job_Number: match[idxJobNumber],
|
||||
Job_Name: match[idxJobName],
|
||||
Job_Address: match[idxJobAddress],
|
||||
Job_Division: match[idxJobDivision],
|
||||
Client_Customer: match[idxClientCustomer],
|
||||
Contact_Person: match[idxContactPerson],
|
||||
Email: match[idxEmail]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Get FormDat range
|
||||
async function getFormData() {
|
||||
return Excel.run(async (context) => {
|
||||
const sheet = context.workbook.worksheets.getItem("FormDat");
|
||||
const range = sheet.getRange("A1:C12");
|
||||
range.load("values");
|
||||
await context.sync();
|
||||
return range.values;
|
||||
});
|
||||
}
|
||||
|
||||
// Generate PDF using Word API
|
||||
async function generatePDF(jobData, formData) {
|
||||
return Word.run(async (context) => {
|
||||
const body = context.document.body;
|
||||
body.clear();
|
||||
|
||||
body.insertParagraph("Project Information", Word.InsertLocation.start);
|
||||
body.insertParagraph(`Job Number: ${jobData.Job_Number}`, Word.InsertLocation.end);
|
||||
body.insertParagraph(`Job Name: ${jobData.Job_Name}`, Word.InsertLocation.end);
|
||||
body.insertParagraph(`Address: ${jobData.Job_Address}`, Word.InsertLocation.end);
|
||||
body.insertParagraph(`Division: ${jobData.Job_Division}`, Word.InsertLocation.end);
|
||||
body.insertParagraph(`Client: ${jobData.Client_Customer}`, Word.InsertLocation.end);
|
||||
body.insertParagraph(`Contact: ${jobData.Contact_Person}`, Word.InsertLocation.end);
|
||||
body.insertParagraph(`Email: ${jobData.Email}`, Word.InsertLocation.end);
|
||||
|
||||
body.insertParagraph("\nForm Data:", Word.InsertLocation.end);
|
||||
formData.forEach(row => {
|
||||
body.insertParagraph(row.join(" | "), Word.InsertLocation.end);
|
||||
});
|
||||
|
||||
await context.sync();
|
||||
|
||||
// Export as PDF
|
||||
return new Promise((resolve, reject) => {
|
||||
Office.context.document.getFileAsync(Office.FileType.Pdf, (result) => {
|
||||
if (result.status === Office.AsyncResultStatus.Succeeded) {
|
||||
const file = result.value;
|
||||
const sliceCount = file.sliceCount;
|
||||
const slices = [];
|
||||
let sliceIndex = 0;
|
||||
|
||||
const getSlice = () => {
|
||||
file.getSliceAsync(sliceIndex, (sliceResult) => {
|
||||
if (sliceResult.status === Office.AsyncResultStatus.Succeeded) {
|
||||
slices.push(sliceResult.value.data);
|
||||
sliceIndex++;
|
||||
if (sliceIndex < sliceCount) {
|
||||
getSlice();
|
||||
} else {
|
||||
file.closeAsync();
|
||||
const blob = new Blob(slices, { type: "application/pdf" });
|
||||
resolve(blob);
|
||||
}
|
||||
} else {
|
||||
reject(sliceResult.error);
|
||||
}
|
||||
});
|
||||
};
|
||||
getSlice();
|
||||
} else {
|
||||
reject(result.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Get Access Token for Graph
|
||||
async function getAccessToken() {
|
||||
return new Promise((resolve, reject) => {
|
||||
Office.context.auth.getAccessTokenAsync({ allowSignInPrompt: true }, (result) => {
|
||||
if (result.status === "succeeded") {
|
||||
resolve(result.value);
|
||||
} else {
|
||||
reject(new Error(result.error.message));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Create folder in SharePoint
|
||||
async function createSharePointFolder(token, driveId, parentId, folderName) {
|
||||
const endpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${parentId}/children`;
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ name: folderName, folder: {}, "@microsoft.graph.conflictBehavior": "rename" })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.id) throw new Error(`Failed to create folder: ${JSON.stringify(data)}`);
|
||||
return data.id;
|
||||
}
|
||||
|
||||
// Upload PDF
|
||||
async function uploadFileToSharePoint(token, driveId, folderId, fileName, pdfBlob) {
|
||||
const endpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${folderId}:/${fileName}:/content`;
|
||||
const response = await fetch(endpoint, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: pdfBlob
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Upload failed: ${errorText}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function run() {
|
||||
const projectInput = projectInputEl.value.trim();
|
||||
if (!projectInput) {
|
||||
statusEl.textContent = "Please enter a project number.";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Excel.run(async (context) => {
|
||||
/**
|
||||
* Insert your Excel code here
|
||||
*/
|
||||
const range = context.workbook.getSelectedRange();
|
||||
statusEl.textContent = "🔎 Looking up job data...";
|
||||
const jobData = await getJobData(projectInput);
|
||||
if (!jobData) {
|
||||
statusEl.textContent = "❌ Job not found.";
|
||||
return;
|
||||
}
|
||||
statusEl.textContent = `✅ Found job: ${jobData.Job_Name}`;
|
||||
|
||||
// Read the range address
|
||||
range.load("address");
|
||||
statusEl.textContent = "📋 Collecting form data...";
|
||||
const formData = await getFormData();
|
||||
|
||||
// Update the fill color
|
||||
range.format.fill.color = "yellow";
|
||||
statusEl.textContent = "🔑 Getting Graph token...";
|
||||
const graphToken = await getGraphToken();
|
||||
statusEl.textContent = "✅ Got Graph token";
|
||||
|
||||
await context.sync();
|
||||
console.log(`The range address was ${range.address}.`);
|
||||
});
|
||||
// ✅ Real IDs you provided earlier
|
||||
const driveId = "b!9YOqqr2xM0G2DFBwMEJYY4UzQgocFddEtpLYWuS9_AtkCKl2q8yjQJteQ7Ti4QSx";
|
||||
const parentItemId = "01SPNXLDW3Y56HAWK5CNAJ4YJLLSQCUCJ7";
|
||||
|
||||
statusEl.textContent = "📂 Creating main folder...";
|
||||
const mainFolderName = `[ ${jobData.Job_Number} ] - ${jobData.Job_Name}`;
|
||||
const mainFolderId = await createSharePointFolder(graphToken, driveId, parentItemId, mainFolderName);
|
||||
console.log("Main folder created:", mainFolderId);
|
||||
statusEl.textContent = "✅ Main folder created";
|
||||
|
||||
statusEl.textContent = "📂 Creating subfolder...";
|
||||
const subFolderId = await createSharePointFolder(graphToken, driveId, mainFolderId, "Manager Info");
|
||||
console.log("Subfolder created:", subFolderId);
|
||||
statusEl.textContent = "✅ Subfolder created";
|
||||
|
||||
statusEl.textContent = "🔗 Generating share link...";
|
||||
const shareLink = await createShareLink(graphToken, driveId, subFolderId);
|
||||
const shareEl = document.getElementById("shareLink");
|
||||
if (shareEl) {
|
||||
shareEl.innerHTML = `<a href="${shareLink}" target="_blank">${shareLink}</a>`;
|
||||
}
|
||||
statusEl.textContent = "✅ Share link created";
|
||||
|
||||
statusEl.textContent = "📝 Generating PDF...";
|
||||
const pdfBlob = await generatePDF(jobData, formData);
|
||||
statusEl.textContent = "✅ PDF generated";
|
||||
|
||||
statusEl.textContent = "⬆️ Uploading PDF to SharePoint...";
|
||||
await uploadFileToSharePoint(graphToken, driveId, subFolderId, `${mainFolderName}.pdf`, pdfBlob);
|
||||
statusEl.textContent = "✅ PDF uploaded successfully!";
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error("handleCreatePDF error:", error);
|
||||
statusEl.textContent = "❌ Error: " + formatError(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Excel helpers
|
||||
async function getJobData(jobNumber) {
|
||||
return Excel.run(async (context) => {
|
||||
const sheet = context.workbook.worksheets.getItem("Job Sheet");
|
||||
|
||||
const headerStart = sheet.getRange("A3");
|
||||
const headerRegion = headerStart.getSurroundingRegion();
|
||||
headerRegion.load("values, rowCount, columnCount");
|
||||
await context.sync();
|
||||
|
||||
const headers = headerRegion.values[0];
|
||||
if (!headers || headers.length === 0) {
|
||||
throw new Error("No headers found at A3.");
|
||||
}
|
||||
|
||||
// Get all rows below header
|
||||
const dataRange = headerRegion
|
||||
.getOffsetRange(1, 0)
|
||||
.getResizedRange(headerRegion.rowCount - 2, 0);
|
||||
dataRange.load("values");
|
||||
await context.sync();
|
||||
|
||||
const rows = dataRange.values;
|
||||
|
||||
const normalize = h => h ? h.toString().trim().toLowerCase() : "";
|
||||
const idxJobNumber = headers.findIndex(h => normalize(h) === "job_number");
|
||||
const idxJobName = headers.findIndex(h => normalize(h) === "job_name");
|
||||
const idxJobAddress = headers.findIndex(h => normalize(h) === "job_address");
|
||||
const idxJobDivision = headers.findIndex(h => normalize(h) === "job_division");
|
||||
const idxClientCustomer = headers.findIndex(h => normalize(h) === "client_customer");
|
||||
const idxContactPerson = headers.findIndex(h => normalize(h) === "contact_person");
|
||||
const idxEmail = headers.findIndex(h => normalize(h) === "email");
|
||||
|
||||
if (idxJobNumber === -1) throw new Error("No 'Job_Number' column found in header row A3.");
|
||||
|
||||
const match = rows.find(r => r[idxJobNumber]?.toString().trim() === jobNumber.toString().trim());
|
||||
if (!match) return null;
|
||||
|
||||
return {
|
||||
Job_Number: match[idxJobNumber],
|
||||
Job_Name: idxJobName >= 0 ? match[idxJobName] : "",
|
||||
Job_Address: idxJobAddress >= 0 ? match[idxJobAddress] : "",
|
||||
Job_Division: idxJobDivision >= 0 ? match[idxJobDivision] : "",
|
||||
Client_Customer: idxClientCustomer >= 0 ? match[idxClientCustomer] : "",
|
||||
Contact_Person: idxContactPerson >= 0 ? match[idxContactPerson] : "",
|
||||
Email: idxEmail >= 0 ? match[idxEmail] : ""
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function getFormData() {
|
||||
return Excel.run(async (context) => {
|
||||
const sheet = context.workbook.worksheets.getItem("FormDat");
|
||||
const range = sheet.getRange("A1").getSurroundingRegion();
|
||||
range.load("values");
|
||||
await context.sync();
|
||||
return range.values;
|
||||
});
|
||||
}
|
||||
|
||||
// Word helpers
|
||||
async function generatePDF(jobData, formData) {
|
||||
return Word.run(async (context) => {
|
||||
const body = context.document.body;
|
||||
body.clear();
|
||||
|
||||
body.insertParagraph("Project Information", Word.InsertLocation.start);
|
||||
body.insertParagraph(`Job Number: ${jobData.Job_Number}`, Word.InsertLocation.end);
|
||||
body.insertParagraph(`Job Name: ${jobData.Job_Name}`, Word.InsertLocation.end);
|
||||
body.insertParagraph(`Address: ${jobData.Job_Address}`, Word.InsertLocation.end);
|
||||
body.insertParagraph(`Division: ${jobData.Job_Division}`, Word.InsertLocation.end);
|
||||
body.insertParagraph(`Client: ${jobData.Client_Customer}`, Word.InsertLocation.end);
|
||||
body.insertParagraph(`Contact: ${jobData.Contact_Person}`, Word.InsertLocation.end);
|
||||
body.insertParagraph(`Email: ${jobData.Email}`, Word.InsertLocation.end);
|
||||
|
||||
body.insertParagraph("\nForm Data:", Word.InsertLocation.end);
|
||||
formData.forEach(row => {
|
||||
body.insertParagraph(row.join(" | "), Word.InsertLocation.end);
|
||||
});
|
||||
|
||||
await context.sync();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
Office.context.document.getFileAsync(Office.FileType.Pdf, (result) => {
|
||||
if (result.status === Office.AsyncResultStatus.Succeeded) {
|
||||
const file = result.value;
|
||||
const sliceCount = file.sliceCount;
|
||||
const slices = [];
|
||||
let sliceIndex = 0;
|
||||
|
||||
const getSlice = () => {
|
||||
file.getSliceAsync(sliceIndex, (sliceResult) => {
|
||||
if (sliceResult.status === Office.AsyncResultStatus.Succeeded) {
|
||||
slices.push(sliceResult.value.data);
|
||||
sliceIndex++;
|
||||
if (sliceIndex < sliceCount) {
|
||||
getSlice();
|
||||
} else {
|
||||
file.closeAsync();
|
||||
const blob = new Blob(slices, { type: "application/pdf" });
|
||||
resolve(blob);
|
||||
}
|
||||
} else {
|
||||
reject(sliceResult.error);
|
||||
}
|
||||
});
|
||||
};
|
||||
getSlice();
|
||||
} else {
|
||||
reject(result.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Graph helpers
|
||||
async function createSharePointFolder(token, driveId, parentId, folderName) {
|
||||
const endpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${parentId}/children`;
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: folderName,
|
||||
folder: {},
|
||||
"@microsoft.graph.conflictBehavior": "rename"
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Create folder response:", data);
|
||||
if (!data.id) throw new Error(`Failed to create folder: ${JSON.stringify(data, null, 2)}`);
|
||||
return data.id;
|
||||
}
|
||||
|
||||
async function uploadFileToSharePoint(token, driveId, folderId, fileName, pdfBlob) {
|
||||
const endpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${folderId}:/${fileName}:/content`;
|
||||
const response = await fetch(endpoint, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: pdfBlob
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Upload failed: ${errorText}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function createShareLink(token, driveId, itemId) {
|
||||
const endpoint = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/createLink`;
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: "view",
|
||||
scope: "organization"
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!data || !data.link || !data.link.webUrl) {
|
||||
throw new Error(`Failed to create share link: ${JSON.stringify(data, null, 2)}`);
|
||||
}
|
||||
return data.link.webUrl;
|
||||
}
|
||||
|
||||
// Demo run function
|
||||
async function run() {
|
||||
try {
|
||||
await Excel.run(async (context) => {
|
||||
const range = context.workbook.getSelectedRange();
|
||||
range.load("address");
|
||||
range.format.fill.color = "yellow";
|
||||
await context.sync();
|
||||
console.log(`The range address was ${range.address}.`);
|
||||
console.log("Headers:", headers);
|
||||
console.log("First row:", rows[0]);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Run error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure run is available globally if needed
|
||||
window.run = run;
|
||||
|
||||
@@ -19,6 +19,7 @@ module.exports = async (env, options) => {
|
||||
entry: {
|
||||
polyfill: ["core-js/stable", "regenerator-runtime/runtime"],
|
||||
taskpane: ["./src/taskpane/taskpane.js", "./src/taskpane/taskpane.html"],
|
||||
createfolder: ["./src/taskpane/createFolder/createFolder.js", "./src/taskpane/createFolder/createFolder.html"],
|
||||
commands: "./src/commands/commands.js",
|
||||
},
|
||||
output: {
|
||||
@@ -56,6 +57,11 @@ module.exports = async (env, options) => {
|
||||
template: "./src/taskpane/taskpane.html",
|
||||
chunks: ["polyfill", "taskpane"],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "createFolder.html",
|
||||
template: "./src/taskpane/createFolder/createFolder.html",
|
||||
chunks: ["polyfill", "createfolder"],
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
|
||||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Excel Add-in Commands</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!-- Office.js must be present so the commands surface initializes -->
|
||||
https://appsforoffice.microsoft.com/lib/1/hosted/office.js</script>
|
||||
<script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Minimal Playground Add-in (sideload instructions)
|
||||
|
||||
This small add-in is a single-button taskpane that calls the local `minimal-uploader-playground` API to generate a Managers Info PDF and upload it to SharePoint.
|
||||
|
||||
Quick steps to run and sideload for testing:
|
||||
|
||||
1. Start the playground API (if not already running):
|
||||
|
||||
```powershell
|
||||
# from repo root
|
||||
npm run start:all
|
||||
```
|
||||
|
||||
2. In Excel (Windows):
|
||||
- Open Excel → File → Options → Add-ins → Manage: COM Add-ins / Office Add-ins (depending) and disable any other dev add-ins that might auto-launch the task pane.
|
||||
- Follow Excel sideloading steps (Developer → Add-ins → Sideload/Upload) and pick `officeaddin/minimal-playground/manifest.xml`.
|
||||
- Open the add-in from the ribbon (should appear under the new Commands group as "Managers UI").
|
||||
|
||||
3. Click the single button in the taskpane — it will POST to `http://localhost:3002/api/upload-managers` and stream status.
|
||||
|
||||
How to avoid conflicts during testing
|
||||
- Use a dedicated Excel profile or temporarily disable other add-ins from the Office Add-ins / COM Add-ins UI.
|
||||
- If another dev add-in is using the same port (3003), you can run the orchestrator which will detect healthy endpoints and not re-start them — or kill the conflicting process first.
|
||||
|
||||
Notes
|
||||
- This is a minimal dev add-in and meant for local development only. Do not put secrets or production configuration into the local .env files when checked into source control.
|
||||
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<OfficeApp
|
||||
xmlns="http://schemas.microsoft.com/office/appforoffice/1.1"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0"
|
||||
xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides"
|
||||
xsi:type="TaskPaneApp">
|
||||
|
||||
<Id>9b5b1e9a-9fbd-4a3f-bcf4-7c1b2a7c5a97</Id>
|
||||
<Version>1.0.0.0</Version>
|
||||
<ProviderName>Local Dev</ProviderName>
|
||||
<DefaultLocale>en-US</DefaultLocale>
|
||||
<DisplayName DefaultValue="Minimal Playground Add-in"/>
|
||||
<Description DefaultValue="Very small add-in with one button that uploads Managers Info PDF to SharePoint"/>
|
||||
<IconUrl DefaultValue="https://localhost:3003/assets/icon-32.png"/>
|
||||
<HighResolutionIconUrl DefaultValue="https://localhost:3003/assets/icon-64.png"/>
|
||||
<SupportUrl DefaultValue="https://localhost:3003/help.html"/>
|
||||
|
||||
<AppDomains>
|
||||
<AppDomain>https://localhost:3003</AppDomain>
|
||||
</AppDomains>
|
||||
|
||||
<Hosts>
|
||||
<Host Name="Workbook"/>
|
||||
</Hosts>
|
||||
|
||||
<DefaultSettings>
|
||||
<SourceLocation DefaultValue="https://localhost:3003/taskpane.html"/>
|
||||
</DefaultSettings>
|
||||
|
||||
<Permissions>ReadWriteDocument</Permissions>
|
||||
|
||||
<VersionOverrides
|
||||
xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides"
|
||||
xsi:type="VersionOverridesV1_0">
|
||||
|
||||
<Hosts>
|
||||
<Host xsi:type="Workbook">
|
||||
<DesktopFormFactor>
|
||||
|
||||
<GetStarted>
|
||||
<Title resid="GetStarted.Title"/>
|
||||
<Description resid="GetStarted.Description"/>
|
||||
<LearnMoreUrl resid="GetStarted.LearnMoreUrl"/>
|
||||
</GetStarted>
|
||||
|
||||
<FunctionFile resid="Commands.Url"/>
|
||||
|
||||
<ExtensionPoint xsi:type="PrimaryCommandSurface">
|
||||
<OfficeTab id="TabHome">
|
||||
<Group id="CommandsGroup">
|
||||
<Label resid="CommandsGroup.Label"/>
|
||||
<Icon>
|
||||
<bt:Image size="16" resid="Icon.16x16"/>
|
||||
<bt:Image size="32" resid="Icon.32x32"/>
|
||||
<bt:Image size="80" resid="Icon.80x80"/>
|
||||
</Icon>
|
||||
<Control xsi:type="Button" id="TaskpaneButton">
|
||||
<Label resid="TaskpaneButton.Label"/>
|
||||
<Supertip>
|
||||
<Title resid="TaskpaneButton.Label"/>
|
||||
<Description resid="TaskpaneButton.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<bt:Image size="16" resid="Icon.16x16"/>
|
||||
<bt:Image size="32" resid="Icon.32x32"/>
|
||||
<bt:Image size="80" resid="Icon.80x80"/>
|
||||
</Icon>
|
||||
<Action xsi:type="ShowTaskpane">
|
||||
<TaskpaneId>ButtonId1</TaskpaneId>
|
||||
<SourceLocation resid="Taskpane.Url"/>
|
||||
</Action>
|
||||
</Control>
|
||||
</Group>
|
||||
</OfficeTab>
|
||||
</ExtensionPoint>
|
||||
|
||||
</DesktopFormFactor>
|
||||
</Host>
|
||||
</Hosts>
|
||||
|
||||
<Resources>
|
||||
<bt:Images>
|
||||
<bt:Image id="Icon.16x16" DefaultValue="https://localhost:3003/assets/icon-16.png"/>
|
||||
<bt:Image id="Icon.32x32" DefaultValue="https://localhost:3003/assets/icon-32.png"/>
|
||||
<bt:Image id="Icon.80x80" DefaultValue="https://localhost:3003/assets/icon-80.png"/>
|
||||
</bt:Images>
|
||||
|
||||
<bt:Urls>
|
||||
<bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://go.microsoft.com/fwlink/?LinkId=276812"/>
|
||||
<bt:Url id="Commands.Url" DefaultValue="https://localhost:3003/commands.html"/>
|
||||
<bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3003/taskpane.html"/>
|
||||
</bt:Urls>
|
||||
|
||||
<bt:ShortStrings>
|
||||
<bt:String id="GetStarted.Title" DefaultValue="Get started with the Minimal Playground"/>
|
||||
<bt:String id="CommandsGroup.Label" DefaultValue="Playground"/>
|
||||
<bt:String id="TaskpaneButton.Label" DefaultValue="Managers UI"/>
|
||||
</bt:ShortStrings>
|
||||
|
||||
<bt:LongStrings>
|
||||
<bt:String id="GetStarted.Description" DefaultValue="A tiny add-in used for testing the playground uploader"/>
|
||||
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Open the Managers Info uploader UI"/>
|
||||
</bt:LongStrings>
|
||||
</Resources>
|
||||
|
||||
</VersionOverrides>
|
||||
</OfficeApp>
|
||||
+927
@@ -0,0 +1,927 @@
|
||||
{
|
||||
"name": "minimal-playground-addin",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "minimal-playground-addin",
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.18.2"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "~1.2.0",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"on-finished": "~2.4.1",
|
||||
"qs": "~6.14.0",
|
||||
"raw-body": "~2.5.3",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.2.3",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
||||
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "~1.20.3",
|
||||
"content-disposition": "~0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "~0.7.1",
|
||||
"cookie-signature": "~1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "~1.3.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "~0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "~6.14.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "~0.19.0",
|
||||
"serve-static": "~1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "~2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "~2.0.2",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz",
|
||||
"integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send/node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static/node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serve-static/node_modules/send": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static/node_modules/send/node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static/node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "minimal-playground-addin",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"dotenv": "^17.2.3"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 81 B |
Binary file not shown.
|
After Width: | Height: | Size: 81 B |
Binary file not shown.
|
After Width: | Height: | Size: 81 B |
Binary file not shown.
|
After Width: | Height: | Size: 81 B |
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Commands</title>
|
||||
<script src="https://appsforoffice.microsoft.com/lib/1.1/hosted/office.js" type="text/javascript"></script>
|
||||
<script>
|
||||
Office.onReady(function() { console.log('Commands page Office.onReady'); });
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Commands placeholder</h2>
|
||||
<p>This file is referenced by the manifest and is not used for the minimal sample.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
body{ font-family: Segoe UI, Arial, Helvetica, sans-serif; background:#fff; color:#111; }
|
||||
.container{ padding:18px; width:380px }
|
||||
header h1{ margin:6px 0 4px; font-size:18px }
|
||||
.muted{ color:#666; font-size:12px; margin-bottom:12px }
|
||||
.primary{ display:inline-block; background:#0078d4; color:#fff; padding:10px 14px; border-radius:6px; border:0; cursor:pointer; font-size:14px }
|
||||
#status{ margin-top:12px; font-size:13px }
|
||||
#status .ok{ color:green }
|
||||
#status .err{ color:crimson }
|
||||
@@ -0,0 +1,30 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Minimal Playground Add-in</title>
|
||||
<link rel="stylesheet" href="/taskpane.css" />
|
||||
<!-- Office.js is REQUIRED for Office Add-ins -->
|
||||
<script src="https://appsforoffice.microsoft.com/lib/1.1/hosted/office.js" type="text/javascript"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Minimal Playground Add-in</h1>
|
||||
<p class="muted">One button → generates Managers Info PDF & uploads to SharePoint</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<button id="uploadBtn" class="primary">Upload Managers Info</button>
|
||||
<div id="status" aria-live="polite"></div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<small>Dev only — runs locally against the playground server</small>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="/taskpane.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
// Minimal add-in taskpane script — one button that calls the playground server
|
||||
// Wait for Office to be ready before attaching event handlers
|
||||
Office.onReady(function(info) {
|
||||
// info.host will be 'Excel' when running in Excel
|
||||
console.log('Office.onReady fired, host:', info.host);
|
||||
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
const status = document.getElementById('status');
|
||||
|
||||
function setStatus(text, ok) {
|
||||
status.innerHTML = '<div class="' + (ok ? 'ok' : 'err') + '">' + text + '</div>';
|
||||
}
|
||||
|
||||
uploadBtn.addEventListener('click', async function() {
|
||||
uploadBtn.disabled = true;
|
||||
setStatus('Starting upload...');
|
||||
try {
|
||||
const res = await fetch('http://localhost:3002/api/upload-managers', { method: 'POST' });
|
||||
const json = await res.json();
|
||||
if (!res.ok) throw new Error(json && json.error ? json.error : 'Unknown response');
|
||||
var link = (json.result && json.result.share && json.result.share.link) ? json.result.share.link.webUrl : 'unknown';
|
||||
setStatus('Upload finished. Share link: ' + link, true);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setStatus('Upload failed: ' + (err.message || err), false);
|
||||
} finally {
|
||||
uploadBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
// Tiny static server for the minimal playground add-in (HTTPS for Office add-in compatibility)
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const app = express();
|
||||
const port = process.env.ADDIN_PORT || 3003;
|
||||
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.get('/help.html', (req,res)=> res.send('<h2>Minimal Playground Add-in</h2><p>Sideload manifest to Excel, then open the Add-ins pane and click "Managers UI".</p>'));
|
||||
|
||||
// Use office-addin-dev-certs (installed via npx office-addin-dev-certs install)
|
||||
const userHome = process.env.USERPROFILE || process.env.HOME;
|
||||
const certDir = path.join(userHome, '.office-addin-dev-certs');
|
||||
const certPath = path.join(certDir, 'localhost.crt');
|
||||
const keyPath = path.join(certDir, 'localhost.key');
|
||||
|
||||
let server;
|
||||
if (fs.existsSync(certPath) && fs.existsSync(keyPath)) {
|
||||
const httpsOptions = {
|
||||
cert: fs.readFileSync(certPath),
|
||||
key: fs.readFileSync(keyPath)
|
||||
};
|
||||
server = https.createServer(httpsOptions, app);
|
||||
server.listen(port, '0.0.0.0', () => {
|
||||
console.log(`minimal-playground add-in server (HTTPS) listening on https://localhost:${port}`);
|
||||
});
|
||||
} else {
|
||||
console.log('No office-addin-dev-certs found. Run: npx office-addin-dev-certs install');
|
||||
server = app.listen(port, '0.0.0.0', () => {
|
||||
console.log(`minimal-playground add-in server (HTTP) listening on http://localhost:${port}`);
|
||||
});
|
||||
}
|
||||
|
||||
server.on('error', (err) => {
|
||||
console.error('Server error:', err);
|
||||
});
|
||||
|
||||
// Only handle explicit shutdown signals
|
||||
process.on('SIGINT', () => { console.log('Received SIGINT, shutting down...'); server.close(); process.exit(0); });
|
||||
process.on('SIGTERM', () => { console.log('Received SIGTERM, shutting down...'); server.close(); process.exit(0); });
|
||||
|
||||
// Prevent the process from exiting on its own
|
||||
process.stdin.resume();
|
||||
Generated
+60
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "job-info-dev-orchestrator",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "job-info-dev-orchestrator",
|
||||
"dependencies": {
|
||||
"job-info-dev-orchestrator": "file:",
|
||||
"node-fetch": "^2.6.7"
|
||||
}
|
||||
},
|
||||
"node_modules/job-info-dev-orchestrator": {
|
||||
"resolved": "",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "job-info-dev-orchestrator",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start:all": "node ./start-all.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"job-info-dev-orchestrator": "file:",
|
||||
"node-fetch": "^2.6.7"
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env node
|
||||
/* start-all.js — orchestrator for local dev
|
||||
- Ensures the playground server (3002) and the add-in static server (3003) are running
|
||||
- If healthy endpoints are reachable, does not start duplicates
|
||||
- Streams logs from started processes and ensures clean shutdown
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const fetch = require('node-fetch');
|
||||
const path = require('path');
|
||||
|
||||
const services = [
|
||||
{
|
||||
name: 'playground',
|
||||
dir: path.join(__dirname, 'minimal-uploader-playground'),
|
||||
startCmd: ['npm', ['run','server']],
|
||||
healthUrl: 'http://localhost:3002/api/health'
|
||||
},
|
||||
{
|
||||
name: 'addin',
|
||||
dir: path.join(__dirname, 'officeaddin', 'minimal-playground'),
|
||||
startCmd: ['npm', ['start']],
|
||||
// static taskpane page used as a health endpoint
|
||||
healthUrl: 'http://localhost:3003/taskpane.html'
|
||||
}
|
||||
];
|
||||
|
||||
async function checkHealth(url, timeout = 2000) {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const id = setTimeout(() => controller.abort(), timeout);
|
||||
const res = await fetch(url, { signal: controller.signal });
|
||||
clearTimeout(id);
|
||||
return res.ok;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function spawnService(svc) {
|
||||
const [cmd, args] = svc.startCmd;
|
||||
const proc = spawn(cmd, args, { cwd: svc.dir, shell: true, env: process.env });
|
||||
|
||||
proc.stdout.on('data', (d) => process.stdout.write(`[${svc.name}] ${d}`));
|
||||
proc.stderr.on('data', (d) => process.stderr.write(`[${svc.name}] ${d}`));
|
||||
proc.on('exit', (code, sig) => console.log(`[${svc.name}] exited code=${code} signal=${sig}`));
|
||||
return proc;
|
||||
}
|
||||
|
||||
async function startAll() {
|
||||
console.log('Orchestrator: checking service health...');
|
||||
const started = [];
|
||||
|
||||
for (const svc of services) {
|
||||
const healthy = await checkHealth(svc.healthUrl, 1500);
|
||||
if (healthy) {
|
||||
console.log(`${svc.name} is already healthy at ${svc.healthUrl}`);
|
||||
started.push({ svc, proc: null, already: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`${svc.name} not healthy — starting: ${svc.startCmd.join(' ')}`);
|
||||
const proc = spawnService(svc);
|
||||
started.push({ svc, proc, already: false });
|
||||
|
||||
// wait for the service to report healthy
|
||||
const maxWait = 30 * 1000; // 30s
|
||||
const start = Date.now();
|
||||
let ok = false;
|
||||
while (Date.now() - start < maxWait) {
|
||||
// wait a moment
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
if (await checkHealth(svc.healthUrl, 1500)) { ok = true; break; }
|
||||
}
|
||||
if (!ok) {
|
||||
console.error(`
|
||||
Service ${svc.name} did not become healthy at ${svc.healthUrl} within ${maxWait/1000}s. Check its logs above.`);
|
||||
// Continue retrying other services — do not abort so we can still start them
|
||||
} else {
|
||||
console.log(`${svc.name} is healthy at ${svc.healthUrl}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('All start requests processed. Press Ctrl-C to stop (or send SIGTERM).');
|
||||
|
||||
// graceful cleanup
|
||||
process.on('SIGINT', () => shutdown(started));
|
||||
process.on('SIGTERM', () => shutdown(started));
|
||||
}
|
||||
|
||||
function shutdown(started) {
|
||||
console.log('\nStopping orchestrator and children...');
|
||||
for (const { svc, proc, already } of started) {
|
||||
if (proc && !proc.killed) {
|
||||
try { proc.kill('SIGINT'); } catch (e) { /* ignore */ }
|
||||
console.log(`Requested stop for ${svc.name}`);
|
||||
} else if (already) {
|
||||
console.log(`Note: ${svc.name} was already running and will not be killed by the orchestrator.`);
|
||||
}
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
startAll().catch((err) => { console.error('Orchestrator failed:', err); process.exit(1); });
|
||||
Reference in New Issue
Block a user