Add minimal add-in playgrounds and orchestrator
This commit is contained in:
@@ -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>
|
||||
</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",
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
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: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user