const DocManVer = "18.13"; function _UpdateDocCategory(DocCat, passedGUID) { console.log("UpdateDocCategory: called ver=" + DocManVer); const flowUrl = "/_api/cloudflow/v1.0/trigger/4c999c4e-3495-f011-b41b-002248c5d639"; const payload = { ActionType: 'UpdateDocCat', // group upload: adds multiple files to one group DocCategory: DocCat, AccGUID: '', DocManRowGuid: passedGUID }; _GenericFlowHandler(flowUrl, payload) .done(function (response) { console.log("UpdateCat: Passed-GUID-"+ passedGUID+ " -response:" + response); }) .fail(function (error) { console.log("UpdateCat: Error-GUID-"+ passedGUID+ " -response:" + error); }) .always(function () { }); } /** * SHARED FUNCTION: extract SAS token/url from flow response. * @param {any} flowResponse * @returns {string} */ function extractSasToken(flowResponse) { var response = flowResponse; if (typeof response === 'string') { try { response = JSON.parse(response); } catch (e) { if (response.indexOf('sig=') !== -1 || response.indexOf('?sv=') !== -1) { return response; } return ''; } } if (!response || typeof response !== 'object') { return ''; } var directCandidates = [ response.sasToken, response.sas, response.token, response.sas_token, response.SASToken, response.url, response.sasUrl ]; for (var i = 0; i < directCandidates.length; i++) { if (typeof directCandidates[i] === 'string' && directCandidates[i]) { return directCandidates[i]; } } var keys = Object.keys(response); for (var j = 0; j < keys.length; j++) { var value = response[keys[j]]; if (typeof value === 'string' && (value.indexOf('sig=') !== -1 || value.indexOf('?sv=') !== -1)) { return value; } } return ''; } /** * SHARED FUNCTION: parse CorrelationId and Error ID from Power Pages HTML error page. * @param {string} responseText * @returns {{correlationId:string, errorId:string, flowTrackingId:string, hasPowerAutomateErrorView:boolean}} * SHOULD THIS FUNCTION BE REMOVED. NOT SURE THAT ITS IS BEING USED */ function _ParsePowerPagesErrorMetadata(responseText) { var html = responseText || ''; var correlationId = ''; var errorId = ''; var flowTrackingId = ''; var parsedJson = null; if (html && (html.charAt(0) === '{' || html.charAt(0) === '[')) { try { parsedJson = JSON.parse(html); } catch (jsonParseError) { parsedJson = null; } } if (parsedJson && typeof parsedJson === 'object') { var messageText = ''; if (typeof parsedJson.Message === 'string') { messageText = parsedJson.Message; } else if (typeof parsedJson.message === 'string') { messageText = parsedJson.message; } var trackingMatchFromJson = /tracking\s+id\s+is\s+["'\u0027]*([0-9a-fA-F-]{36})/i.exec(messageText); if (trackingMatchFromJson && trackingMatchFromJson[1]) { flowTrackingId = trackingMatchFromJson[1]; } } var correlationMatch = /correlationId:\s*'([0-9a-fA-F-]{36})'/i.exec(html) || /Error\s*ID\s*#\s*\[([0-9a-fA-F-]{36})\]/i.exec(html) || /CorrelationId\s*:\s*([0-9a-fA-F-]{36})/i.exec(html); if (correlationMatch && correlationMatch[1]) { correlationId = correlationMatch[1]; } var errorIdMatch = /Error\s*ID\s*#\s*\[([^\]]+)\]/i.exec(html); if (errorIdMatch && errorIdMatch[1]) { errorId = errorIdMatch[1]; } if (!flowTrackingId) { var trackingMatchFromText = /tracking\s+id\s+is\s+["'\u0027]*([0-9a-fA-F-]{36})/i.exec(html); if (trackingMatchFromText && trackingMatchFromText[1]) { flowTrackingId = trackingMatchFromText[1]; } } if (!errorId && correlationId) { errorId = correlationId; } return { correlationId: correlationId, errorId: errorId, flowTrackingId: flowTrackingId, hasPowerAutomateErrorView: html.indexOf('~/Areas/PowerAutomate/Views/PowerAutomate/Error') !== -1 }; } function _normalizeGuidForODataBind(rawGuid) { var value = String(rawGuid || '').replace(/[{}]/g, '').trim(); if (!/^[0-9a-fA-F-]{36}$/.test(value)) { return ''; } return value.toLowerCase(); } function _toOptionalInt(rawValue) { if (rawValue === undefined || rawValue === null || rawValue === '') { return null; } var parsed = parseInt(rawValue, 10); if (isNaN(parsed)) { return null; } return parsed; } function _shouldFallbackToPortalWebApi(metaError) { var source = ''; if (metaError && metaError.message) { source += String(metaError.message); } if (metaError && metaError.responseText) { source += ' ' + String(metaError.responseText); } return source.indexOf('9004010A') >= 0 || source.indexOf("cr5ad_gprs_attachmentstorage") >= 0; } function _registerMetadataViaPortalWebApiFallback(metaPayload) { var entitySetUrl = '/_api/cr5ad_gprs_attachmentstorages'; var accountGuid = _normalizeGuidForODataBind(metaPayload && (metaPayload.AccountGUID || metaPayload.AccGUID)); var productGuid = _normalizeGuidForODataBind(metaPayload && (metaPayload.ProductGUID || metaPayload.ProdGUID)); var fileName = String(metaPayload && metaPayload.FileName ? metaPayload.FileName : '').trim(); var azureUri = String(metaPayload && metaPayload.AzureURI ? metaPayload.AzureURI : '').trim(); var docCategory = _toOptionalInt(metaPayload && metaPayload.DocCategory); if (!fileName || docCategory === null) { return Promise.reject({ message: 'Portal Web API fallback skipped: missing required metadata fields', fileNamePresent: !!fileName, docCategoryPresent: docCategory !== null, url: entitySetUrl, attempts: [] }); } var accountBindCandidates = accountGuid ? ['gtdv1_linktoaccount@odata.bind', 'cr5ad_linktoaccount@odata.bind', ''] : ['']; var productBindCandidates = productGuid ? ['cr5ad_linktoproduct@odata.bind', 'gtdv1_linktoproduct@odata.bind', ''] : ['']; var docCategoryCandidates = docCategory !== null ? ['gtdv1_documentcategory', 'cr5ad_documentcategory'] : ['']; var fileNameCandidates = fileName ? ['cr5ad_filename', 'gtdv1_filename', 'cr5ad_name', 'gtdv1_name'] : ['']; var blobPathCandidates = azureUri ? ['cr5ad_blobpath', 'gtdv1_blobpath'] : ['']; var rowsToTry = []; var attemptSummaries = []; for (var accountIndex = 0; accountIndex < accountBindCandidates.length; accountIndex++) { var accountBind = accountBindCandidates[accountIndex]; for (var productIndex = 0; productIndex < productBindCandidates.length; productIndex++) { var productBind = productBindCandidates[productIndex]; for (var docCategoryIndex = 0; docCategoryIndex < docCategoryCandidates.length; docCategoryIndex++) { var docCategoryField = docCategoryCandidates[docCategoryIndex]; for (var fileNameIndex = 0; fileNameIndex < fileNameCandidates.length; fileNameIndex++) { var fileNameField = fileNameCandidates[fileNameIndex]; for (var blobPathIndex = 0; blobPathIndex < blobPathCandidates.length; blobPathIndex++) { var blobPathField = blobPathCandidates[blobPathIndex]; var row = {}; if (accountBind) { row[accountBind] = '/accounts(' + accountGuid + ')'; } if (productBind) { row[productBind] = '/products(' + productGuid + ')'; } if (docCategoryField) { row[docCategoryField] = docCategory; } if (fileNameField) { row[fileNameField] = fileName; } if (blobPathField) { row[blobPathField] = azureUri; } if (!Object.keys(row).length) { continue; } rowsToTry.push({ row: row, signature: 'accountBind=' + (accountBind || 'none') + ',productBind=' + (productBind || 'none') + ',docCategoryField=' + (docCategoryField || 'none') + ',fileNameField=' + (fileNameField || 'none') + ',blobPathField=' + (blobPathField || 'none') }); } } } } } var maxAttempts = Math.min(rowsToTry.length, 35); console.warn('Meta registration fallback enabled. Trying direct Portal Web API create attempts:', maxAttempts); function _tryAttempt(index) { if (index >= maxAttempts) { return Promise.reject({ message: 'Portal Web API fallback failed for all attempts', url: entitySetUrl, attempts: attemptSummaries }); } var attempt = rowsToTry[index]; return window.webapi.safeAjax({ type: 'POST', url: entitySetUrl, xhrFields: { withCredentials: true }, contentType: 'application/json', dataType: 'json', data: JSON.stringify(attempt.row) }).then(function(result) { console.warn('Meta registration fallback succeeded via Portal Web API:', attempt.signature); return { source: 'portal-webapi-fallback', signature: attempt.signature, response: result }; }).catch(function(err) { var errText = ''; if (err && err.responseText) { errText = String(err.responseText); } else if (err && err.message) { errText = String(err.message); } attemptSummaries.push(attempt.signature + ',error=' + errText.substring(0, 280)); return _tryAttempt(index + 1); }); } if (!window.webapi || typeof window.webapi.safeAjax !== 'function') { return Promise.reject({ message: 'Portal Web API fallback unavailable: window.webapi.safeAjax not found', url: entitySetUrl, attempts: [] }); } return _tryAttempt(0); } /** * SHARED FUNCTION: preflight diagnostics for cloud flow calls. * Logs request context and common configuration checks before invoking the endpoint. * @param {string} flowUrl * @param {any} payload * @returns {object} */ function _PreflightCloudFlowCheck(flowUrl, payload) { var portal = window.Microsoft && window.Microsoft.Dynamic365 && window.Microsoft.Dynamic365.Portal ? window.Microsoft.Dynamic365.Portal : null; var flowMatch = /^\/_api\/cloudflow\/v1\.0\/trigger\/([0-9a-fA-F-]{36})$/.exec(flowUrl || ''); var triggerId = flowMatch ? flowMatch[1] : ''; var payloadKeys = payload && typeof payload === 'object' ? Object.keys(payload) : []; var preflight = { timestamp: new Date().toISOString(), flowUrl: flowUrl, triggerId: triggerId, isCloudFlowEndpointPattern: !!flowMatch, pageUrl: window.location.href, origin: window.location.origin, hasShellAjaxSafePost: typeof shell !== 'undefined' && shell && typeof shell.ajaxSafePost === 'function', hasJQuery: typeof $ !== 'undefined', payloadType: typeof payload, payloadKeys: payloadKeys, portalContextAvailable: !!portal, portalType: portal && portal.type ? portal.type : '', portalVersion: portal && portal.version ? portal.version : '', tenant: portal && portal.tenant ? portal.tenant : '', geo: portal && portal.geo ? portal.geo : '', contactId: portal && portal.User && portal.User.contactId ? portal.User.contactId : '' }; console.log('_PreflightCloudFlowCheck: summary', preflight); if (!preflight.isCloudFlowEndpointPattern) { console.error('_PreflightCloudFlowCheck: flowUrl format does not match /_api/cloudflow/v1.0/trigger/{guid}', { flowUrl: flowUrl }); } if (!preflight.triggerId) { console.error('_PreflightCloudFlowCheck: no triggerId detected in URL.'); } if (!preflight.portalContextAvailable) { console.error('_PreflightCloudFlowCheck: Microsoft.Dynamic365.Portal context not found on page.'); } if (!preflight.contactId) { console.warn('_PreflightCloudFlowCheck: no contactId found in portal context. Authenticated user may be required by flow permissions.'); } console.log('_PreflightCloudFlowCheck: checklist', { step1_enableCloudFlowIntegration: 'Verify Power Pages cloud flow integration is enabled in site settings', step2_flowBinding: 'Verify this trigger ID belongs to current site/environment and is bound to this page context', step3_permissions: 'Verify current user has permission to run the flow', step4_triggerSchema: 'Verify trigger expects eventData with text/number in current stage', step5_publishAndRetry: 'Republish flow and retry from same portal environment' }); return preflight; } /** * SHARED FUNCTION: generic flow transport used to communicate with Power Automate. * Shared functions must only use console logging and return status/results. * @param flowUrl * @param payload * @returns */ function _GenericFlowHandler(flowUrl, payload) { //https://learn.microsoft.com/en-us/power-pages/configure/cloud-flow-integration var startedAt = Date.now(); var handlerVersion = "DocManVer"; console.log("_GenericFlowHandler: ver", handlerVersion); console.log("_GenericFlowHandler: flowUrl", flowUrl); console.log("_GenericFlowHandler: payload (raw)", payload); console.log("_GenericFlowHandler: payload typeof", typeof payload); var payloadString = ""; try { payloadString = JSON.stringify(payload); console.log("_GenericFlowHandler: payload stringify success", payloadString); } catch (stringifyError) { console.error("_GenericFlowHandler: payload stringify failed", stringifyError); } //here is the section where we acutally configure what is being sent to the flow //Send the object directly and let jQuery handle serialization with proper Content-Type //This ensures Power Pages cloud flow receives valid JSON, not a JSON string var requestConfig = { type: "POST", url: flowUrl, data: { "eventData": JSON.stringify(payload) }, }; console.log("_GenericFlowHandler: requestConfig", { type: requestConfig.type, contentType: requestConfig.contentType, url: requestConfig.url, processData: requestConfig.processData, global: requestConfig.global, dataPreview: requestConfig.data }); //all of this code for this line!!!! var request = shell.ajaxSafePost(requestConfig); request.done(function(responseData, textStatus, jqXHR) { console.log("_GenericFlowHandler: DONE", { durationMs: Date.now() - startedAt, textStatus: textStatus, status: jqXHR && typeof jqXHR.status === "number" ? jqXHR.status : null, responseData: responseData }); }); request.fail(function(jqXHR, textStatus, errorThrown) { var responseText = jqXHR && jqXHR.responseText ? jqXHR.responseText : ""; var responseSnippet = responseText ? responseText.substring(0, 700) : ""; var errorMeta = _ParsePowerPagesErrorMetadata(responseText); var isPowerAutomateErrorView = errorMeta.hasPowerAutomateErrorView; console.error("_GenericFlowHandler: FAIL", { durationMs: Date.now() - startedAt, textStatus: textStatus, errorThrown: errorThrown ? String(errorThrown) : "", status: jqXHR && typeof jqXHR.status === "number" ? jqXHR.status : null, statusText: jqXHR && jqXHR.statusText ? jqXHR.statusText : "", responseText: responseText, isPowerAutomateErrorView: isPowerAutomateErrorView, correlationId: errorMeta.correlationId, errorId: errorMeta.errorId, flowTrackingId: errorMeta.flowTrackingId }); console.error("_GenericFlowHandler: FAIL response snippet", responseSnippet); if (isPowerAutomateErrorView) { console.error("_GenericFlowHandler: The cloud flow trigger endpoint exists but failed server-side in Power Pages. Check site settings for Power Automate integration, flow binding/permissions, and whether the trigger ID belongs to the current site/environment."); } // Run preflight becuase there was an error console.error("_GenericFlowHandler: Running preflight diagnostics due to flow failure"); _PreflightCloudFlowCheck(flowUrl, payload); }); console.log("_GenericFlowHandler: request sent"); return request; } function _awaitJQueryRequest(request, stageLabel, requestUrl) { return new Promise(function(resolve, reject) { if (!request || typeof request.done !== 'function' || typeof request.fail !== 'function') { reject({ message: (stageLabel || 'Request') + ' did not return a valid request object.', stage: stageLabel || '', url: requestUrl || '' }); return; } request.done(function(responseData) { resolve(responseData); }); request.fail(function(jqXHR, textStatus, errorThrown) { reject({ message: (stageLabel || 'Request') + ' failed.', stage: stageLabel || '', status: jqXHR && typeof jqXHR.status === 'number' ? jqXHR.status : null, statusText: jqXHR && jqXHR.statusText ? jqXHR.statusText : (textStatus || ''), textStatus: textStatus || '', errorThrown: errorThrown ? String(errorThrown) : '', responseText: jqXHR && jqXHR.responseText ? jqXHR.responseText : '', url: requestUrl || '' }); }); }); } /** * SHARED FUNCTION: flow invocation helper. * Shared functions must only use console logging and return status/results. * Keeps HTML pages simple by exposing sendFilesWithCategory(DocCat, files, passedGUID). * * @param {number|string} DocCat * @param {File|null} file * @param {string} passedGUID * @param {object=} uploadContext * @returns {Promise<{success:boolean, fileName:string, sasToken:string, outputFileName:string, duration:number}>} */ function _normalizeGuidText(value) { return String(value || '').replace(/[{}]/g, '').trim(); } function _normalizeBooleanValue(value, defaultValue) { if (value === undefined || value === null || value === '') { return defaultValue; } if (typeof value === 'boolean') { return value; } var text = String(value).trim().toLowerCase(); if (!text) { return defaultValue; } if (text === 'true' || text === '1' || text === 'yes') { return true; } if (text === 'false' || text === '0' || text === 'no') { return false; } return defaultValue; } async function _sendFilesWithCategory(DocCat, file, prodGUID, accGUID, uploadContext) { //init functions - geting the inputs sorted var startedAt = new Date(); var selectedFile = file || null; var SelectedFileSize = selectedFile ? selectedFile.size : 0; var passedFileName = selectedFile ? selectedFile.name : ''; var sasToken = ''; var sasflowUrl = "/_api/cloudflow/v1.0/trigger/7fb44881-790e-f111-8342-002248c8d803"; var safeUploadContext = uploadContext && typeof uploadContext === 'object' ? uploadContext : {}; var globalDocument = _normalizeBooleanValue(safeUploadContext.globalDocument, false); var normalizedAccountGuid = _normalizeGuidText(accGUID); var normalizedProductGuid = _normalizeGuidText(prodGUID); var effectiveAccountGuid = normalizedAccountGuid; var effectiveProductGuid = globalDocument ? '' : normalizedProductGuid; var uploadStages = { sasTokenReceived: false, docUploadCompleted: false, metadataAdded: false }; var currentStage = 'validation'; if (!effectiveAccountGuid || (!globalDocument && !effectiveProductGuid)) { console.error('sendFilesWithCategory: metadata save blocked because required GUIDs are missing.', { globalDocument: globalDocument, accountGuidPresent: Boolean(effectiveAccountGuid), productGuidPresent: Boolean(effectiveProductGuid), docCategory: DocCat, fileName: passedFileName }); throw new Error(globalDocument ? 'AccountGUID is required when saving global document metadata.' : 'AccountGUID and ProductGUID are required when saving document metadata.'); } console.log('sendFilesWithCategory (501) called', { docCategory: DocCat, globalDocument: globalDocument, accountGuid: effectiveAccountGuid, prodGuid: effectiveProductGuid, fileCount: selectedFile ? 1 : 0, fileName: passedFileName }); //there is a local helper file called PATrigger.html that helps generate these. //you will need the peek code data from the flow trigger const operation = "StubFileForIngest"; const ttlMinutes = 10; const NewFileName = file.name; const ReadFileURI = ""; const SASPayload = { Operation: operation, TimeLength: ttlMinutes, action: "new", ttl: ttlMinutes, NewFileName: NewFileName, FileName: NewFileName, ReadFileURI: ReadFileURI }; console.log('sendFilesWithCategoryAbout to send payload:', SASPayload); try { //////////////////////////////////////////////////////////////////////////////////////////////////// //Stage One: stage of the flow is to get a SAS token. we are calling a flow, that calls an Azure function //////////////////////////////////////////////////////////////////////////////////////////////////// currentStage = 'sasTokenRequest'; const GetSASResponse = await _awaitJQueryRequest( _GenericFlowHandler(sasflowUrl, SASPayload), 'SAS token request', sasflowUrl ); console.log('sendFilesWithCategory: SAS Flow response Raw:', GetSASResponse); sasToken = _extractSasUploadUrl(GetSASResponse) || GetSASResponse; uploadStages.sasTokenReceived = Boolean(sasToken); console.log('sendFilesWithCategory: SAS token details', _getSasTokenDebugDetails(GetSASResponse, sasToken)); //////////////////////////////////////////////////////////////////////////////////////////////////// //Stage two now that we have the SAS token we are able to upload a file into that 'token' location //////////////////////////////////////////////////////////////////////////////////////////////////// currentStage = 'docUpload'; const GetFileURIResponse = await _UploadFilesSAS(sasToken, selectedFile); uploadStages.docUploadCompleted = Boolean(GetFileURIResponse && GetFileURIResponse.success === true); console.log('sendFilesWithCategory: UploadFilesSAS response', GetFileURIResponse); const AzureURIFull = _extractBlobUrl(GetSASResponse) || (GetFileURIResponse && GetFileURIResponse.sasUrl ? GetFileURIResponse.sasUrl : ''); // full URL from Azure const AzureURI = _stripBlobHostFromUrl(AzureURIFull); // short path for storage //////////////////////////////////////////////////////////////////////////////////////////////////// //StageThree - register the file meta data ////////////////////////////////////////////////////////////////////////////////////////////////////// currentStage = 'metadataRegistration'; const metaRegistrationURL ="/_api/cloudflow/v1.0/trigger/973f617d-7c54-f111-bec7-002248c8d803"; const metaFileName = passedFileName; // required const metaAccGUID = effectiveAccountGuid; // optional const metaProdGUID = effectiveProductGuid; // optional const metaDocCat = DocCat; // required const metaAzureURI = AzureURI; // required const metaFileSize = SelectedFileSize; // required const MetaRegPayload = { FileName: metaFileName, AccGUID: metaAccGUID, ProdGUID: metaProdGUID, DocCat: metaDocCat, AzureURI: metaAzureURI, FileSize: metaFileSize }; console.log('sendFilesWithCategory: Meta registration payload', MetaRegPayload); const MetaUpdateResponse = await _awaitJQueryRequest( _GenericFlowHandler(metaRegistrationURL, MetaRegPayload), 'Metadata registration', metaRegistrationURL ); uploadStages.metadataAdded = Boolean(MetaUpdateResponse !== null && MetaUpdateResponse !== undefined); console.log('sendFilesWithCategory: Meta registration response', MetaUpdateResponse); return { success: true, fileName: passedFileName, sasToken: typeof sasToken === 'string' ? sasToken : '', outputFileName: GetFileURIResponse && GetFileURIResponse.outputFileName ? GetFileURIResponse.outputFileName : passedFileName, duration: new Date() - startedAt, azureUri: AzureURI, uploadStages: uploadStages }; } catch (uploadError) { var normalizedError = uploadError instanceof Error ? uploadError : new Error(String(uploadError && uploadError.message ? uploadError.message : uploadError || 'Unknown upload error')); normalizedError.uploadStage = currentStage; normalizedError.uploadStages = uploadStages; normalizedError.fileName = passedFileName; throw normalizedError; } } function _extractBlobUrl(flowResponse) { var parsed = flowResponse; if (typeof parsed === 'string') { var trimmed = parsed.trim(); if (!trimmed) { return ''; } if ((trimmed.charAt(0) === '{' && trimmed.charAt(trimmed.length - 1) === '}') || (trimmed.charAt(0) === '[' && trimmed.charAt(trimmed.length - 1) === ']')) { try { parsed = JSON.parse(trimmed); } catch (ignoredBlobUrlParseError) { return ''; } } else { return ''; } } if (!parsed || typeof parsed !== 'object') { return ''; } var blobUrl = parsed.blobURL || parsed.BlobURL || parsed.blobUrl || ''; if (typeof blobUrl === 'string' && blobUrl.trim()) { return blobUrl.trim(); } if (parsed.body && typeof parsed.body === 'object') { var nestedBodyBlobUrl = _extractBlobUrl(parsed.body); if (nestedBodyBlobUrl) { return nestedBodyBlobUrl; } } if (parsed.result && typeof parsed.result === 'object') { var nestedResultBlobUrl = _extractBlobUrl(parsed.result); if (nestedResultBlobUrl) { return nestedResultBlobUrl; } } for (var key in parsed) { if (!Object.prototype.hasOwnProperty.call(parsed, key)) { continue; } var value = parsed[key]; if (value && typeof value === 'object') { var nestedBlobUrl = _extractBlobUrl(value); if (nestedBlobUrl) { return nestedBlobUrl; } } } return ''; } function _stripBlobHostFromUrl(fullBlobUrl) { if (!fullBlobUrl || typeof fullBlobUrl !== 'string') { return ''; } var trimmed = fullBlobUrl.trim(); if (!trimmed) { return ''; } // If it's already a path (starts with /), return as is if (trimmed.charAt(0) === '/') { return trimmed; } // If it's a full URL, extract just the path portion if (/^https?:\/\//i.test(trimmed)) { try { var parsedUrl = new URL(trimmed); return parsedUrl.pathname || ''; } catch (urlParseError) { console.error('_stripBlobHostFromUrl: failed to parse URL', urlParseError); return ''; } } // Not a URL and doesn't start with /, return empty return ''; } function _extractSasUploadUrl(flowResponse) { var parsed = flowResponse; if (typeof parsed === 'string') { var trimmed = parsed.trim(); if (!trimmed) { return ''; } if (trimmed.charAt(0) === '"' && trimmed.charAt(trimmed.length - 1) === '"') { try { trimmed = JSON.parse(trimmed); } catch (ignoredQuotedStringParseError) { // keep original trimmed string } } if (typeof trimmed === 'string' && /^https?:\/\//i.test(trimmed)) { return trimmed; } if ((trimmed.charAt(0) === '{' && trimmed.charAt(trimmed.length - 1) === '}') || (trimmed.charAt(0) === '[' && trimmed.charAt(trimmed.length - 1) === ']')) { try { parsed = JSON.parse(trimmed); } catch (ignoredJsonParseError) { return ''; } } else { return ''; } } if (!parsed || typeof parsed !== 'object') { return ''; } var preferredKeys = [ 'UploadURL', 'uploadURL', 'UploadUrl', 'uploadSasUrl', 'uploadUri', 'uploadUrl', 'writeSasUrl', 'writeUri', 'sasUploadUrl', 'blobWriteUri', 'blobUploadUrl', 'sasUrl', 'sasURI', 'sasUri', 'token' ]; for (var i = 0; i < preferredKeys.length; i++) { var key = preferredKeys[i]; if (typeof parsed[key] === 'string' && /^https?:\/\//i.test(parsed[key])) { return parsed[key].trim(); } } var fallbackKeys = ['url', 'uri', 'href', 'value']; for (var j = 0; j < fallbackKeys.length; j++) { var fallbackKey = fallbackKeys[j]; if (typeof parsed[fallbackKey] === 'string' && /^https?:\/\//i.test(parsed[fallbackKey])) { return parsed[fallbackKey].trim(); } } if (parsed.body && typeof parsed.body === 'object') { var nestedBodyUrl = _extractSasUploadUrl(parsed.body); if (nestedBodyUrl) { return nestedBodyUrl; } } if (parsed.result && typeof parsed.result === 'object') { var nestedResultUrl = _extractSasUploadUrl(parsed.result); if (nestedResultUrl) { return nestedResultUrl; } } for (var prop in parsed) { if (!Object.prototype.hasOwnProperty.call(parsed, prop)) { continue; } var value = parsed[prop]; if (value && typeof value === 'object') { var nestedUrl = _extractSasUploadUrl(value); if (nestedUrl) { return nestedUrl; } } } return ''; } function _analyzeSasUrl(sasUrl) { var analysis = { isValid: false, host: '', container: '', blobPath: '', permissions: '', resourceType: '', startsOn: '', expiresOn: '', hasSignature: false, errors: [] }; if (!sasUrl || typeof sasUrl !== 'string') { analysis.errors.push('SAS URL is empty or not a string.'); return analysis; } try { var parsedUrl = new URL(sasUrl); analysis.host = parsedUrl.host || ''; var pathSegments = (parsedUrl.pathname || '').split('/').filter(function(segment) { return !!segment; }); if (pathSegments.length === 0) { analysis.errors.push('SAS URL path is missing container and blob path.'); } else if (pathSegments.length === 1) { analysis.container = decodeURIComponent(pathSegments[0]); analysis.errors.push('SAS URL path is missing blob name segment.'); } else { analysis.container = decodeURIComponent(pathSegments[0]); analysis.blobPath = decodeURIComponent(pathSegments.slice(1).join('/')); } analysis.permissions = parsedUrl.searchParams.get('sp') || ''; analysis.resourceType = parsedUrl.searchParams.get('sr') || ''; analysis.startsOn = parsedUrl.searchParams.get('st') || ''; analysis.expiresOn = parsedUrl.searchParams.get('se') || ''; analysis.hasSignature = !!parsedUrl.searchParams.get('sig'); if (!analysis.permissions) { analysis.errors.push('SAS URL query parameter "sp" (permissions) is missing.'); } if (!analysis.resourceType) { analysis.errors.push('SAS URL query parameter "sr" (resource type) is missing.'); } if (!analysis.hasSignature) { analysis.errors.push('SAS URL query parameter "sig" (signature) is missing.'); } } catch (parseError) { analysis.errors.push('SAS URL is not a valid absolute URL.'); } analysis.isValid = analysis.errors.length === 0; return analysis; } function _getSasTokenDebugDetails(flowResponse, extractedSasUrl) { var details = { flowResponseType: typeof flowResponse, hasFlowResponse: flowResponse !== null && flowResponse !== undefined, uploadURL: '', blobURL: '', expiresOn: '', permissions: '', extractedSasUrl: extractedSasUrl || '', effectiveSasUrl: '', sasAnalysis: null, sasQueryParams: {}, sasPathname: '', sasOrigin: '' }; var parsed = flowResponse; if (typeof parsed === 'string') { var trimmed = parsed.trim(); if (trimmed && ((trimmed.charAt(0) === '{' && trimmed.charAt(trimmed.length - 1) === '}') || (trimmed.charAt(0) === '[' && trimmed.charAt(trimmed.length - 1) === ']'))) { try { parsed = JSON.parse(trimmed); } catch (ignoredFlowResponseParseError) { // keep original parsed value } } } if (parsed && typeof parsed === 'object') { details.uploadURL = parsed.UploadURL || parsed.uploadURL || parsed.UploadUrl || ''; details.blobURL = parsed.blobURL || parsed.BlobURL || parsed.blobUrl || ''; details.expiresOn = parsed.ExpiresOn || parsed.expiresOn || ''; details.permissions = parsed.Permissions || parsed.permissions || ''; } if (!details.extractedSasUrl) { details.extractedSasUrl = _extractSasUploadUrl(flowResponse) || ''; } details.effectiveSasUrl = details.extractedSasUrl || details.uploadURL || ''; details.sasAnalysis = _analyzeSasUrl(details.effectiveSasUrl); if (details.effectiveSasUrl) { try { var parsedUrl = new URL(details.effectiveSasUrl); details.sasPathname = decodeURIComponent(parsedUrl.pathname || ''); details.sasOrigin = parsedUrl.origin || ''; parsedUrl.searchParams.forEach(function(value, key) { details.sasQueryParams[key] = value; }); } catch (ignoredSasUrlParseError) { // handled by _analyzeSasUrl } } return details; } function _UploadFilesSAS(sasToken, file) { return new Promise(function(resolve, reject) { var progressDiv = document.getElementById('UploadProgress'); var lastLoggedPercent = -1; console.log('_UploadFilesSAS: start', { fileName: file && file.name ? file.name : null, fileSize: file && typeof file.size === 'number' ? file.size : null, fileType: file && file.type ? file.type : null, hasSasToken: !!sasToken }); if (!file) { console.log('_UploadFilesSAS: validation failed - no file provided'); reject({ message: 'No file provided for SAS upload.' }); return; } if (!sasToken) { console.log('_UploadFilesSAS: validation failed - missing SAS token'); reject({ message: 'SAS token was not returned from the flow.' }); return; } var sasUrl = _extractSasUploadUrl(sasToken); var sasAnalysis = _analyzeSasUrl(sasUrl); console.log('_UploadFilesSAS: SAS token full details', _getSasTokenDebugDetails(sasToken, sasUrl)); console.log('_UploadFilesSAS: SAS URL extracted', { hasSasUrl: !!sasUrl, host: sasAnalysis.host || null, container: sasAnalysis.container || null, blobPath: sasAnalysis.blobPath || null, permissions: sasAnalysis.permissions || null, resourceType: sasAnalysis.resourceType || null, expiresOn: sasAnalysis.expiresOn || null, isStructurallyValid: sasAnalysis.isValid, urlValidationErrors: sasAnalysis.errors }); if (!sasUrl) { console.log('_UploadFilesSAS: failed to determine SAS URL from token', { tokenPreview: typeof sasToken === 'string' ? sasToken.substring(0, 200) : sasToken }); reject({ message: 'Could not determine SAS upload URL from flow response.', sasToken: sasToken }); return; } if (!sasAnalysis.isValid) { var malformedMessage = 'Returned SAS upload URL is malformed or missing required SAS components.'; console.log('_UploadFilesSAS: validation failed - malformed SAS URL', { fileName: file.name, sasUrl: sasUrl, errors: sasAnalysis.errors }); reject({ message: malformedMessage, status: null, statusText: '', responseText: '', url: sasUrl, sasAnalysis: sasAnalysis }); return; } if (progressDiv) { progressDiv.textContent = 'Uploading ' + file.name + ' (0%)...'; } var xhr = new XMLHttpRequest(); xhr.open('PUT', sasUrl, true); xhr.setRequestHeader('x-ms-blob-type', 'BlockBlob'); xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream'); console.log('_UploadFilesSAS: PUT initialized', { method: 'PUT', url: sasUrl, storageHost: sasAnalysis.host, container: sasAnalysis.container, blobPath: sasAnalysis.blobPath, fileName: file.name, fileSize: file.size, contentType: file.type || 'application/octet-stream' }); xhr.upload.onprogress = function(event) { if (!progressDiv) { return; } if (event.lengthComputable && event.total > 0) { var percent = Math.round((event.loaded / event.total) * 100); if (percent >= 100 || percent - lastLoggedPercent >= 25) { lastLoggedPercent = percent; console.log('_UploadFilesSAS: upload progress', { fileName: file.name, loaded: event.loaded, total: event.total, percent: percent }); } progressDiv.textContent = 'Uploading ' + file.name + ' (' + percent + '%)...'; } else { console.log('_UploadFilesSAS: upload progress (bytes only)', { fileName: file.name, loaded: event.loaded }); progressDiv.textContent = 'Uploading ' + file.name + ' (' + event.loaded + ' bytes sent)...'; } }; xhr.onload = function() { console.log('_UploadFilesSAS: response received', { fileName: file.name, status: xhr.status, statusText: xhr.statusText }); if (xhr.status < 200 || xhr.status >= 300) { if (progressDiv) { progressDiv.textContent = 'Upload failed for ' + file.name + ' (' + xhr.status + ' ' + (xhr.statusText || 'Error') + ').'; } var responseBody = xhr.responseText || ''; var uploadMessage = 'SAS upload request failed.'; if (responseBody.indexOf('ContainerNotFound') !== -1) { uploadMessage = 'SAS upload request failed: container "' + (sasAnalysis.container || 'unknown') + '" does not exist in storage account "' + (sasAnalysis.host || 'unknown') + '".'; } console.log('_UploadFilesSAS: upload failed', { fileName: file.name, message: uploadMessage, status: xhr.status, statusText: xhr.statusText, storageHost: sasAnalysis.host, container: sasAnalysis.container, blobPath: sasAnalysis.blobPath, responseSnippet: responseBody ? responseBody.substring(0, 500) : '' }); reject({ message: uploadMessage, status: xhr.status, statusText: xhr.statusText, responseText: responseBody, url: sasUrl, sasAnalysis: sasAnalysis, troubleshooting: responseBody.indexOf('ContainerNotFound') !== -1 ? 'Verify the Azure Blob container exists in this storage account and that your flow/Azure Function is configured to generate SAS URLs for that exact container name.' : '' }); return; } if (progressDiv) { progressDiv.textContent = 'Uploaded ' + file.name + ' (100%).'; } console.log('_UploadFilesSAS: upload succeeded', { fileName: file.name, status: xhr.status, url: sasUrl }); resolve({ success: true, sasUrl: sasUrl, outputFileName: file.name, status: xhr.status }); }; xhr.onerror = function(error) { if (progressDiv) { progressDiv.textContent = 'Network error while uploading ' + file.name + '.'; } console.log('_UploadFilesSAS: network error', { fileName: file.name, error: error, url: sasUrl }); reject({ message: 'Network error while uploading via SAS.', error: error, url: sasUrl }); }; console.log('_UploadFilesSAS: sending file bytes', { fileName: file.name, fileSize: file.size }); xhr.send(file); }); } function _UploadFileSAS(sasToken, File, FileName) { console.log('_UploadFileSAS: called', { hasFileObject: !!File, fileNameArg: FileName || null, hasSasToken: !!sasToken }); var fileToUpload = File; if (!fileToUpload && FileName) { console.log('_UploadFileSAS: no File object, creating empty File from FileName', { fileName: FileName }); fileToUpload = new File([], FileName); } console.log('_UploadFileSAS: forwarding to _UploadFilesSAS', { resolvedFileName: fileToUpload && fileToUpload.name ? fileToUpload.name : null, resolvedFileSize: fileToUpload && typeof fileToUpload.size === 'number' ? fileToUpload.size : null }); return _UploadFilesSAS(sasToken, fileToUpload); } /** * SHARED FUNCTION: resolve a document category name by category id. * Uses the Power Pages Web API category-list table, and only returns categories marked as public (cr5ad_public = 1). * Throws when the id is invalid, no matching public category exists, or the API response is invalid. * @param {number|string} docCatId * @returns {Promise} Resolved category display name. */ async function _DocMan_getDocCatDetails(docCatId) { console.log("_DocMan_getDocCatDetailsL: called for " + docCatId); let categoryName = ""; const parsedDocCatId = Number.parseInt(docCatId, 10); if (!Number.isFinite(parsedDocCatId)) { throw new Error(`Invalid docCatId: ${docCatId}`); } const origin = (window.location && window.location.origin) ? window.location.origin.replace(/\/$/, "") : ""; const apiUrls = [ `${origin}/_api/cr5ad_docman_categorylists`, `${origin}/_api/cr5ad_docman_categorylist` ]; const handler = window._GenericWEBAPIHandler; if (typeof handler !== "function") { return Promise.reject(new Error("_GenericWEBAPIHandler is not available on window.")); } try { // Probe candidate entity-set URLs to handle singular/plural naming differences. let workingApiUrl = ""; let probeResponse = null; let lastProbeError = null; for (const candidateUrl of apiUrls) { try { const candidateProbe = await handler({ url: candidateUrl, select: "cr5ad_name", top: 1 }); if (candidateProbe && Array.isArray(candidateProbe.value)) { workingApiUrl = candidateUrl; probeResponse = candidateProbe; break; } } catch (probeErr) { lastProbeError = probeErr; } } if (!workingApiUrl || !probeResponse || !Array.isArray(probeResponse.value)) { const probeErrorText = (lastProbeError && lastProbeError.responseText) ? String(lastProbeError.responseText) : String(lastProbeError || "unknown"); throw new Error(`Probe query failed for all category-list endpoints. Last error: ${probeErrorText}`); } console.log("[DocCatLookup] Probe success URL:", workingApiUrl); console.log("[DocCatLookup] Probe success row count:", probeResponse.value.length); const query = { url: workingApiUrl, select: "cr5ad_name,cr5ad_doccat,cr5ad_public", filter: `cr5ad_doccat eq ${parsedDocCatId} and cr5ad_public eq true` }; const response = await handler(query); console.log("[DocCatLookup] Raw handler response:", response); if (!response || !Array.isArray(response.value)) { throw new Error("Web API returned an invalid response for document category details."); } const matchedRow = response.value[0]; if (!matchedRow) { throw new Error(`No document category found for docCatId ${parsedDocCatId}.`); } categoryName = String( matchedRow?.cr5ad_name ?? matchedRow?.["cr5ad_name@OData.Community.Display.V1.FormattedValue"] ?? "" ).trim(); if (!categoryName) { throw new Error(`Document category name is empty for docCatId ${parsedDocCatId}.`); } return categoryName; } catch (err) { console.error("Error fetching document category details:", { docCatId: parsedDocCatId, categoryName, error: err }); throw err; } } async function _DocMan_getCategories(bollPublicOnly) { console.log("_DocMan_getCategories: called", { bollPublicOnly: bollPublicOnly }); const origin = (window.location && window.location.origin) ? window.location.origin.replace(/\/$/, "") : ""; const apiUrls = [ `${origin}/_api/cr5ad_docman_categorylists`, `${origin}/_api/cr5ad_docman_categorylist` ]; const handler = window._GenericWEBAPIHandler; if (typeof handler !== "function") { throw new Error("_GenericWEBAPIHandler is not available on window."); } // true => public categories only, false/null/undefined => all categories let filter = ""; if (bollPublicOnly === true) { filter = "cr5ad_public eq true"; } let lastError = null; for (const url of apiUrls) { try { const response = await handler({ url: url, select: "cr5ad_docman_categorylistid,cr5ad_doccat,cr5ad_name,cr5ad_public", filter: filter, orderby: "cr5ad_name asc", top: null }); if (!response || !Array.isArray(response.value)) { throw new Error("Web API returned an invalid response for category list query."); } const categories = response.value .map(function(row) { const id = String(row?.cr5ad_docman_categorylistid || "").trim(); const docCat = Number.parseInt(row?.cr5ad_doccat, 10); const name = String( row?.cr5ad_name ?? row?.["cr5ad_name@OData.Community.Display.V1.FormattedValue"] ?? "" ).trim(); const rawPublic = row?.cr5ad_public; const isPublic = ( rawPublic === true || rawPublic === 1 || String(rawPublic).trim().toLowerCase() === "true" || String(rawPublic).trim() === "1" ); return { id: id, docCat: Number.isFinite(docCat) ? docCat : null, name: name, isPublic: isPublic, // Convenience aliases for dropdown/component binding. value: Number.isFinite(docCat) ? String(docCat) : "", text: name }; }) .filter(function(item) { return item.docCat !== null && !!item.name; }); console.log("_DocMan_getCategories: resolved", { url: url, count: categories.length, filter: filter || "(none)" }); return categories; } catch (err) { lastError = err; } } console.error("_DocMan_getCategories: failed", { bollPublicOnly: bollPublicOnly, error: lastError }); throw lastError || new Error("Unable to fetch document categories."); } async function _GenericWEBAPIHandler({ url, select, filter, orderby, top, count, prefer, cacheBust }) { if (!url || typeof url !== 'string' || !url.startsWith('https://')) { console.error('_GenericWEBAPIHandler: Invalid or missing URL'); return null; } if (!select || typeof select !== 'string') { console.error('Select criteria is required'); return null; } let params = []; params.push(`$select=${encodeURIComponent(select)}`); let processedFilter = filter; if (filter && (filter.includes(' and ') || filter.includes(' or '))) { let splitAnd = filter.split(' and '); if (splitAnd.length > 1) { processedFilter = splitAnd.map(f => f.trim().startsWith('(') ? f.trim() : `(${f.trim()})`).join(' and '); } else { let splitOr = filter.split(' or '); if (splitOr.length > 1) { processedFilter = splitOr.map(f => f.trim().startsWith('(') ? f.trim() : `(${f.trim()})`).join(' or '); } } } if (processedFilter) params.push(`$filter=${processedFilter}`); if (processedFilter) { params[params.length - 1] = `$filter=${encodeURIComponent(processedFilter)}`; } if (orderby) params.push(`$orderby=${encodeURIComponent(orderby)}`); if (top) params.push(`$top=${encodeURIComponent(top)}`); if (count) params.push(`$count=${encodeURIComponent(count)}`); const shouldCacheBust = cacheBust === true; if (shouldCacheBust) { params.push(`_cb=${Date.now()}`); } let queryString = ''; if (params.length) { queryString = (url.includes('?') ? '&' : '?') + params.join('&'); } const fullUrl = url + queryString; let enforcedPrefer = prefer; const choiceColumns = ['gtdv1_documentcategory']; const selectLower = (select || '').toLowerCase(); const filterLower = (filter || '').toLowerCase(); let choiceColumnUsed = false; for (const col of choiceColumns) { if (selectLower.includes(col.toLowerCase()) || filterLower.includes(col.toLowerCase())) { choiceColumnUsed = true; break; } } if (choiceColumnUsed && (!prefer || !prefer.includes('OData.Community.Display.V1.FormattedValue'))) { enforcedPrefer = `odata.include-annotations="OData.Community.Display.V1.FormattedValue"`; } const effectivePrefer = enforcedPrefer && enforcedPrefer.trim() ? enforcedPrefer.trim() : 'odata.include-annotations="OData.Community.Display.V1.FormattedValue"'; const requestHeaders = { 'Accept': 'application/json', 'Prefer': effectivePrefer }; if (shouldCacheBust) { requestHeaders['Cache-Control'] = 'no-cache, no-store, must-revalidate'; requestHeaders['Pragma'] = 'no-cache'; requestHeaders['Expires'] = '0'; } try { const result = await window.webapi.safeAjax({ type: 'GET', url: fullUrl, headers: requestHeaders, xhrFields: { withCredentials: true }, contentType: 'application/json', dataType: 'json' }); return result; } catch (e) { console.error('[WebAPIWrapper] safeAjax error', e); return null; } } async function _GerAllFilesInTechFile(productGuid) { const fallbackProductGuid = _DocMan_getProductGroupGuidFromQueryString(); const normalizedProductGuid = String(productGuid || fallbackProductGuid || "") .trim() .replace(/^\{/, "") .replace(/\}$/, ""); if (!normalizedProductGuid) { console.error("_GerAllFilesInTechFile: productGuid is required (argument or query string)."); return []; } const origin = (window.location && window.location.origin) ? window.location.origin.replace(/\/$/, "") : ""; const apiUrl = `${origin}/_api/cr5ad_gprs_attachmentstorages`; const filter = `_cr5ad_linktoproduct_value eq '${normalizedProductGuid}'`; const response = await _GenericWEBAPIHandler({ url: apiUrl, select: "cr5ad_gprs_attachmentstorageid,_cr5ad_linktoproduct_value,cr5ad_filename,createdon,gtdv1_documentcategory", filter: filter, orderby: "createdon desc", prefer: `odata.include-annotations=\"OData.Community.Display.V1.FormattedValue\"`, cacheBust: true }); if (!response || !Array.isArray(response.value)) { console.error("_GerAllFilesInTechFile: invalid response.", response); return []; } console.log("_GerAllFilesInTechFile: query succeeded", { filterUsed: filter, count: response.value.length }); return response.value.map(function(row) { return { cr5ad_gprs_attachmentstorageid: row.cr5ad_gprs_attachmentstorageid || "", cr5ad_linktoproduct: String( row["_cr5ad_linktoproduct_value@OData.Community.Display.V1.FormattedValue"] || "" ).trim(), cr5ad_filename: row.cr5ad_filename || "", createdon: row.createdon || "", gtdv1_documentcategory: String( row["gtdv1_documentcategory@OData.Community.Display.V1.FormattedValue"] || "" ).trim() }; }); } function _DocMan_getProductGroupGuidFromQueryString(paramNames) { const search = (window.location && typeof window.location.search === "string") ? window.location.search : ""; const params = new URLSearchParams(search); const candidateNames = Array.isArray(paramNames) && paramNames.length ? paramNames : ["productgroup", "productGroup", "ProductGroup", "productguid", "ProductGUID", "id"]; for (var i = 0; i < candidateNames.length; i++) { var paramName = String(candidateNames[i] || "").trim(); if (!paramName) { continue; } var value = params.get(paramName); if (value && String(value).trim()) { return String(value).trim().replace(/^\{/, "").replace(/\}$/, ""); } } return ""; } function _DocMan_escapeHtml(value) { return String(value ?? "").replace(/[&<>"]/g, function(ch) { return { "&": "&", "<": "<", ">": ">", "\"": """ }[ch]; }); } function _DocMan_formatFilesAsTable(rows) { const safeRows = Array.isArray(rows) ? rows : []; const formatter = new Intl.DateTimeFormat(undefined, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); if (!safeRows.length) { return "
No files found for this product group.
"; } var tbody = safeRows.map(function(row) { var created = ""; if (row && row.createdon) { var parsedDate = new Date(row.createdon); created = Number.isNaN(parsedDate.getTime()) ? String(row.createdon) : formatter.format(parsedDate); } var rowGuid = row && row.cr5ad_gprs_attachmentstorageid ? String(row.cr5ad_gprs_attachmentstorageid) : ""; return "" + "" + "" + _DocMan_escapeHtml(row && row.cr5ad_filename ? row.cr5ad_filename : "") + "" + "" + _DocMan_escapeHtml(row && row.cr5ad_linktoproduct ? row.cr5ad_linktoproduct : "") + "" + "" + _DocMan_escapeHtml(created) + "" + "" + _DocMan_escapeHtml(row && row.gtdv1_documentcategory ? row.gtdv1_documentcategory : "") + "" + ""; }).join(""); // GUID is intentionally not displayed as a visible column. return "" + "
" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + tbody + "" + "
File nameProduct groupCreatedDocument category
" + "
"; } async function _DocMan_renderFilesForProductGroupFromQueryString(targetSelector, queryParamNames) { var selector = String(targetSelector || "").trim() || "#DocManFilesTable"; var target = document.querySelector(selector); if (!target) { throw new Error("Target element not found for selector: " + selector); } var productGroupGuid = _DocMan_getProductGroupGuidFromQueryString(queryParamNames); if (!productGroupGuid) { target.innerHTML = "
Missing product group GUID in query string.
"; return []; } target.innerHTML = "
Loading files...
"; var rows = await _GerAllFilesInTechFile(productGroupGuid); target.innerHTML = _DocMan_formatFilesAsTable(rows); return rows; } class DocViewer { constructor({ ID, ReplaceText, ProductGUID, AccountGUID, GlobalDocument = false, DocCategory, HighlightMode = false, ShowHighlightButton = false, ShowCategoryColumn = false, CompactActionButtons = false, PageSize = 10, AllowMultipleFiles = false, MaxFileSizeMB = null, maxFileSizeMB = null, FileTypeLimit = "", fileTypeLimit = "", UseDragAndDrop = false, HideGridCard = false, hideGridCard = false, HideSectionTitle = false, hideSectionTitle = false, SectionTitleText = "", sectionTitleText = "", ActionButtons = [] }) { this.controlId = ID || `docviewer-${crypto.randomUUID()}`; this.replaceText = ReplaceText; this.productGuid = this.normalizeGuid(ProductGUID); this.accountGuid = this.normalizeGuid(AccountGUID); this.globalDocument = this.normalizeBoolean(GlobalDocument, false); this.docCategory = this.normalizeDocCategory(DocCategory); this.rowGuidField = "cr5ad_gprs_attachmentstorageid"; this.highlightModeEnabled = this.normalizeBoolean(HighlightMode, false); this.showAllDocuments = this.highlightModeEnabled && !this.globalDocument; this.showHighlightButton = this.highlightModeEnabled && !this.globalDocument && Boolean(ShowHighlightButton); this.showCategoryColumn = this.normalizeBoolean(ShowCategoryColumn, false); this.compactActionButtons = this.normalizeBoolean(CompactActionButtons, false); this.hideSectionTitle = this.normalizeBoolean(HideSectionTitle ?? hideSectionTitle, false); this.sectionTitleText = String(SectionTitleText ?? sectionTitleText ?? "").trim(); this.pageSize = Number(PageSize) || 10; this.allowMultipleFiles = Boolean(AllowMultipleFiles); this.uploadUiMode = this.normalizeUploadUiMode(UseDragAndDrop); this.hideGridCard = this.normalizeBoolean(HideGridCard ?? hideGridCard, false); this.currentPage = 1; this.totalCount = 0; this.allRows = []; this.sortState = { column: "createdon", direction: "desc" }; this.isBusy = false; this.maxFileSizeBytes = this.normalizeMaxFileSizeBytes(MaxFileSizeMB ?? maxFileSizeMB); this.allowedFileExtensions = this.normalizeFileTypeLimit(FileTypeLimit ?? fileTypeLimit); this.allowedFileExtensionSet = new Set(this.allowedFileExtensions); this.root = null; this.fileInput = null; this.uploadButton = null; this.uploadDropzone = null; this.uploadProgressWrap = null; this.uploadProgressBar = null; this.uploadProgressFill = null; this.uploadProgressText = null; this.uploadEta = null; this.uploadSpinner = null; this.uploadSizeHint = null; this.uploadTypeHint = null; this.categoryPickerSection = null; this.categoryPickerTableBody = null; this.categoryPickerStatus = null; this.categoryPickerConfirmBtn = null; this.categoryPickerCancelBtn = null; this.tbody = null; this.pageInfo = null; this.prevBtn = null; this.nextBtn = null; this.refreshBtn = null; this.filterModeSelect = null; this.filterModeCategoryOption = null; this.filterModeHint = null; this.tableHeaderRow = null; this.gridSection = null; this.gridRefreshOverlay = null; this.errorBox = null; this.errorDiagnosticsWrap = null; this.errorDiagnosticsTextarea = null; this.copyDiagnosticsBtn = null; this.copyDiagnosticsStatus = null; this.lastErrorDiagnosticsText = ""; this.copyDiagnosticsStatusTimerId = null; this.lastGridLoadOutcome = { passed: false, attempted: false, lastAttemptUtc: "", lastSuccessUtc: "", rowCount: 0, errorMessage: "" }; this.categoryListTbody = null; this.categoryListStatus = null; this.pendingUploadFiles = []; this.availableUploadCategories = []; this.docCategoryDetails = null; this.docCategoryDetailsPromise = null; this.docCategoryDetailsById = new Map(); this.docCategoryDetailsPromiseById = new Map(); this.actionButtons = this.normalizeActionButtons(ActionButtons); this.uploadProgressHideTimerId = null; this.sizeLimitModal = null; this.sizeLimitModalTitle = null; this.sizeLimitModalMessage = null; this.sizeLimitModalCloseBtn = null; this.sizeLimitModalConfirmBtn = null; this.sizeLimitModalBackdrop = null; this.boundHandleSizeLimitModalKeydown = null; this.uploadSuccessModal = null; this.uploadSuccessModalTitle = null; this.uploadSuccessModalMessage = null; this.uploadSuccessModalCloseBtn = null; this.uploadSuccessModalConfirmBtn = null; this.uploadSuccessModalBackdrop = null; this.boundHandleUploadSuccessModalKeydown = null; } normalizeUploadUiMode(value) { if (value === undefined || value === null || value === "") { return "button"; } if (typeof value === "boolean") { return value ? "dragdrop" : "button"; } const text = String(value).trim().toLowerCase(); return text === "dragdrop" ? "dragdrop" : "button"; } isDragDropMode() { return this.uploadUiMode === "dragdrop"; } normalizeMaxFileSizeBytes(value) { if (value === null || value === undefined || String(value).trim() === "") { return null; } const parsedMb = Number.parseFloat(String(value).trim()); if (!Number.isFinite(parsedMb) || parsedMb <= 0) { return null; } return Math.round(parsedMb * 1024 * 1024); } normalizeFileTypeLimit(value) { const raw = String(value ?? "").trim(); if (!raw) return []; // Normalize to lowercase ".ext" tokens and remove duplicates. const uniqueExtensions = new Set(); raw.split(",").forEach((part) => { const token = String(part || "").trim().toLowerCase(); if (!token) return; const withoutLeadingDot = token.replace(/^\.+/, ""); if (!withoutLeadingDot) return; // Keep simple extension tokens only (letters/numbers) to avoid invalid accept values. if (!/^[a-z0-9]+$/i.test(withoutLeadingDot)) return; uniqueExtensions.add(`.${withoutLeadingDot}`); }); return Array.from(uniqueExtensions); } formatBytes(bytes) { const numericBytes = Number(bytes) || 0; if (numericBytes <= 0) return "0 B"; const units = ["B", "KB", "MB", "GB", "TB"]; const unitIndex = Math.min(Math.floor(Math.log(numericBytes) / Math.log(1024)), units.length - 1); const value = numericBytes / (1024 ** unitIndex); const decimals = value >= 10 ? 0 : 1; return `${value.toFixed(decimals)} ${units[unitIndex]}`; } getMaxFileSizeHintText() { if (!this.maxFileSizeBytes) { return "Max file size: No limit"; } return `Max file size: ${this.formatBytes(this.maxFileSizeBytes)} per file`; } getFileTypeHintText() { if (!this.allowedFileExtensions.length) { return "Allowed file types: All documents"; } return `Allowed file types: ${this.allowedFileExtensions.join(", ")}`; } getFileExtension(fileName) { const name = String(fileName || "").trim().toLowerCase(); const lastDotIndex = name.lastIndexOf("."); if (lastDotIndex <= 0 || lastDotIndex === name.length - 1) return ""; return name.slice(lastDotIndex); } isAllowedFileType(fileName) { if (!this.allowedFileExtensions.length) return true; const extension = this.getFileExtension(fileName); if (!extension) return false; return this.allowedFileExtensionSet.has(extension); } normalizeGuid(value) { const s = String(value ?? "").trim().replace(/[{}()\s]/g, "").toLowerCase(); if (!s) return ""; const dashless = /^[0-9a-f]{32}$/.test(s) ? s.replace(/^(.{8})(.{4})(.{4})(.{4})(.{12})$/, "$1-$2-$3-$4-$5") : s; if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(dashless)) return ""; if (dashless === "00000000-0000-0000-0000-000000000000") return ""; return dashless; } normalizeBoolean(value, defaultValue = false) { if (value === undefined || value === null || value === "") { return defaultValue; } if (typeof value === "boolean") { return value; } const text = String(value).trim().toLowerCase(); if (!text) { return defaultValue; } if (text === "true" || text === "1" || text === "yes") { return true; } if (text === "false" || text === "0" || text === "no") { return false; } return defaultValue; } getActiveScopeConfig() { if (this.globalDocument) { return { mode: "account", guidKey: "AccountGUID", guidValue: this.normalizeGuid(this.accountGuid), serverFunction: "GetDocsForAccount", missingMessage: "missing account id." }; } return { mode: "product", guidKey: "ProductGUID", guidValue: this.normalizeGuid(this.productGuid), serverFunction: "GetDocForTechFile", missingMessage: "missing product id." }; } getUploadMetadataValidation() { const productGuid = this.globalDocument ? "" : this.normalizeGuid(this.productGuid); const accountGuid = this.normalizeGuid(this.accountGuid); const missing = []; if (!productGuid && !this.globalDocument) { missing.push("product id"); } if (!accountGuid) { missing.push("account id"); } return { productGuid, accountGuid, isValid: missing.length === 0, missingMessage: missing.length ? `missing ${missing.join(" and ")}.` : "" }; } getStartupGuidValidation() { const accountGuid = this.normalizeGuid(this.accountGuid); const productGuid = this.globalDocument ? "" : this.normalizeGuid(this.productGuid); const accountGuidValid = Boolean(accountGuid); const productGuidValid = this.globalDocument ? true : Boolean(productGuid); const missing = []; if (!accountGuidValid) missing.push("Account GUID"); if (!productGuidValid) missing.push("Product GUID"); return { isValid: missing.length === 0, accountGuidValid, productGuidValid, userMessage: missing.length ? `Configuration error: ${missing.join(" and ")} is missing or invalid. Please contact support@GlobalTradeDept.com.` : "" }; } normalizeDocCategory(value, { asNumber = false } = {}) { const raw = String(value ?? "").trim(); if (!raw) return null; const parsed = Number.parseInt(raw, 10); if (Number.isNaN(parsed)) return null; return asNumber ? parsed : String(parsed); } isSortableColumn(columnName) { return columnName === "cr5ad_filename" || columnName === "createdon" || columnName === "gtdv1_documentcategory"; } getDefaultSortDirection(columnName) { return columnName === "createdon" ? "desc" : "asc"; } getAriaSortValue(columnName) { if (this.sortState.column !== columnName) { return "none"; } return this.sortState.direction === "asc" ? "ascending" : "descending"; } getSortIndicator(columnName) { if (this.sortState.column !== columnName) { return "↕"; } return this.sortState.direction === "asc" ? "▲" : "▼"; } getSortButtonLabel(columnName, label) { const isActive = this.sortState.column === columnName; const currentDirection = isActive ? this.sortState.direction : this.getDefaultSortDirection(columnName); const nextDirection = isActive ? (currentDirection === "asc" ? "desc" : "asc") : currentDirection; const nextDirectionLabel = nextDirection === "asc" ? "ascending" : "descending"; return `${label}. Activate to sort ${nextDirectionLabel}.`; } buildSortableHeader(columnName, label, width) { const isActive = this.sortState.column === columnName; const activeClass = isActive ? " is-active" : ""; return ` `; } compareTextValues(leftValue, rightValue) { return String(leftValue || "").localeCompare(String(rightValue || ""), undefined, { numeric: true, sensitivity: "base" }); } compareCreatedValues(leftValue, rightValue) { const leftTime = Date.parse(leftValue || ""); const rightTime = Date.parse(rightValue || ""); const leftIsValid = Number.isFinite(leftTime); const rightIsValid = Number.isFinite(rightTime); if (leftIsValid && rightIsValid) { return leftTime - rightTime; } if (leftIsValid) return -1; if (rightIsValid) return 1; return this.compareTextValues(leftValue, rightValue); } getRowCategorySortText(row) { const formattedValue = String(row?.["gtdv1_documentcategory@OData.Community.Display.V1.FormattedValue"] || "").trim(); if (formattedValue) { return formattedValue; } const categoryValue = this.getRowCategoryValue(row); const cachedCategoryName = String(this.docCategoryDetailsById.get(categoryValue)?.categoryName || "").trim(); return cachedCategoryName || categoryValue; } compareRows(leftRow, rightRow) { const directionMultiplier = this.sortState.direction === "asc" ? 1 : -1; let result = 0; if (this.sortState.column === "cr5ad_filename") { result = this.compareTextValues(leftRow?.cr5ad_filename, rightRow?.cr5ad_filename); } else if (this.sortState.column === "gtdv1_documentcategory") { result = this.compareTextValues(this.getRowCategorySortText(leftRow), this.getRowCategorySortText(rightRow)); } else { result = this.compareCreatedValues(leftRow?.createdon, rightRow?.createdon); } if (result === 0) { result = this.compareTextValues(leftRow?.cr5ad_filename, rightRow?.cr5ad_filename); } if (result === 0) { result = this.compareTextValues( this.getRowGuid(leftRow) || this.getRowCategorySortText(leftRow), this.getRowGuid(rightRow) || this.getRowCategorySortText(rightRow) ); } return result * directionMultiplier; } sortAllRowsInPlace() { if (!Array.isArray(this.allRows) || this.allRows.length <= 1 || !this.isSortableColumn(this.sortState.column)) { return; } this.allRows.sort((leftRow, rightRow) => this.compareRows(leftRow, rightRow)); } normalizeActionButtons(actionButtons) { const sourceButtons = Array.isArray(actionButtons) ? actionButtons : []; const normalizedButtons = sourceButtons .map((button, index) => { const id = String(button?.Id || button?.id || "").trim(); const name = String(button?.Name || button?.name || "").trim(); if (!id || !name) return null; const className = String(button?.ClassName || button?.className || "btn btn-sm btn-outline-primary").trim() || "btn btn-sm btn-outline-primary"; const visibleWhenHighlightedOn = button?.VisibleWhenHighlightedOn ?? button?.visibleWhenHighlightedOn; const visibleWhenHighlightedOff = button?.VisibleWhenHighlightedOff ?? button?.visibleWhenHighlightedOff; const clickHandler = button?.OnClick ?? button?.onClick ?? button?.HandlerFunction ?? button?.handlerFunction ?? button?.Handler ?? button?.handler; const clickHandlerName = typeof clickHandler === "string" ? clickHandler.trim() : ""; return { id, name, className, clickHandler: typeof clickHandler === "function" ? clickHandler : null, clickHandlerName, visibleWhenHighlightedOn: visibleWhenHighlightedOn === undefined ? true : Boolean(visibleWhenHighlightedOn), visibleWhenHighlightedOff: visibleWhenHighlightedOff === undefined ? true : Boolean(visibleWhenHighlightedOff), order: Number(button?.Order || button?.order || index) }; }) .filter(Boolean) .sort((a, b) => a.order - b.order); if (normalizedButtons.length) { return normalizedButtons; } // Backward-compatible default action. return [{ id: "view", name: "View", className: "btn btn-sm btn-outline-primary", visibleWhenHighlightedOn: true, visibleWhenHighlightedOff: true, order: 0 }]; } getVisibleActionButtons() { const showHighlightedOn = this.shouldRenderHighlightBadges(); return this.actionButtons.filter((button) => ( showHighlightedOn ? button.visibleWhenHighlightedOn : button.visibleWhenHighlightedOff )); } getActionButtonConfig(buttonId) { const normalizedButtonId = String(buttonId || "").trim(); if (!normalizedButtonId) return null; return this.actionButtons.find((button) => button.id === normalizedButtonId) || null; } getRowByGuid(rowGuid) { const normalizedRowGuid = String(rowGuid || "").trim(); if (!normalizedRowGuid) return null; return this.allRows.find((row) => this.getRowGuid(row) === normalizedRowGuid) || null; } resolveActionButtonHandler(actionButton) { if (!actionButton) return null; if (typeof actionButton.clickHandler === "function") { return actionButton.clickHandler; } if (actionButton.clickHandlerName && typeof window[actionButton.clickHandlerName] === "function") { return window[actionButton.clickHandlerName]; } return null; } invokeActionButtonHandler(rowGuid, buttonName, buttonId) { const normalizedRowGuid = String(rowGuid || "").trim(); const normalizedButtonId = String(buttonId || "unknown").trim() || "unknown"; const normalizedButtonName = String(buttonName || "unknown").trim() || "unknown"; const actionButton = this.getActionButtonConfig(normalizedButtonId); const context = { rowGuid: normalizedRowGuid, buttonId: normalizedButtonId, buttonName: normalizedButtonName, row: this.getRowByGuid(normalizedRowGuid), controlId: this.controlId, globalDocument: this.globalDocument, accountGuid: this.normalizeGuid(this.accountGuid), productGuid: this.normalizeGuid(this.productGuid) }; const explicitHandler = this.resolveActionButtonHandler(actionButton); if (explicitHandler) { explicitHandler(normalizedRowGuid, context); return; } if (actionButton?.clickHandlerName) { console.warn(`DocViewer action '${normalizedButtonId}' configured handler '${actionButton.clickHandlerName}', but no matching function was found on window.`); } if (typeof window.handleGridButtonClick === "function") { window.handleGridButtonClick(normalizedRowGuid, normalizedButtonName, normalizedButtonId, context); } else { console.warn("handleGridButtonClick(rowGuid, buttonName, buttonId, context) is not defined on window."); } } resolveMountTarget() { const replaceKey = String(this.replaceText || "").trim(); if (!replaceKey) { return null; } const escapeAttributeValue = (value) => String(value) .replace(/\\/g, "\\\\") .replace(/"/g, '\\"'); const escapedKey = escapeAttributeValue(replaceKey); // Prefer mounting inside the specific MSF section cell to avoid replacing form-level containers. const preferredSelectors = [ `fieldset[aria-label="${escapedKey}"] table.section[data-name="${escapedKey}"] td.cell.clearfix`, `table.section[data-name="${escapedKey}"] td.cell.clearfix`, `fieldset[aria-label="${escapedKey}"] table.section[data-name="${escapedKey}"] td.cell`, `table.section[data-name="${escapedKey}"] td.cell` ]; for (const selector of preferredSelectors) { const candidate = document.querySelector(selector); if (candidate) { console.log("DocViewer.resolveMountTarget: using MSF section selector", { replaceKey, selector, tagName: candidate.tagName, className: candidate.className || "" }); return candidate; } } // Backward-compatible fallback: match an exact-text DIV as before. const fallbackDiv = Array.from(document.querySelectorAll("div")) .find((div) => (div.textContent || "").trim() === replaceKey); return fallbackDiv || null; } expandMfsMountCellIfNeeded() { if (!this.root || this.root.tagName !== "TD") { return; } const sectionRow = this.root.parentElement; if (!sectionRow || sectionRow.tagName !== "TR") { return; } const sectionTable = sectionRow.closest("table.section"); if (!sectionTable) { return; } const nextCell = this.root.nextElementSibling; if (!nextCell || nextCell.tagName !== "TD") { return; } if (nextCell.classList.contains("zero-cell")) { return; } const nextCellHasContent = String(nextCell.textContent || "").trim().length > 0 || !!nextCell.querySelector("input,select,textarea,button,a,[data-name],iframe,img"); if (nextCellHasContent) { return; } const existingColspan = Number.parseInt(this.root.getAttribute("colspan") || "1", 10); const safeColspan = Number.isFinite(existingColspan) && existingColspan > 0 ? existingColspan : 1; this.root.setAttribute("colspan", String(safeColspan + 1)); this.root.style.width = "100%"; nextCell.style.display = "none"; console.log("DocViewer.expandMfsMountCellIfNeeded: expanded mount cell to use adjacent empty section column", { controlId: this.controlId, previousColspan: safeColspan, newColspan: safeColspan + 1 }); } applySectionTitleSettings() { if (!this.root) { return; } const fieldset = this.root.closest("fieldset"); if (!fieldset) { return; } const sectionTitle = fieldset.querySelector("legend.section-title h3, legend h3"); if (!sectionTitle) { return; } const sectionLegend = sectionTitle.closest("legend"); if (this.sectionTitleText) { sectionTitle.textContent = this.sectionTitleText; if (sectionLegend) { sectionLegend.style.display = ""; sectionLegend.hidden = false; } return; } if (this.hideSectionTitle && sectionLegend) { sectionLegend.style.display = "none"; sectionLegend.hidden = true; } } init() { const target = this.resolveMountTarget(); if (!target) { console.warn(`DocViewer could not find a mount target for '${this.replaceText}'.`); return; } console.log("DocViewer.init: mount target resolved", { replaceText: this.replaceText, controlId: this.controlId, tagName: target.tagName, className: target.className || "", id: target.id || "" }); // Reuse the placeholder element so any page-level layout classes/styles are preserved. this.root = target; this.root.id = this.controlId; this.expandMfsMountCellIfNeeded(); this.applySectionTitleSettings(); this.ensureControlStyles(); this.root.innerHTML = this.buildTemplate(); this.cacheElements(); this.renderTableHeader(); this.wireEvents(); this.refreshFilteredModeButtonText(); const startupValidation = this.getStartupGuidValidation(); if (!startupValidation.isValid) { console.error("DocViewer.init: invalid or missing GUIDs detected at startup.", { controlId: this.controlId, accountGuidValid: startupValidation.accountGuidValid, productGuidValid: startupValidation.productGuidValid, accountGuidRaw: this.accountGuid, productGuidRaw: this.productGuid }); const diagnosticsText = this.buildErrorDiagnosticsText({ operation: "init", userMessage: startupValidation.userMessage, err: { message: startupValidation.userMessage }, files: [], extra: { reason: "invalid-guid-at-startup", accountGuidValid: startupValidation.accountGuidValid, productGuidValid: startupValidation.productGuidValid } }); this.setError(startupValidation.userMessage, diagnosticsText); return; } if (!this.hideGridCard) { this.loadPage(1); } } buildTemplate() { const filterToggle = !this.hideGridCard && this.showHighlightButton ? `

Documents marked with a star match the current document category.

` : ""; const categoryPickerAttentionClass = this.requiresUploadCategoryPicker() ? " dv-category-picker-attention" : ""; const uploadPickerMarkup = this.isDragDropMode() ? `
Drag and drop ${this.allowMultipleFiles ? "files" : "a file"} here
or press Enter/Space to browse
` : `
`; const gridMarkup = this.hideGridCard ? "" : `
Loading…
Page 1
Version ${DocManVer}
`; return `
${uploadPickerMarkup}
${this.getMaxFileSizeHintText()}
${this.getFileTypeHintText()}
${filterToggle} ${gridMarkup}
`; } ensureControlStyles() { const styleId = "docviewer-control-styles"; if (document.getElementById(styleId)) return; const style = document.createElement("style"); style.id = styleId; style.textContent = ` .dv-card { background: #ffffff; border: 1px solid #dee2e6; border-radius: 0.75rem; overflow: hidden; } .dv-card-section + .dv-card-section { border-top: 1px solid #e9ecef; } .dv-upload-stack { gap: 0.6rem; } .dv-upload-controls { gap: 0.5rem; } .dv-dropzone { width: 100%; min-height: 7rem; border: 2px dashed #9ec5fe; border-radius: 0.65rem; background: #f8fbff; color: #0a58ca; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 0.9rem; cursor: pointer; transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease; } .dv-dropzone:focus { outline: none; box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); } .dv-dropzone.is-active { border-color: #0d6efd; background: #e7f1ff; } .dv-dropzone.is-disabled { opacity: 0.65; cursor: not-allowed; } .dv-dropzone-title { font-weight: 600; } .dv-dropzone-subtitle { font-size: 0.82rem; margin-top: 0.2rem; } .dv-upload-hints { display: flex; flex-direction: column; gap: 0.15rem; } .dv-upload-hint { font-size: 0.78rem; line-height: 1.2; } .dv-category-picker-attention { border: 2px solid #dc3545; border-radius: 0.5rem; margin: 0.5rem; } .dv-page-info { font-size: 0.82rem; line-height: 1.2; } .dv-grid-section { position: relative; overflow: hidden; } .dv-grid-section .table { font-size: 0.875rem; } .dv-grid-section .table th, .dv-grid-section .table td { padding-top: 0.375rem; padding-bottom: 0.375rem; } .dv-sort-button { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0; border: none; background: transparent; color: #212529; font: inherit; font-weight: 600; text-align: left; } .dv-sort-button:hover, .dv-sort-button:focus { color: #0d6efd; } .dv-sort-button.is-active { color: #0d6efd; } .dv-sort-indicator { min-width: 0.9rem; font-size: 0.75rem; line-height: 1; text-align: center; } .dv-action-buttons { align-items: flex-start; } .dv-action-button--compact { --bs-btn-padding-y: 0.12rem; --bs-btn-padding-x: 0.45rem; --bs-btn-font-size: 0.75rem; line-height: 1.15; white-space: nowrap; } .dv-error-diagnostics { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.75rem; line-height: 1.35; white-space: pre; overflow: auto; resize: none; } .dv-error-support-note { font-size: 0.875rem; line-height: 1.35; color: #212529; } .dv-error-pane { width: 90%; max-width: none; box-sizing: border-box; } .dv-grid-overlay { position: absolute; inset: 0; z-index: 10; display: flex; align-items: center; justify-content: center; background: rgba(255, 255, 255, 0.78); backdrop-filter: blur(1px); } .dv-grid-overlay-card { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; padding: 0.75rem 1rem; border-radius: 0.75rem; border: 1px solid rgba(0, 0, 0, 0.08); background: rgba(255, 255, 255, 0.95); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1); } .dv-modal-backdrop { position: fixed; inset: 0; z-index: 1050; background: rgba(0, 0, 0, 0.55); } .dv-size-limit-modal { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1051; width: min(92vw, 560px); background: #ffffff; border: 2px solid #dc3545; border-radius: 0.5rem; box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.3); } .dv-size-limit-header { display: flex; align-items: center; justify-content: space-between; background: #dc3545; color: #ffffff; padding: 0.75rem 1rem; border-top-left-radius: 0.35rem; border-top-right-radius: 0.35rem; } .dv-size-limit-title { margin: 0; font-size: 1.1rem; font-weight: 700; } .dv-size-limit-body { padding: 1rem; } .dv-size-limit-message { margin: 0; color: #212529; font-size: 0.98rem; } .dv-size-limit-footer { display: flex; justify-content: flex-end; padding: 0 1rem 1rem; } .dv-size-limit-close { border: none; background: transparent; color: #ffffff; font-size: 1.6rem; line-height: 1; cursor: pointer; padding: 0; } .dv-success-modal { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1051; width: min(92vw, 560px); background: #ffffff; border: 2px solid #198754; border-radius: 0.5rem; box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.3); } .dv-success-header { display: flex; align-items: center; justify-content: space-between; background: #198754; color: #ffffff; padding: 0.75rem 1rem; border-top-left-radius: 0.35rem; border-top-right-radius: 0.35rem; } .dv-success-title { margin: 0; font-size: 1.1rem; font-weight: 700; } .dv-success-body { padding: 1rem; } .dv-success-message { margin: 0; color: #212529; font-size: 0.98rem; } .dv-success-footer { display: flex; justify-content: flex-end; padding: 0 1rem 1rem; } .dv-success-close { border: none; background: transparent; color: #ffffff; font-size: 1.6rem; line-height: 1; cursor: pointer; padding: 0; } .dv-clock { position: relative; width: 2.6rem; height: 2.6rem; } .dv-clock-face { position: absolute; inset: 0; border-radius: 999px; border: 2px solid #0d6efd; background: #ffffff; box-shadow: inset 0 0 0 2px rgba(13, 110, 253, 0.12); } .dv-clock-hand { position: absolute; left: 50%; top: 50%; transform-origin: bottom center; border-radius: 999px; background: #0d6efd; } .dv-clock-hand-hour { width: 0.16rem; height: 0.68rem; margin-left: -0.08rem; margin-top: -0.68rem; animation: dv-spin-hour 3s linear infinite; } .dv-clock-hand-minute { width: 0.12rem; height: 0.95rem; margin-left: -0.06rem; margin-top: -0.95rem; animation: dv-spin-minute 1.2s linear infinite; } .dv-clock-center { position: absolute; left: 50%; top: 50%; width: 0.32rem; height: 0.32rem; margin-left: -0.16rem; margin-top: -0.16rem; border-radius: 999px; background: #0d6efd; } @keyframes dv-spin-minute { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes dv-spin-hour { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } `; document.head.appendChild(style); } cacheElements() { this.fileInput = this.root.querySelector("input[data-role='upload-file-input']"); this.uploadButton = this.root.querySelector("button[data-role='upload']"); this.uploadDropzone = this.root.querySelector("[data-role='upload-dropzone']"); this.uploadProgressWrap = this.root.querySelector("[data-role='upload-progress-wrap']"); this.uploadProgressBar = this.root.querySelector("[data-role='upload-progress-bar']"); this.uploadProgressFill = this.root.querySelector("[data-role='upload-progress-fill']"); this.uploadProgressText = this.root.querySelector("[data-role='upload-progress-message']"); this.uploadEta = this.root.querySelector("[data-role='upload-eta']"); this.uploadSpinner = this.root.querySelector("[data-role='upload-spinner']"); this.uploadSizeHint = this.root.querySelector("[data-role='upload-size-hint']"); this.uploadTypeHint = this.root.querySelector("[data-role='upload-type-hint']"); this.categoryPickerSection = this.root.querySelector("[data-role='category-picker-section']"); this.categoryPickerTableBody = this.root.querySelector("tbody[data-role='category-picker-rows']"); this.categoryPickerStatus = this.root.querySelector("[data-role='category-picker-status']"); this.categoryPickerConfirmBtn = this.root.querySelector("[data-role='category-picker-confirm']"); this.categoryPickerCancelBtn = this.root.querySelector("[data-role='category-picker-cancel']"); this.tbody = this.root.querySelector("tbody[data-role='rows']"); this.pageInfo = this.root.querySelector("[data-role='page-info']"); this.prevBtn = this.root.querySelector("button[data-role='prev']"); this.nextBtn = this.root.querySelector("button[data-role='next']"); this.refreshBtn = this.root.querySelector("button[data-role='refresh']"); this.filterModeSelect = this.root.querySelector("select[data-role='filter-mode']"); this.filterModeCategoryOption = this.filterModeSelect?.querySelector("option[value='category']") || null; this.filterModeHint = this.root.querySelector("[data-role='filter-mode-hint']"); this.tableHeaderRow = this.root.querySelector("tr[data-role='header-row']"); this.gridSection = this.root.querySelector(`#${this.controlId}-grid-section`); this.gridRefreshOverlay = this.root.querySelector("[data-role='grid-refresh-overlay']"); this.statusBox = this.root.querySelector("[data-role='status']"); this.errorBox = this.root.querySelector("[data-role='error']"); this.errorDiagnosticsWrap = this.root.querySelector("[data-role='error-diagnostics-wrap']"); this.errorDiagnosticsTextarea = this.root.querySelector("[data-role='error-diagnostics-text']"); this.copyDiagnosticsBtn = this.root.querySelector("[data-role='copy-diagnostics']"); this.copyDiagnosticsStatus = this.root.querySelector("[data-role='copy-diagnostics-status']"); this.categoryListTbody = this.root.querySelector("tbody[data-role='category-list-rows']"); this.categoryListStatus = this.root.querySelector("[data-role='category-list-status']"); this.sizeLimitModal = this.root.querySelector("[data-role='size-limit-modal']"); this.sizeLimitModalTitle = this.root.querySelector("[data-role='size-limit-title']"); this.sizeLimitModalMessage = this.root.querySelector("[data-role='size-limit-message']"); this.sizeLimitModalCloseBtn = this.root.querySelector("[data-role='size-limit-close']"); this.sizeLimitModalConfirmBtn = this.root.querySelector("[data-role='size-limit-confirm']"); this.sizeLimitModalBackdrop = this.root.querySelector("[data-role='size-limit-backdrop']"); this.uploadSuccessModal = this.root.querySelector("[data-role='upload-success-modal']"); this.uploadSuccessModalTitle = this.root.querySelector("[data-role='upload-success-title']"); this.uploadSuccessModalMessage = this.root.querySelector("[data-role='upload-success-message']"); this.uploadSuccessModalCloseBtn = this.root.querySelector("[data-role='upload-success-close']"); this.uploadSuccessModalConfirmBtn = this.root.querySelector("[data-role='upload-success-confirm']"); this.uploadSuccessModalBackdrop = this.root.querySelector("[data-role='upload-success-backdrop']"); if (this.uploadSizeHint) { this.uploadSizeHint.textContent = this.getMaxFileSizeHintText(); } if (this.uploadTypeHint) { this.uploadTypeHint.textContent = this.getFileTypeHintText(); } if (this.fileInput) { if (this.allowedFileExtensions.length) { this.fileInput.setAttribute("accept", this.allowedFileExtensions.join(",")); } else { this.fileInput.removeAttribute("accept"); } } } setGridRefreshing(isRefreshing) { if (!this.gridRefreshOverlay) return; this.gridRefreshOverlay.hidden = !isRefreshing; } hideUploadProgressIfComplete() { if (!this.uploadProgressWrap || this.uploadProgressWrap.hidden) return; const percent = Number(this.uploadProgressBar?.getAttribute("aria-valuenow") || 0); if (percent < 100) return; this.uploadProgressWrap.hidden = true; } shouldRequestAllDocuments() { return !this.globalDocument && (!this.highlightModeEnabled || this.showAllDocuments); } shouldRenderHighlightBadges() { return this.highlightModeEnabled && this.shouldRequestAllDocuments(); } requiresUploadCategoryPicker() { return !this.highlightModeEnabled; } getFilteredModeButtonText(docCategory = this.docCategory, { withPrefix = true } = {}) { const normalizedCategory = String(docCategory || "").trim(); const categoryName = this.docCategoryDetailsById.get(normalizedCategory)?.categoryName || ""; if (!withPrefix) { return categoryName || normalizedCategory || "Unknown category"; } if (categoryName) { return `Show only ${categoryName}`; } return "Show only selected document category"; } async getDocCategoryDetails(docCategory = this.docCategory) { const normalizedCategory = String(docCategory || "").trim(); if (!normalizedCategory) { return null; } if (this.docCategoryDetailsById.has(normalizedCategory)) { return this.docCategoryDetailsById.get(normalizedCategory); } if (this.docCategoryDetailsPromiseById.has(normalizedCategory)) { return this.docCategoryDetailsPromiseById.get(normalizedCategory); } // Cache each category lookup promise so each category is fetched once per control instance. const lookupPromise = _DocMan_getDocCatDetails(normalizedCategory) .then((categoryName) => { const details = { docCategory: normalizedCategory, categoryName: String(categoryName || "").trim() }; this.docCategoryDetailsById.set(normalizedCategory, details); if (normalizedCategory === String(this.docCategory || "").trim()) { this.docCategoryDetails = details; } return details; }) .catch((err) => { console.warn("Unable to resolve document category details.", err); return null; }) .finally(() => { this.docCategoryDetailsPromiseById.delete(normalizedCategory); }); this.docCategoryDetailsPromiseById.set(normalizedCategory, lookupPromise); if (normalizedCategory === String(this.docCategory || "").trim()) { this.docCategoryDetailsPromise = lookupPromise; } return lookupPromise; } async refreshFilteredModeButtonText() { if (!this.filterModeCategoryOption) return; await this.getDocCategoryDetails(); this.filterModeCategoryOption.textContent = this.getFilteredModeButtonText(); } renderTableHeader() { if (!this.tableHeaderRow) return; const showCategoryColumn = this.getShouldShowCategoryColumn(); const fileNameWidth = "32"; const createdWidth = "18"; const categoryWidth = "22"; const actionsWidth = showCategoryColumn ? "28" : "50"; this.tableHeaderRow.innerHTML = ` Row GUID ${this.buildSortableHeader("cr5ad_filename", "File name", fileNameWidth)} ${this.buildSortableHeader("createdon", "Created", createdWidth)} ${showCategoryColumn ? this.buildSortableHeader("gtdv1_documentcategory", "Document category", categoryWidth) : ""} Actions `; } wireEvents() { if (this.uploadButton) { this.uploadButton.addEventListener("click", () => this.handleUpload()); } if (this.fileInput) { this.fileInput.addEventListener("change", async () => { await this.handleUpload(); }); } if (this.uploadDropzone && this.fileInput) { const preventDefault = (event) => { event.preventDefault(); event.stopPropagation(); }; this.uploadDropzone.addEventListener("click", () => { if (this.isBusy) return; this.fileInput.click(); }); this.uploadDropzone.addEventListener("keydown", (event) => { if (this.isBusy) return; if (event.key === "Enter" || event.key === " ") { preventDefault(event); this.fileInput.click(); } }); ["dragenter", "dragover"].forEach((eventName) => { this.uploadDropzone.addEventListener(eventName, (event) => { preventDefault(event); if (!this.isBusy) { this.uploadDropzone.classList.add("is-active"); } }); }); ["dragleave", "dragend"].forEach((eventName) => { this.uploadDropzone.addEventListener(eventName, (event) => { preventDefault(event); this.uploadDropzone.classList.remove("is-active"); }); }); this.uploadDropzone.addEventListener("drop", async (event) => { preventDefault(event); this.uploadDropzone.classList.remove("is-active"); if (this.isBusy) return; const droppedFiles = Array.from((event.dataTransfer && event.dataTransfer.files) || []); await this.handleSelectedFiles(droppedFiles); }); } if (this.categoryPickerConfirmBtn) { this.categoryPickerConfirmBtn.addEventListener("click", async () => { await this.handleCategoryPickerConfirm(); }); } if (this.categoryPickerCancelBtn) { this.categoryPickerCancelBtn.addEventListener("click", () => this.hideCategoryPicker()); } if (this.prevBtn) { this.prevBtn.addEventListener("click", () => { this.changePage(-1); }); } if (this.nextBtn) { this.nextBtn.addEventListener("click", () => { this.changePage(1); }); } if (this.refreshBtn) { this.refreshBtn.addEventListener("click", async () => { await this.handleManualRefresh(); }); } if (this.filterModeSelect) { this.filterModeSelect.addEventListener("change", async () => { await this.handleFilterModeChange(); }); } if (this.tableHeaderRow) { this.tableHeaderRow.addEventListener("click", async (event) => { const button = event.target.closest("button[data-sort-column]"); if (!button || !this.tableHeaderRow.contains(button)) return; await this.handleSortChange(button.getAttribute("data-sort-column") || ""); }); } if (this.tbody) { this.tbody.addEventListener("click", (event) => { const button = event.target.closest("button[data-action-id]"); if (!button || !this.tbody.contains(button)) return; event.preventDefault(); event.stopPropagation(); const rowGuid = button.getAttribute("data-id") || button.closest("tr")?.dataset.rowGuid || ""; const buttonId = button.getAttribute("data-action-id") || "unknown"; const buttonName = button.getAttribute("data-action-name") || button.textContent || "unknown"; this.invokeActionButtonHandler(rowGuid, buttonName, buttonId); }); } if (this.sizeLimitModalCloseBtn) { this.sizeLimitModalCloseBtn.addEventListener("click", () => this.hideSizeLimitModal()); } if (this.sizeLimitModalConfirmBtn) { this.sizeLimitModalConfirmBtn.addEventListener("click", () => this.hideSizeLimitModal()); } if (this.sizeLimitModalBackdrop) { this.sizeLimitModalBackdrop.addEventListener("click", () => this.hideSizeLimitModal()); } if (this.uploadSuccessModalCloseBtn) { this.uploadSuccessModalCloseBtn.addEventListener("click", () => this.hideUploadSuccessModal()); } if (this.uploadSuccessModalConfirmBtn) { this.uploadSuccessModalConfirmBtn.addEventListener("click", () => this.hideUploadSuccessModal()); } if (this.uploadSuccessModalBackdrop) { this.uploadSuccessModalBackdrop.addEventListener("click", () => this.hideUploadSuccessModal()); } if (this.copyDiagnosticsBtn) { this.copyDiagnosticsBtn.addEventListener("click", async () => { await this.copyErrorDiagnosticsToClipboard(); }); } } showSizeLimitModal(message, title = "Upload blocked") { const safeMessage = String(message || "Upload failed: file size limit exceeded.").trim(); const safeTitle = String(title || "Upload blocked").trim(); if (!this.sizeLimitModal || !this.sizeLimitModalBackdrop || !this.sizeLimitModalMessage) { alert(safeMessage); return; } if (this.sizeLimitModalTitle) { this.sizeLimitModalTitle.textContent = safeTitle; } this.sizeLimitModalMessage.textContent = safeMessage; this.sizeLimitModal.hidden = false; this.sizeLimitModalBackdrop.hidden = false; if (this.boundHandleSizeLimitModalKeydown) { document.removeEventListener("keydown", this.boundHandleSizeLimitModalKeydown); } this.boundHandleSizeLimitModalKeydown = (event) => { if (event.key === "Escape") { this.hideSizeLimitModal(); } }; document.addEventListener("keydown", this.boundHandleSizeLimitModalKeydown); if (this.sizeLimitModalConfirmBtn) { this.sizeLimitModalConfirmBtn.focus(); } } hideSizeLimitModal() { if (this.sizeLimitModal) { this.sizeLimitModal.hidden = true; } if (this.sizeLimitModalBackdrop) { this.sizeLimitModalBackdrop.hidden = true; } if (this.boundHandleSizeLimitModalKeydown) { document.removeEventListener("keydown", this.boundHandleSizeLimitModalKeydown); this.boundHandleSizeLimitModalKeydown = null; } } showUploadSuccessModal(message, title = "Upload complete") { const safeMessage = String(message || "Upload completed successfully.").trim(); const safeTitle = String(title || "Upload complete").trim(); if (!this.uploadSuccessModal || !this.uploadSuccessModalBackdrop || !this.uploadSuccessModalMessage) { alert(safeMessage); return; } if (this.uploadSuccessModalTitle) { this.uploadSuccessModalTitle.textContent = safeTitle; } this.uploadSuccessModalMessage.textContent = safeMessage; this.uploadSuccessModal.hidden = false; this.uploadSuccessModalBackdrop.hidden = false; if (this.boundHandleUploadSuccessModalKeydown) { document.removeEventListener("keydown", this.boundHandleUploadSuccessModalKeydown); } this.boundHandleUploadSuccessModalKeydown = (event) => { if (event.key === "Escape") { this.hideUploadSuccessModal(); } }; document.addEventListener("keydown", this.boundHandleUploadSuccessModalKeydown); if (this.uploadSuccessModalConfirmBtn) { this.uploadSuccessModalConfirmBtn.focus(); } } hideUploadSuccessModal() { if (this.uploadSuccessModal) { this.uploadSuccessModal.hidden = true; } if (this.uploadSuccessModalBackdrop) { this.uploadSuccessModalBackdrop.hidden = true; } if (this.boundHandleUploadSuccessModalKeydown) { document.removeEventListener("keydown", this.boundHandleUploadSuccessModalKeydown); this.boundHandleUploadSuccessModalKeydown = null; } } async ensureUploadCategoriesLoaded() { if (this.availableUploadCategories.length) { return this.availableUploadCategories; } const categories = await _DocMan_getCategories(false); if (!Array.isArray(categories) || !categories.length) { throw new Error("No document categories are available for upload."); } this.availableUploadCategories = categories.slice(); categories.forEach((category) => { const categoryValue = String(category?.value || category?.docCat || "").trim(); const categoryName = String(category?.text || category?.name || "").trim(); if (!categoryValue || !categoryName) return; this.docCategoryDetailsById.set(categoryValue, { docCategory: categoryValue, categoryName: categoryName }); }); return this.availableUploadCategories; } renderCategoryPickerRows(filesToUpload) { if (!this.categoryPickerTableBody) return; const optionsMarkup = [ ``, ...this.availableUploadCategories.map((category) => { const value = String(category?.value || category?.docCat || "").trim(); const label = String(category?.text || category?.name || value || "").trim(); return ``; }) ].join(""); this.categoryPickerTableBody.innerHTML = filesToUpload.map((file, index) => { const safeFileName = String(file?.name || "Unnamed file").trim() || "Unnamed file"; const safeSize = this.formatBytes(Number(file?.size || 0)); return ` ${this.escape(safeFileName)} ${this.escape(safeSize)} `; }).join(""); } async showCategoryPicker(filesToUpload) { if (!this.categoryPickerSection || !this.categoryPickerTableBody) { throw new Error("Upload category picker is unavailable."); } await this.ensureUploadCategoriesLoaded(); this.pendingUploadFiles = Array.isArray(filesToUpload) ? filesToUpload.slice() : []; this.renderCategoryPickerRows(this.pendingUploadFiles); if (this.categoryPickerStatus) { const count = this.pendingUploadFiles.length; this.categoryPickerStatus.textContent = `${count} file${count === 1 ? "" : "s"} ready`; } this.categoryPickerSection.hidden = false; const firstSelect = this.categoryPickerSection.querySelector("select[data-role='category-picker-select']"); if (firstSelect) { firstSelect.focus(); } } hideCategoryPicker() { this.pendingUploadFiles = []; if (this.categoryPickerSection) { this.categoryPickerSection.hidden = true; } if (this.categoryPickerTableBody) { this.categoryPickerTableBody.innerHTML = `No files selected.`; } if (this.categoryPickerStatus) { this.categoryPickerStatus.textContent = ""; } } getCategoryPickerSelections() { return this.pendingUploadFiles.map((file, index) => { const select = this.categoryPickerTableBody?.querySelector(`select[data-picker-index="${index}"]`) || null; return { file, docCategory: this.normalizeDocCategory(select?.value, { asNumber: true }), select }; }); } async handleCategoryPickerConfirm() { const selections = this.getCategoryPickerSelections(); if (!selections.length) { this.hideCategoryPicker(); return; } const firstMissingSelection = selections.find((entry) => entry.docCategory === null); if (firstMissingSelection) { this.setError("Select a document category for each file before upload."); firstMissingSelection.select?.focus(); return; } this.setError(""); await this.uploadFilesWithCategories( selections.map((entry) => entry.file), selections.map((entry) => entry.docCategory) ); } async handleFilterModeChange() { if (!this.filterModeSelect) return; this.showAllDocuments = this.filterModeSelect.value === "all"; this.updateFilterModeHint(); this.renderTableHeader(); this.resetPagingState(); await this.loadPage(1); } async handleSortChange(columnName) { if (!this.isSortableColumn(columnName)) return; if (columnName === "gtdv1_documentcategory") { await this.ensureCategoryDetailsLoaded(this.allRows); } const defaultDirection = this.getDefaultSortDirection(columnName); const nextDirection = this.sortState.column === columnName ? (this.sortState.direction === "asc" ? "desc" : "asc") : defaultDirection; this.sortState = { column: columnName, direction: nextDirection }; this.currentPage = 1; this.sortAllRowsInPlace(); this.renderTableHeader(); await this.renderRows(this.getCurrentPageRows()); this.updatePaging(); } updateFilterModeHint() { if (!this.filterModeHint) return; this.filterModeHint.hidden = !this.showAllDocuments; } async changePage(delta) { const nextPage = this.currentPage + delta; const totalPages = Math.max(1, Math.ceil(this.totalCount / this.pageSize)); if (nextPage < 1 || nextPage > totalPages) return; this.currentPage = nextPage; const pageRows = this.getCurrentPageRows(); await this.renderRows(pageRows); this.updatePaging(); } async handleManualRefresh() { this.resetUploadProgressUi(); await this.loadPage(this.currentPage || 1); } async loadCategoryList() { if (!this.categoryListTbody) return; this.categoryListTbody.innerHTML = `Loading categories...`; if (this.categoryListStatus) { this.categoryListStatus.textContent = ""; } try { const categories = await _DocMan_getCategories(false); if (!Array.isArray(categories)) { throw new Error("_DocMan_getCategories returned an invalid response."); } this.renderCategoryListRows(categories); if (this.categoryListStatus) { this.categoryListStatus.textContent = `${categories.length} categor${categories.length === 1 ? "y" : "ies"}`; } } catch (err) { console.error("DocViewer: failed to load category list", err); this.categoryListTbody.innerHTML = `Unable to load categories.`; if (this.categoryListStatus) { this.categoryListStatus.textContent = ""; } } } renderCategoryListRows(categories) { if (!this.categoryListTbody) return; const safeCategories = Array.isArray(categories) ? categories : []; if (!safeCategories.length) { this.categoryListTbody.innerHTML = `No categories found.`; return; } this.categoryListTbody.innerHTML = safeCategories.map((category) => { const docCatValue = Number.isFinite(Number.parseInt(category?.docCat, 10)) ? String(Number.parseInt(category.docCat, 10)) : ""; const name = String(category?.name || "").trim(); const isPublic = category?.isPublic === true; return ` ${this.escape(docCatValue)} ${this.escape(name)} ${isPublic ? "Yes" : "No"} `; }).join(""); } resetUploadProgressUi() { if (this.uploadProgressWrap) this.uploadProgressWrap.hidden = true; if (this.uploadProgressFill) { this.uploadProgressFill.style.width = "0%"; this.uploadProgressFill.textContent = "0%"; this.uploadProgressFill.classList.add("progress-bar-striped", "progress-bar-animated"); this.uploadProgressFill.classList.remove("bg-success", "bg-danger"); } if (this.uploadProgressBar) { this.uploadProgressBar.setAttribute("aria-valuenow", "0"); } if (this.uploadProgressText) { this.uploadProgressText.textContent = "Uploading..."; } if (this.uploadEta) { this.uploadEta.textContent = "Preparing upload..."; } if (this.uploadSpinner) { this.uploadSpinner.hidden = false; } } resetPagingState() { this.currentPage = 1; this.totalCount = 0; this.allRows = []; } buildServerLogicRequestOptions() { const scopeConfig = this.getActiveScopeConfig(); if (!scopeConfig.guidValue) return null; const normalizedDocCategory = this.normalizeDocCategory(this.docCategory); if (this.globalDocument && normalizedDocCategory === null) { throw new Error("Unable to load documents: missing document category."); } const requestOptions = { [scopeConfig.guidKey]: scopeConfig.guidValue, skipCache: true, ByPassCache: true }; if (this.shouldRequestAllDocuments()) { requestOptions.ShowAllDocuments = true; return requestOptions; } if (normalizedDocCategory !== null) { requestOptions.DocCategory = normalizedDocCategory; } return requestOptions; } buildServerLogicUrl(requestOptions) { const origin = (window.location && window.location.origin) ? window.location.origin.replace(/\/$/, "") : ""; const query = new URLSearchParams(); query.set("function", this.getActiveScopeConfig().serverFunction); Object.keys(requestOptions || {}).forEach((key) => { const value = requestOptions[key]; if (value === null || value === undefined) return; const textValue = String(value).trim(); if (!textValue) return; query.set(key, textValue); }); return `${origin}/_api/serverlogics/DocMan?${query.toString()}`; } parseServerLogicPayload(input) { if (input === null || input === undefined) { return null; } let current = input; for (let attempt = 0; attempt < 3; attempt += 1) { if (typeof current !== "string") { break; } try { current = JSON.parse(current); } catch (parseError) { return current; } } if (current && typeof current === "object" && typeof current.Body === "string") { try { current = JSON.parse(current.Body); } catch (bodyParseError) { return current; } } return current; } extractServerLogicRecords(payload) { const seenObjects = []; const queue = [payload]; while (queue.length > 0) { const current = this.parseServerLogicPayload(queue.shift()); if (!current) { continue; } if (Array.isArray(current)) { return current; } if (typeof current !== "object") { continue; } if (seenObjects.indexOf(current) >= 0) { continue; } seenObjects.push(current); if (Array.isArray(current.records)) return current.records; if (Array.isArray(current.value)) return current.value; if (Array.isArray(current.items)) return current.items; queue.push(current.Body, current.body, current.data, current.result, current.responseJSON, current.responseText); } return []; } normalizeServerLogicRow(row) { const categoryLabel = String( row?.gtdv1_documentcategory || row?.["gtdv1_documentcategory@OData.Community.Display.V1.FormattedValue"] || "" ).trim(); const rawCategory = this.normalizeDocCategory( row?.gtdv1_documentcategory_value !== undefined ? row.gtdv1_documentcategory_value : row?.gtdv1_documentcategory ); const normalizedRow = { cr5ad_gprs_attachmentstorageid: String(row?.cr5ad_gprs_attachmentstorageid || "").trim(), cr5ad_filename: String(row?.cr5ad_filename || "").trim(), createdon: row?.createdon || "", gtdv1_documentcategory: rawCategory || "" }; if (categoryLabel) { normalizedRow["gtdv1_documentcategory@OData.Community.Display.V1.FormattedValue"] = categoryLabel; } return normalizedRow; } async fetchRowsFromServerLogic() { const requestOptions = this.buildServerLogicRequestOptions(); if (!requestOptions) { throw new Error(`Unable to load documents: ${this.getActiveScopeConfig().missingMessage}`); } if (!window.webapi || typeof window.webapi.safeAjax !== "function") { throw new Error("window.webapi.safeAjax is not available on window."); } const url = this.buildServerLogicUrl(requestOptions); console.log("DocViewer.fetchRowsFromServerLogic: request", { controlId: this.controlId, scope: this.getActiveScopeConfig().mode, globalDocument: this.globalDocument, productGuid: this.normalizeGuid(this.productGuid), accountGuid: this.normalizeGuid(this.accountGuid), docCategory: this.normalizeDocCategory(this.docCategory), showAllDocuments: this.showAllDocuments, requestOptions, url }); const response = await window.webapi.safeAjax({ type: "GET", url: url, headers: { Accept: "application/json" }, xhrFields: { withCredentials: true }, contentType: "application/json", dataType: "json" }); const payload = this.parseServerLogicPayload(response); if (!payload || typeof payload !== "object") { throw new Error("Server logic returned an invalid response for DocViewer."); } if (String(payload.status || "").toLowerCase() === "error") { throw new Error(payload.message || "Server logic failed to load documents."); } const rows = this.extractServerLogicRecords(payload).map((row) => this.normalizeServerLogicRow(row)); const normalizedDocCategory = this.normalizeDocCategory(this.docCategory); const summarizeRowsByCategory = (items) => { return (Array.isArray(items) ? items : []).reduce((summary, row) => { const categoryValue = this.getRowCategoryValue(row) || "(blank)"; summary[categoryValue] = (summary[categoryValue] || 0) + 1; return summary; }, {}); }; const effectiveRows = (this.globalDocument && normalizedDocCategory) ? rows.filter((row) => this.getRowCategoryValue(row) === normalizedDocCategory) : rows; console.log("DocViewer.fetchRowsFromServerLogic: response", { controlId: this.controlId, scope: this.getActiveScopeConfig().mode, payloadDebugSearch: payload.debugSearch || null, rawRowCount: rows.length, rawCategorySummary: summarizeRowsByCategory(rows), effectiveRowCount: effectiveRows.length, effectiveCategorySummary: summarizeRowsByCategory(effectiveRows), docCategory: normalizedDocCategory, filterUsed: String(payload.filterUsed || "") }); if (!effectiveRows.length) { console.warn("DocViewer.fetchRowsFromServerLogic: no rows after filtering", { controlId: this.controlId, scope: this.getActiveScopeConfig().mode, globalDocument: this.globalDocument, docCategory: normalizedDocCategory, payloadDebugSearch: payload.debugSearch || null }); } return { rows: effectiveRows, count: effectiveRows.length, filterUsed: String(payload.filterUsed || "") }; } async loadPage(pageNumber) { this.setBusy(true); this.setGridRefreshing(true); this.setError(""); this.lastGridLoadOutcome.attempted = true; this.lastGridLoadOutcome.lastAttemptUtc = new Date().toISOString(); try { const response = await this.fetchRowsFromServerLogic(); const rows = response.rows; this.allRows = rows; this.sortAllRowsInPlace(); this.totalCount = Number(response.count) || rows.length; this.lastGridLoadOutcome.passed = true; this.lastGridLoadOutcome.lastSuccessUtc = new Date().toISOString(); this.lastGridLoadOutcome.rowCount = this.totalCount; this.lastGridLoadOutcome.errorMessage = ""; console.log("DocViewer.loadPage: server logic response", { count: this.totalCount, filterUsed: response.filterUsed, showAllDocuments: this.showAllDocuments, docCategory: this.docCategory }); const totalPages = Math.max(1, Math.ceil(this.totalCount / this.pageSize)); this.currentPage = Math.min(Math.max(1, pageNumber), totalPages); const pageRows = this.getCurrentPageRows(); await this.renderRows(pageRows); this.updatePaging(); } catch (err) { console.error(err); this.lastGridLoadOutcome.passed = false; this.lastGridLoadOutcome.errorMessage = String(err && err.message ? err.message : "Unable to load documents."); const userMessage = err.message || "Unable to load documents."; const diagnosticsText = this.buildErrorDiagnosticsText({ operation: "loadPage", userMessage, err, files: [], extra: { currentPage: this.currentPage, pageSize: this.pageSize } }); this.setError(userMessage, diagnosticsText); } finally { this.hideUploadProgressIfComplete(); this.setGridRefreshing(false); this.setBusy(false); } } getCurrentPageRows() { const start = (this.currentPage - 1) * this.pageSize; const end = start + this.pageSize; return this.allRows.slice(start, end); } delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } areUploadedFilesVisible(fileNames) { if (!Array.isArray(fileNames) || !fileNames.length) return true; const existingNames = new Set(this.allRows.map((row) => String(row?.cr5ad_filename || "").trim())); const normalizedNames = fileNames.map((name) => String(name || "").trim()); const missingNames = normalizedNames.filter((name) => !existingNames.has(name)); return missingNames.length === 0; } async refreshAfterUpload(fileNames, { maxAttempts = 5, retryDelayMs = 1200 } = {}) { for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { this.resetPagingState(); await this.loadPage(1); if (this.areUploadedFilesVisible(fileNames)) { return; } if (attempt < maxAttempts) { await this.delay(retryDelayMs); } } const visibilityError = new Error("Uploaded files were not visible after refresh."); visibilityError.uploadStage = "postUploadVisibilityCheck"; visibilityError.postUploadVisibility = { maxAttempts, retryDelayMs, fileNames: Array.isArray(fileNames) ? fileNames : [], lastGridLoadOutcome: this.lastGridLoadOutcome }; throw visibilityError; } buildUploadFailureSupportMessage(files) { const safeFiles = Array.isArray(files) ? files.map((file) => String(file?.name || "").trim()).filter(Boolean) : []; let uploadSubject = "the selected document"; if (safeFiles.length === 1) { uploadSubject = 'file "' + safeFiles[0] + '"'; } else if (safeFiles.length > 1) { uploadSubject = safeFiles.length + ' files'; } return 'Upload failed. Please try again. If you contact support, copy the diagnostics below and send this message: "DocMan upload failed for ' + uploadSubject + '."'; } normalizeUploadFailureForDetection(err) { const normalized = err || {}; return { name: String(normalized.name || ""), message: String(normalized.message || ""), uploadStage: String(normalized.uploadStage || ""), status: normalized.status ?? null, statusText: String(normalized.statusText || ""), url: String(normalized.url || "") }; } detectKnownUploadFailure(err) { const normalized = this.normalizeUploadFailureForDetection(err); const lowerMessage = normalized.message.toLowerCase(); const lowerUrl = normalized.url.toLowerCase(); const isBlobUrl = lowerUrl.includes("blob.core.windows.net"); const isSasUploadNetworkFailure = normalized.uploadStage === "docUpload" && lowerMessage.includes("network error") && (isBlobUrl || lowerMessage.includes("sas")); const hasEmbeddedJsonMarkersInSasUrl = /%22,%22bloburl%22/i.test(normalized.url) || /%22,%22expireson%22/i.test(normalized.url) || /%22,%22permissions%22/i.test(normalized.url) || /%7d$/i.test(normalized.url); if (!isSasUploadNetworkFailure && !hasEmbeddedJsonMarkersInSasUrl) { return null; } return { detected: true, reasonCode: hasEmbeddedJsonMarkersInSasUrl ? "sas_url_contains_embedded_json" : "sas_upload_network_or_cors_error", supportMessage: hasEmbeddedJsonMarkersInSasUrl ? "Detected malformed SAS UploadURL (embedded JSON markers in URL)." : "Detected SAS upload network/CORS-style failure while uploading to Azure Blob.", recommendFallback: true, signals: { uploadStage: normalized.uploadStage, isBlobUrl, hasEmbeddedJsonMarkersInSasUrl } }; } buildKnownUploadFailureSupportMessage(files, knownFailure) { const safeFiles = Array.isArray(files) ? files.map((file) => String(file?.name || "").trim()).filter(Boolean) : []; const fileSummary = safeFiles.length === 1 ? `file "${safeFiles[0]}"` : `${safeFiles.length || "selected"} file(s)`; const reasonText = String(knownFailure?.supportMessage || "Known upload failure detected.").trim(); return `Upload failed for ${fileSummary}. ${reasonText} Please retry once. If this keeps happening, use the alternate upload route and include diagnostics for support.`; } notifyKnownUploadFailure(knownFailure, err, files) { const detail = { classification: knownFailure, error: this.normalizeUploadFailureForDetection(err), files: (Array.isArray(files) ? files : []).map((file) => ({ name: String(file?.name || "").trim(), size: Number(file?.size || 0), type: String(file?.type || "") })), capturedAtUtc: new Date().toISOString(), controlId: this.controlId }; this.lastKnownUploadFailure = detail; console.error("DocViewer: known upload failure signature detected.", detail); window.dispatchEvent(new CustomEvent("docman:uploadFailureDetected", { detail })); if (typeof window.onDocManUploadFailureDetected === "function") { try { window.onDocManUploadFailureDetected(detail); } catch (callbackError) { console.error("DocViewer: onDocManUploadFailureDetected callback failed.", callbackError); } } } serializeDiagnosticsValue(value, maxLength = 3000) { if (value === undefined || value === null) { return ""; } let text = ""; if (typeof value === "string") { text = value; } else { try { text = JSON.stringify(value, null, 2); } catch (serializeError) { text = String(value); } } if (text.length > maxLength) { return `${text.slice(0, maxLength)} ...[truncated]`; } return text; } buildErrorDiagnosticsText({ operation, userMessage, err, files, extra }) { const normalizedError = err || {}; const responseTextSnippet = this.serializeDiagnosticsValue(normalizedError.responseText, 1400); const stackSnippet = this.serializeDiagnosticsValue(normalizedError.stack, 1400); const uploadStageByFile = Array.isArray(extra && extra.uploadStageByFile) ? extra.uploadStageByFile : []; const latestFileStage = uploadStageByFile.length ? uploadStageByFile[uploadStageByFile.length - 1] : null; const uploadStages = normalizedError && normalizedError.uploadStages && typeof normalizedError.uploadStages === "object" ? normalizedError.uploadStages : (latestFileStage && latestFileStage.uploadStages && typeof latestFileStage.uploadStages === "object" ? latestFileStage.uploadStages : null); const stageChecks = { gridLoaded: this.lastGridLoadOutcome ? Boolean(this.lastGridLoadOutcome.passed) : false, sasTokenReceived: uploadStages ? Boolean(uploadStages.sasTokenReceived) : null, docUploadCompleted: uploadStages ? Boolean(uploadStages.docUploadCompleted) : null, metadataAdded: uploadStages ? Boolean(uploadStages.metadataAdded) : null }; const toStageText = (value) => { if (value === true) return "PASS"; if (value === false) return "FAIL"; return "UNKNOWN"; }; const stageSummary = [ `grid loaded: ${toStageText(stageChecks.gridLoaded)}`, `SAS token received: ${toStageText(stageChecks.sasTokenReceived)}`, `Doc upload completed: ${toStageText(stageChecks.docUploadCompleted)}`, `Meta data added: ${toStageText(stageChecks.metadataAdded)}` ]; const payload = { type: "DocViewerError", generatedAtUtc: new Date().toISOString(), docManVersion: DocManVer, operation: String(operation || "unknown"), stageChecks: stageChecks, stageSummary: stageSummary, userMessage: String(userMessage || "").trim(), control: { controlId: this.controlId, pageUrl: (window.location && window.location.href) ? String(window.location.href) : "", globalDocument: this.globalDocument, showAllDocuments: this.showAllDocuments, docCategory: String(this.docCategory || ""), accountGuid: this.normalizeGuid(this.accountGuid), productGuid: this.normalizeGuid(this.productGuid) }, files: (Array.isArray(files) ? files : []).map((file) => ({ name: String(file?.name || "").trim(), size: Number(file?.size || 0), type: String(file?.type || "") })), error: { name: String(normalizedError.name || ""), message: String(normalizedError.message || ""), status: normalizedError.status ?? null, statusText: String(normalizedError.statusText || ""), textStatus: String(normalizedError.textStatus || ""), errorThrown: String(normalizedError.errorThrown || ""), uploadStage: String(normalizedError.uploadStage || ""), url: String(normalizedError.url || ""), responseTextSnippet, stackSnippet }, extra: { ...(extra || {}), lastGridLoadOutcome: this.lastGridLoadOutcome, postUploadVisibility: normalizedError.postUploadVisibility || null } }; return this.serializeDiagnosticsValue(payload, 8000); } setCopyDiagnosticsStatus(message) { if (!this.copyDiagnosticsStatus) { return; } if (this.copyDiagnosticsStatusTimerId) { window.clearTimeout(this.copyDiagnosticsStatusTimerId); this.copyDiagnosticsStatusTimerId = null; } this.copyDiagnosticsStatus.textContent = String(message || ""); if (!message) { return; } this.copyDiagnosticsStatusTimerId = window.setTimeout(() => { if (this.copyDiagnosticsStatus) { this.copyDiagnosticsStatus.textContent = ""; } this.copyDiagnosticsStatusTimerId = null; }, 2500); } async copyErrorDiagnosticsToClipboard() { const diagnosticsText = String(this.lastErrorDiagnosticsText || "").trim(); if (!diagnosticsText) { this.setCopyDiagnosticsStatus("No diagnostics available."); return; } try { if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") { await navigator.clipboard.writeText(diagnosticsText); } else if (this.errorDiagnosticsTextarea && typeof document.execCommand === "function") { this.errorDiagnosticsTextarea.focus(); this.errorDiagnosticsTextarea.select(); const copied = document.execCommand("copy"); if (!copied) { throw new Error("execCommand copy returned false"); } } else { throw new Error("Clipboard API unavailable"); } this.setCopyDiagnosticsStatus("Diagnostics copied."); } catch (copyError) { console.warn("DocViewer.copyErrorDiagnosticsToClipboard: copy failed", copyError); this.setCopyDiagnosticsStatus("Copy failed. Select the text and copy manually."); } } getNoRowsColspan() { return this.getShouldShowCategoryColumn() ? 5 : 4; } getShouldShowCategoryColumn() { return this.showCategoryColumn || this.shouldRenderHighlightBadges(); } getRowCategoryValue(row) { return String(row?.gtdv1_documentcategory ?? "").trim(); } async ensureCategoryDetailsLoaded(rows = this.allRows) { const categoryIds = Array.from(new Set( (Array.isArray(rows) ? rows : []) .map((row) => this.getRowCategoryValue(row)) .filter(Boolean) )); if (!categoryIds.length) { return; } await Promise.all(categoryIds.map((categoryId) => this.getDocCategoryDetails(categoryId))); } async getRowCategoryText(row) { const categoryValue = this.getRowCategoryValue(row); const formattedValue = String(row?.["gtdv1_documentcategory@OData.Community.Display.V1.FormattedValue"] || "").trim(); if (formattedValue && categoryValue) { this.docCategoryDetailsById.set(categoryValue, { docCategory: categoryValue, categoryName: formattedValue }); } if (formattedValue) { return formattedValue; } await this.getDocCategoryDetails(categoryValue); return this.getFilteredModeButtonText(categoryValue, { withPrefix: false }); } async renderRows(rows) { if (!this.tbody) return; this.tbody.innerHTML = ""; const visibleActionButtons = this.getVisibleActionButtons(); const showCategoryColumn = this.getShouldShowCategoryColumn(); if (!rows.length) { this.tbody.innerHTML = `No documents found.`; return; } const formatter = new Intl.DateTimeFormat(undefined, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); const categoryTexts = showCategoryColumn ? await Promise.all(rows.map((row) => this.getRowCategoryText(row))) : []; rows.forEach((row, index) => { const created = row.createdon ? formatter.format(new Date(row.createdon)) : ""; const rowGuid = this.getRowGuid(row); const isDocCategoryMatch = this.isDocCategoryMatch(row); const categoryText = categoryTexts[index] || "Unknown category"; const highlightBadge = (this.shouldRenderHighlightBadges() && isDocCategoryMatch) ? `` : ""; const actionButtonsMarkup = visibleActionButtons.length ? visibleActionButtons.map((button) => (` `)).join(" ") : `No actions`; const tr = document.createElement("tr"); tr.dataset.rowGuid = rowGuid; tr.innerHTML = ` ${this.escape(rowGuid)} ${this.escape(row.cr5ad_filename)}${highlightBadge} ${created} ${showCategoryColumn ? `${this.escape(categoryText)}` : ""}
${actionButtonsMarkup}
`; this.tbody.appendChild(tr); }); } getRowGuid(row) { return String(row?.[this.rowGuidField] ?? ""); } isDocCategoryMatch(row) { if (!this.docCategory) return false; return String(row?.gtdv1_documentcategory ?? "") === String(this.docCategory); } renderErrorRow() { if (!this.tbody) return; this.tbody.innerHTML = `There was a problem loading documents.`; } updatePaging() { const totalPages = Math.max(1, Math.ceil(this.totalCount / this.pageSize)); if (this.pageInfo) { this.pageInfo.textContent = `Page ${this.currentPage} of ${totalPages}`; } if (this.statusBox) { const start = this.totalCount === 0 ? 0 : (this.currentPage - 1) * this.pageSize + 1; const end = Math.min(this.totalCount, this.currentPage * this.pageSize); this.statusBox.textContent = `${start}-${end} of ${this.totalCount} documents`; } if (this.prevBtn) this.prevBtn.disabled = this.currentPage <= 1; if (this.nextBtn) this.nextBtn.disabled = this.currentPage >= totalPages; } async handleUpload() { if (!this.fileInput || !this.fileInput.files?.length) return; const selectedFiles = Array.from(this.fileInput.files || []); await this.handleSelectedFiles(selectedFiles); } async handleSelectedFiles(selectedFiles) { if (!Array.isArray(selectedFiles) || !selectedFiles.length) return; const filesToUpload = this.allowMultipleFiles ? selectedFiles : selectedFiles.slice(0, 1); const metadataValidation = this.getUploadMetadataValidation(); if (!metadataValidation.isValid) { console.error("DocViewer.handleUpload: upload blocked due to missing required metadata GUIDs.", { controlId: this.controlId, globalDocument: this.globalDocument, productGuidPresent: Boolean(metadataValidation.productGuid), accountGuidPresent: Boolean(metadataValidation.accountGuid) }); this.setError(`Upload failed: ${metadataValidation.missingMessage}`); return; } if (this.maxFileSizeBytes) { const oversizedFiles = filesToUpload.filter((file) => Number(file?.size || 0) > this.maxFileSizeBytes); if (oversizedFiles.length) { const limitText = this.formatBytes(this.maxFileSizeBytes); const fileNames = oversizedFiles.map((file) => `'${String(file?.name || "Unnamed file")}'`).join(", "); const message = `Upload failed: ${fileNames} exceed the maximum file size of ${limitText} per file.`; this.setError(message); this.showSizeLimitModal(message, "Upload blocked: file size limit exceeded"); return; } } if (this.allowedFileExtensions.length) { const invalidFiles = filesToUpload.filter((file) => !this.isAllowedFileType(file?.name)); if (invalidFiles.length) { const allowedText = this.allowedFileExtensions.join(", "); const fileNames = invalidFiles.map((file) => `'${String(file?.name || "Unnamed file")}'`).join(", "); const message = `Upload failed: ${fileNames} are not allowed. Allowed file types: ${allowedText}.`; this.setError(message); this.showSizeLimitModal(message, "Upload blocked: file type not allowed"); return; } } if (this.requiresUploadCategoryPicker()) { try { this.setError(""); await this.showCategoryPicker(filesToUpload); } catch (err) { console.error(err); this.setError(err.message || "Unable to prepare upload categories."); } return; } const normalizedDocCategory = this.normalizeDocCategory(this.docCategory, { asNumber: true }); if (normalizedDocCategory === null) { this.setError("Upload failed: invalid document category."); return; } await this.uploadFilesWithCategories( filesToUpload, filesToUpload.map(() => normalizedDocCategory) ); } async uploadFilesWithCategories(filesToUpload, docCategories) { const safeFilesToUpload = Array.isArray(filesToUpload) ? filesToUpload : []; const safeDocCategories = Array.isArray(docCategories) ? docCategories : []; const uploadedFileNames = safeFilesToUpload.map((file) => String(file?.name || "").trim()).filter(Boolean); const metadataValidation = this.getUploadMetadataValidation(); if (!metadataValidation.isValid) { console.error("DocViewer.uploadFilesWithCategories: upload blocked due to missing required metadata GUIDs.", { controlId: this.controlId, globalDocument: this.globalDocument, productGuidPresent: Boolean(metadataValidation.productGuid), accountGuidPresent: Boolean(metadataValidation.accountGuid), fileCount: safeFilesToUpload.length }); this.setError(`Upload failed: ${metadataValidation.missingMessage}`); return; } if (!safeFilesToUpload.length || safeFilesToUpload.length !== safeDocCategories.length) { this.setError("Upload failed: selected files and categories do not match."); return; } if (safeDocCategories.some((docCategory) => !Number.isInteger(docCategory))) { this.setError("Upload failed: invalid document category selection."); return; } this.setBusy(true); this.setError(""); const totalFiles = safeFilesToUpload.length; const uploadStartedAt = Date.now(); const completedDurations = []; const uploadStageByFile = []; let completedCount = 0; try { const sender = window._sendFilesWithCategory; if (typeof sender !== "function") { throw new Error("_sendFilesWithCategory is not available on window."); } this.beginUploadProgress(totalFiles); // The sender handles one file per call, so upload selected files sequentially. for (let index = 0; index < safeFilesToUpload.length; index += 1) { const file = safeFilesToUpload[index]; const docCategory = safeDocCategories[index]; const fileStartAt = Date.now(); this.updateUploadProgress({ completedCount, totalFiles, currentFileName: file?.name, completedDurations, currentFileElapsedMs: 0, totalElapsedMs: Date.now() - uploadStartedAt }); const intervalId = window.setInterval(() => { this.updateUploadProgress({ completedCount, totalFiles, currentFileName: file?.name, completedDurations, currentFileElapsedMs: Date.now() - fileStartAt, totalElapsedMs: Date.now() - uploadStartedAt }); }, 1000); try { const senderResult = await sender(docCategory, file, metadataValidation.productGuid, metadataValidation.accountGuid, { globalDocument: this.globalDocument, accountGuid: metadataValidation.accountGuid, productGuid: metadataValidation.productGuid }); uploadStageByFile.push({ fileName: String(file?.name || "").trim(), docCategory: docCategory, uploadStages: senderResult && senderResult.uploadStages ? senderResult.uploadStages : null }); } finally { window.clearInterval(intervalId); } completedDurations.push(Date.now() - fileStartAt); completedCount += 1; this.updateUploadProgress({ completedCount, totalFiles, currentFileName: file?.name, completedDurations, currentFileElapsedMs: 0, totalElapsedMs: Date.now() - uploadStartedAt }); } this.fileInput.value = ""; this.hideCategoryPicker(); if (!this.hideGridCard) { await this.refreshAfterUpload(uploadedFileNames); } this.completeUploadProgress(totalFiles, Date.now() - uploadStartedAt); } catch (err) { console.error(err); this.failUploadProgress(); const knownFailure = this.detectKnownUploadFailure(err); if (knownFailure && knownFailure.detected) { this.notifyKnownUploadFailure(knownFailure, err, safeFilesToUpload); } const userMessage = knownFailure && knownFailure.detected ? this.buildKnownUploadFailureSupportMessage(safeFilesToUpload, knownFailure) : this.buildUploadFailureSupportMessage(safeFilesToUpload); const diagnosticsText = this.buildErrorDiagnosticsText({ operation: "uploadFilesWithCategories", userMessage, err, files: safeFilesToUpload, extra: { totalFiles, completedCount, elapsedMs: Date.now() - uploadStartedAt, docCategories: safeDocCategories, uploadStageByFile: uploadStageByFile, knownUploadFailure: knownFailure } }); this.setError(userMessage, diagnosticsText); this.showSizeLimitModal(userMessage, "Upload failed"); } finally { this.setBusy(false); } } beginUploadProgress(totalFiles) { if (this.uploadProgressHideTimerId) { window.clearTimeout(this.uploadProgressHideTimerId); this.uploadProgressHideTimerId = null; } if (this.uploadProgressWrap) this.uploadProgressWrap.hidden = false; if (this.uploadProgressFill) { this.uploadProgressFill.style.width = "0%"; this.uploadProgressFill.textContent = "0%"; this.uploadProgressFill.classList.add("progress-bar-striped", "progress-bar-animated"); this.uploadProgressFill.classList.remove("bg-success", "bg-danger"); } if (this.uploadProgressText) { this.uploadProgressText.textContent = `Starting upload for ${totalFiles} file${totalFiles === 1 ? "" : "s"}...`; } if (this.uploadEta) { this.uploadEta.textContent = "Estimating time remaining..."; } if (this.uploadSpinner) { this.uploadSpinner.hidden = false; } } updateUploadProgress({ completedCount, totalFiles, currentFileName, completedDurations, currentFileElapsedMs, totalElapsedMs }) { if (!totalFiles || !this.uploadProgressFill) return; const safeCompletedCount = Math.min(Math.max(Number(completedCount) || 0, 0), totalFiles); const currentIndex = Math.min(safeCompletedCount + 1, totalFiles); const avgDurationMs = completedDurations.length ? completedDurations.reduce((sum, value) => sum + value, 0) / completedDurations.length : null; const estimatedCurrentDurationMs = avgDurationMs || null; const estimatedCurrentProgress = estimatedCurrentDurationMs ? Math.min((currentFileElapsedMs || 0) / estimatedCurrentDurationMs, 0.95) : 0; const progressRatio = safeCompletedCount >= totalFiles ? 1 : Math.min((safeCompletedCount + estimatedCurrentProgress) / totalFiles, 0.99); const progressPercent = Math.round(progressRatio * 100); this.uploadProgressFill.style.width = `${progressPercent}%`; this.uploadProgressFill.textContent = `${progressPercent}%`; if (this.uploadProgressBar) { this.uploadProgressBar.setAttribute("aria-valuenow", String(progressPercent)); } if (this.uploadProgressText) { const currentName = String(currentFileName || "").trim(); this.uploadProgressText.textContent = safeCompletedCount >= totalFiles ? `Upload complete (${totalFiles}/${totalFiles})` : `Uploading ${currentIndex}/${totalFiles}${currentName ? `: ${currentName}` : ""}`; } if (this.uploadEta) { if (safeCompletedCount >= totalFiles) { this.uploadEta.textContent = `Finished in ${this.formatDuration(totalElapsedMs)}.`; return; } if (!avgDurationMs) { this.uploadEta.textContent = `Estimating time remaining... (elapsed ${this.formatDuration(totalElapsedMs)})`; return; } // Estimate by average completed-file duration plus current file elapsed progress. const remainingFilesAfterCurrent = Math.max(totalFiles - safeCompletedCount - 1, 0); const currentRemainingMs = Math.max(avgDurationMs - (currentFileElapsedMs || 0), 0); const remainingMs = Math.max((remainingFilesAfterCurrent * avgDurationMs) + currentRemainingMs, 0); this.uploadEta.textContent = `Estimated time remaining: ${this.formatDuration(remainingMs)} (elapsed ${this.formatDuration(totalElapsedMs)})`; } } completeUploadProgress(totalFiles, totalElapsedMs) { if (this.uploadProgressFill) { this.uploadProgressFill.style.width = "100%"; this.uploadProgressFill.textContent = "100%"; this.uploadProgressFill.classList.remove("progress-bar-animated"); this.uploadProgressFill.classList.add("bg-success"); } if (this.uploadProgressBar) { this.uploadProgressBar.setAttribute("aria-valuenow", "100"); } if (this.uploadSpinner) { this.uploadSpinner.hidden = true; } if (this.uploadProgressText) { this.uploadProgressText.textContent = `Uploaded ${totalFiles} file${totalFiles === 1 ? "" : "s"}.`; } if (this.uploadEta) { this.uploadEta.textContent = `Finished in ${this.formatDuration(totalElapsedMs)}.`; } if (this.uploadProgressHideTimerId) { window.clearTimeout(this.uploadProgressHideTimerId); } this.uploadProgressHideTimerId = window.setTimeout(() => { this.hideUploadProgressIfComplete(); this.uploadProgressHideTimerId = null; }, 1500); const message = `Uploaded ${totalFiles} file${totalFiles === 1 ? "" : "s"} successfully in ${this.formatDuration(totalElapsedMs)}.`; this.showUploadSuccessModal(message, "Upload complete"); } failUploadProgress() { if (this.uploadProgressFill) { this.uploadProgressFill.classList.remove("progress-bar-animated", "bg-success"); this.uploadProgressFill.classList.add("bg-danger"); } if (this.uploadSpinner) { this.uploadSpinner.hidden = true; } if (this.uploadProgressText) { this.uploadProgressText.textContent = "Upload failed."; } if (this.uploadEta) { this.uploadEta.textContent = "Upload stopped due to an error."; } } formatDuration(durationMs) { const totalSeconds = Math.max(Math.ceil((Number(durationMs) || 0) / 1000), 0); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; if (minutes > 0) { return `${minutes}m ${seconds}s`; } return `${seconds}s`; } setBusy(isBusy) { this.isBusy = Boolean(isBusy); if (this.fileInput) this.fileInput.disabled = isBusy; if (this.uploadButton) this.uploadButton.disabled = isBusy; if (this.uploadDropzone) { this.uploadDropzone.classList.toggle("is-disabled", Boolean(isBusy)); this.uploadDropzone.setAttribute("aria-disabled", isBusy ? "true" : "false"); } if (this.prevBtn) this.prevBtn.disabled = isBusy; if (this.nextBtn) this.nextBtn.disabled = isBusy; if (this.refreshBtn) this.refreshBtn.disabled = isBusy; if (this.categoryPickerConfirmBtn) this.categoryPickerConfirmBtn.disabled = isBusy; if (this.categoryPickerCancelBtn) this.categoryPickerCancelBtn.disabled = isBusy; if (this.categoryPickerSection) { this.categoryPickerSection.querySelectorAll("select[data-role='category-picker-select']").forEach((element) => { element.disabled = isBusy; }); } if (!isBusy) { this.updatePaging(); } } setError(message, diagnosticsText = "") { if (!this.errorBox) return; const hasMessage = Boolean(message); const safeDiagnostics = String(diagnosticsText || "").trim(); this.lastErrorDiagnosticsText = safeDiagnostics; if (!message) { this.errorBox.hidden = true; this.errorBox.textContent = ""; if (this.errorDiagnosticsWrap) { this.errorDiagnosticsWrap.hidden = true; } if (this.errorDiagnosticsTextarea) { this.errorDiagnosticsTextarea.value = ""; } this.setCopyDiagnosticsStatus(""); } else { this.errorBox.hidden = false; this.errorBox.textContent = message; if (this.errorDiagnosticsWrap) { this.errorDiagnosticsWrap.hidden = !safeDiagnostics; } if (this.errorDiagnosticsTextarea) { this.errorDiagnosticsTextarea.value = safeDiagnostics; } if (!hasMessage || !safeDiagnostics) { this.setCopyDiagnosticsStatus(""); } } } escape(value) { return String(value ?? "").replace(/[&<>"]/g, (ch) => ({ "&": "&", "<": "<", ">": ">", "\"": """ })[ch]); } } function _DocMan_awaitFlowResponse(request, stageLabel, requestUrl) { if (!request || typeof request.then !== "function") { throw new Error((stageLabel || "Request") + " did not return a promise-like response."); } return Promise.resolve(request).catch((error) => { const normalizedError = new Error((stageLabel || "Request") + " failed."); normalizedError.status = error && typeof error.status === "number" ? error.status : null; normalizedError.statusText = error && error.statusText ? String(error.statusText) : ""; normalizedError.responseText = error && error.responseText ? String(error.responseText) : ""; normalizedError.url = requestUrl || ""; normalizedError.errorThrown = error && error.message ? String(error.message) : ""; throw normalizedError; }); } function _DocMan_parseMaybeJsonValue(value) { let current = value; for (let attempt = 0; attempt < 3; attempt += 1) { if (typeof current !== "string") { break; } const trimmed = current.trim(); if (!trimmed) { break; } try { current = JSON.parse(trimmed); } catch (parseError) { break; } } return current; } function _DocMan_extractWebUriFromFlowResponse(input) { const queue = [input]; const seen = new Set(); while (queue.length) { const raw = queue.shift(); const current = _DocMan_parseMaybeJsonValue(raw); if (!current) { continue; } if (typeof current === "string") { const trimmed = current.trim(); if (/^https?:\/\//i.test(trimmed)) { return trimmed; } continue; } if (Array.isArray(current)) { for (const item of current) { queue.push(item); } continue; } if (typeof current === "object") { if (seen.has(current)) { continue; } seen.add(current); const directWebUri = String(current.weburi || current.webUri || "").trim(); if (directWebUri) { return directWebUri; } const candidateKeys = ["file", "body", "Body", "data", "response", "result", "value"]; for (const key of candidateKeys) { if (current[key] !== undefined && current[key] !== null) { queue.push(current[key]); } } for (const key of Object.keys(current)) { const value = current[key]; if (value && (typeof value === "object" || typeof value === "string")) { queue.push(value); } } } } return ""; } function _DocMan_openDownloadUrl(url) { const link = document.createElement("a"); link.href = url; link.target = "_blank"; link.rel = "noopener noreferrer"; link.style.display = "none"; document.body.appendChild(link); link.click(); document.body.removeChild(link); } // Export shared handlers explicitly for page scripts that read from window. window.DocViewer = DocViewer; window._GenericWEBAPIHandler = _GenericWEBAPIHandler; window._GerAllFilesInTechFile = _GerAllFilesInTechFile; window._DocMan_getProductGroupGuidFromQueryString = _DocMan_getProductGroupGuidFromQueryString; window._DocMan_renderFilesForProductGroupFromQueryString = _DocMan_renderFilesForProductGroupFromQueryString; window._sendFilesWithCategory = _sendFilesWithCategory; window._DocMan_getDocCatDetails = _DocMan_getDocCatDetails; window._DocMan_getCategories = _DocMan_getCategories; window._DocMan_awaitFlowResponse = _DocMan_awaitFlowResponse; window._DocMan_parseMaybeJsonValue = _DocMan_parseMaybeJsonValue; window._DocMan_extractWebUriFromFlowResponse = _DocMan_extractWebUriFromFlowResponse; window._DocMan_openDownloadUrl = _DocMan_openDownloadUrl; window.DocMan = window.DocMan || {}; window.DocMan.getAllFilesInTechFile = _GerAllFilesInTechFile; window.DocMan.getProductGroupGuidFromQueryString = _DocMan_getProductGroupGuidFromQueryString; window.DocMan.renderFilesForProductGroupFromQueryString = _DocMan_renderFilesForProductGroupFromQueryString; window.DocMan.getDocCatDetails = _DocMan_getDocCatDetails; window.DocMan.getCategories = _DocMan_getCategories; window.DocMan.utils = Object.assign({}, window.DocMan.utils, { awaitFlowResponse: _DocMan_awaitFlowResponse, parseMaybeJsonValue: _DocMan_parseMaybeJsonValue, extractWebUriFromFlowResponse: _DocMan_extractWebUriFromFlowResponse, openDownloadUrl: _DocMan_openDownloadUrl }); window.DocMan.version = DocManVer; window._DocManVer = DocManVer;