Add minimal add-in playgrounds and orchestrator

This commit is contained in:
2025-12-07 21:26:10 -06:00
parent 40bcd939ff
commit ca95d296b5
54 changed files with 13165 additions and 26764 deletions
@@ -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
}
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="
}
+3 -94
View File
@@ -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=="],
+32 -26
View File
@@ -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>
</OfficeApp>
File diff suppressed because it is too large Load Diff
+7 -4
View File
@@ -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",
@@ -59,4 +62,4 @@
"last 2 versions",
"ie 11"
]
}
}
@@ -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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
};
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">
<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>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>
<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>
<p id="statusMessage"></p>
<div>
<strong>Share link:</strong>
<div id="shareLink"></div>
</div>
<div id="preview"></div>
</body>
</html>
</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: [
{