diff --git a/test/core/workflow/workflow-acrobat/action-binder.test.js b/test/core/workflow/workflow-acrobat/action-binder.test.js index 430a1c4b7..1de88fae9 100644 --- a/test/core/workflow/workflow-acrobat/action-binder.test.js +++ b/test/core/workflow/workflow-acrobat/action-binder.test.js @@ -286,6 +286,22 @@ describe('ActionBinder', () => { it('should match image/tiff for pdf-to-png', () => { expect(actionBinder.isSameFileType('pdf-to-png', 'image/tiff')).to.be.true; }); + + it('should return false for quiz-maker with application/pdf', () => { + expect(actionBinder.isSameFileType('quiz-maker', 'application/pdf')).to.be.false; + }); + + it('should return false for quiz-maker with image/jpeg', () => { + expect(actionBinder.isSameFileType('quiz-maker', 'image/jpeg')).to.be.false; + }); + + it('should return false for flashcard-maker with application/pdf', () => { + expect(actionBinder.isSameFileType('flashcard-maker', 'application/pdf')).to.be.false; + }); + + it('should return false for flashcard-maker with image/jpeg', () => { + expect(actionBinder.isSameFileType('flashcard-maker', 'image/jpeg')).to.be.false; + }); }); describe('validateFiles', () => { @@ -988,6 +1004,36 @@ describe('ActionBinder', () => { expect(result).to.be.true; }); + it('should handle redirect for returning user for quiz-maker', async () => { + actionBinder.workflowCfg.enabledFeatures = ['quiz-maker']; + localStorage.setItem('unity.user', 'test-user'); + localStorage.setItem('quiz-maker_attempts', '2'); + + const cOpts = { payload: {} }; + const filesData = { test: 'data' }; + const result = await actionBinder.handleRedirect(cOpts, filesData); + + expect(cOpts.payload.newUser).to.be.false; + expect(cOpts.payload.attempts).to.equal('2+'); + expect(actionBinder.getRedirectUrl.calledWith(cOpts)).to.be.true; + expect(result).to.be.true; + }); + + it('should handle redirect for returning user for flashcard-maker', async () => { + actionBinder.workflowCfg.enabledFeatures = ['flashcard-maker']; + localStorage.setItem('unity.user', 'test-user'); + localStorage.setItem('flashcard-maker_attempts', '2'); + + const cOpts = { payload: {} }; + const filesData = { test: 'data' }; + const result = await actionBinder.handleRedirect(cOpts, filesData); + + expect(cOpts.payload.newUser).to.be.false; + expect(cOpts.payload.attempts).to.equal('2+'); + expect(actionBinder.getRedirectUrl.calledWith(cOpts)).to.be.true; + expect(result).to.be.true; + }); + it('should handle redirect with feedback for multi-file validation failure', async () => { actionBinder.multiFileValidationFailure = true; const cOpts = { payload: {} }; @@ -1211,6 +1257,52 @@ describe('ActionBinder', () => { expect(actionBinder.handleMultiFileUpload.calledWith(validFile)).to.be.true; expect(actionBinder.handleSingleFileUpload.called).to.be.false; }); + + it('should handle verbs that require multi-file upload for quiz-maker', async () => { + actionBinder.workflowCfg = { + name: 'workflow-acrobat', + enabledFeatures: ['quiz-maker'], + targetCfg: { verbsWithoutMfuToSfuFallback: ['compress-pdf', 'quiz-maker'] }, + }; + const files = [ + { name: 'test1.pdf', type: 'application/pdf', size: 1048576 }, + { name: 'test2.pdf', type: 'application/pdf', size: 2097152 }, + ]; + const validFile = [files[0]]; + actionBinder.validateFiles.resolves({ isValid: true, validFiles: validFile }); + + await actionBinder.handleFileUpload(files); + + expect(actionBinder.sanitizeFileName.calledTwice).to.be.true; + expect(actionBinder.filterFilesWithPdflite.called).to.be.true; + expect(actionBinder.validateFiles.called).to.be.true; + expect(actionBinder.initUploadHandler.called).to.be.true; + expect(actionBinder.handleMultiFileUpload.calledWith(validFile)).to.be.true; + expect(actionBinder.handleSingleFileUpload.called).to.be.false; + }); + + it('should handle verbs that require multi-file upload for flashcard-maker', async () => { + actionBinder.workflowCfg = { + name: 'workflow-acrobat', + enabledFeatures: ['flashcard-maker'], + targetCfg: { verbsWithoutMfuToSfuFallback: ['compress-pdf', 'flashcard-maker'] }, + }; + const files = [ + { name: 'test1.pdf', type: 'application/pdf', size: 1048576 }, + { name: 'test2.pdf', type: 'application/pdf', size: 2097152 }, + ]; + const validFile = [files[0]]; + actionBinder.validateFiles.resolves({ isValid: true, validFiles: validFile }); + + await actionBinder.handleFileUpload(files); + + expect(actionBinder.sanitizeFileName.calledTwice).to.be.true; + expect(actionBinder.filterFilesWithPdflite.called).to.be.true; + expect(actionBinder.validateFiles.called).to.be.true; + expect(actionBinder.initUploadHandler.called).to.be.true; + expect(actionBinder.handleMultiFileUpload.calledWith(validFile)).to.be.true; + expect(actionBinder.handleSingleFileUpload.called).to.be.false; + }); }); describe('continueInApp', () => { @@ -1406,6 +1498,64 @@ describe('ActionBinder', () => { spy.restore(); }); + it('should handle input change event with single file for quiz-maker', async () => { + const el = document.createElement('input'); + el.type = 'file'; + const addEventListenerSpy = sinon.spy(el, 'addEventListener'); + const block = { querySelector: sinon.stub().returns(el) }; + const actMap = { input: 'upload' }; + const extractSpy = sinon.spy(actionBinder, 'extractFiles'); + const spy = sinon.spy(actionBinder, 'acrobatActionMaps'); + + await actionBinder.initActionListeners(block, actMap); + + const handler = addEventListenerSpy.getCalls().find((call) => call.args[0] === 'change').args[1]; + actionBinder.signedOut = false; + actionBinder.tokenError = null; + actionBinder.workflowCfg.enabledFeatures = ['quiz-maker']; + + const file = new File(['test content'], 'test.pdf', { type: 'application/pdf' }); + const event = { target: { files: [file], value: '' } }; + + await handler(event); + + expect(extractSpy.called).to.be.true; + expect(spy.called).to.be.true; + expect(spy.firstCall.args).to.deep.equal(['upload', [file], file.size, 'change']); + + spy.restore(); + extractSpy.restore(); + }); + + it('should handle input change event with single file for flashcard-maker', async () => { + const el = document.createElement('input'); + el.type = 'file'; + const addEventListenerSpy = sinon.spy(el, 'addEventListener'); + const block = { querySelector: sinon.stub().returns(el) }; + const actMap = { input: 'upload' }; + const extractSpy = sinon.spy(actionBinder, 'extractFiles'); + const spy = sinon.spy(actionBinder, 'acrobatActionMaps'); + + await actionBinder.initActionListeners(block, actMap); + + const handler = addEventListenerSpy.getCalls().find((call) => call.args[0] === 'change').args[1]; + actionBinder.signedOut = false; + actionBinder.tokenError = null; + actionBinder.workflowCfg.enabledFeatures = ['flashcard-maker']; + + const file = new File(['test content'], 'test.pdf', { type: 'application/pdf' }); + const event = { target: { files: [file], value: '' } }; + + await handler(event); + + expect(extractSpy.called).to.be.true; + expect(spy.called).to.be.true; + expect(spy.firstCall.args).to.deep.equal(['upload', [file], file.size, 'change']); + + spy.restore(); + extractSpy.restore(); + }); + it('should handle input element not found', async () => { const block = { querySelector: sinon.stub().returns(null) }; const actMap = { 'nonexistent-input': 'upload' }; @@ -1612,6 +1762,46 @@ describe('ActionBinder', () => { { code: 'pre_upload_error_missing_verb_config' }, )).to.be.true; }); + + it('should not dispatch error when enabledFeatures[0] is quiz-maker', async () => { + actionBinder.dispatchErrorToast.resetHistory(); + actionBinder.processSingleFile = sinon.stub().resolves(); + actionBinder.processHybrid = sinon.stub().resolves(); + actionBinder.workflowCfg.enabledFeatures = ['quiz-maker']; + const validFiles = [ + { name: 'test.pdf', type: 'application/pdf', size: 1048576 }, + ]; + const totalFileSize = validFiles.reduce((sum, file) => sum + file.size, 0); + await actionBinder.acrobatActionMaps('upload', validFiles, totalFileSize, 'test-event'); + expect(actionBinder.dispatchErrorToast.neverCalledWith( + 'error_generic', + 500, + 'Invalid or missing verb configuration on Unity', + false, + true, + { code: 'pre_upload_error_missing_verb_config' }, + )).to.be.true; + }); + + it('should not dispatch error when enabledFeatures[0] is flashcard-maker', async () => { + actionBinder.dispatchErrorToast.resetHistory(); + actionBinder.processSingleFile = sinon.stub().resolves(); + actionBinder.processHybrid = sinon.stub().resolves(); + actionBinder.workflowCfg.enabledFeatures = ['flashcard-maker']; + const validFiles = [ + { name: 'test.pdf', type: 'application/pdf', size: 1048576 }, + ]; + const totalFileSize = validFiles.reduce((sum, file) => sum + file.size, 0); + await actionBinder.acrobatActionMaps('upload', validFiles, totalFileSize, 'test-event'); + expect(actionBinder.dispatchErrorToast.neverCalledWith( + 'error_generic', + 500, + 'Invalid or missing verb configuration on Unity', + false, + true, + { code: 'pre_upload_error_missing_verb_config' }, + )).to.be.true; + }); }); }); @@ -1776,6 +1966,46 @@ describe('ActionBinder', () => { localStorageStub.restore(); actionBinder.getRedirectUrl.restore(); }); + + it('should handle localStorage access error for quiz-maker', async () => { + actionBinder.workflowCfg.enabledFeatures = ['quiz-maker']; + const localStorageStub = sinon.stub(window.localStorage, 'getItem'); + localStorageStub.throws(new Error('localStorage not available')); + + const cOpts = { payload: {} }; + const filesData = { type: 'application/pdf', size: 123, count: 1 }; + sinon.stub(actionBinder, 'getRedirectUrl').resolves(); + actionBinder.redirectUrl = 'https://test-redirect.com'; + + const result = await actionBinder.handleRedirect(cOpts, filesData); + + expect(result).to.be.true; + expect(cOpts.payload.newUser).to.be.true; + expect(cOpts.payload.attempts).to.equal('1st'); + + localStorageStub.restore(); + actionBinder.getRedirectUrl.restore(); + }); + + it('should handle localStorage access error for flashcard-maker', async () => { + actionBinder.workflowCfg.enabledFeatures = ['flashcard-maker']; + const localStorageStub = sinon.stub(window.localStorage, 'getItem'); + localStorageStub.throws(new Error('localStorage not available')); + + const cOpts = { payload: {} }; + const filesData = { type: 'application/pdf', size: 123, count: 1 }; + sinon.stub(actionBinder, 'getRedirectUrl').resolves(); + actionBinder.redirectUrl = 'https://test-redirect.com'; + + const result = await actionBinder.handleRedirect(cOpts, filesData); + + expect(result).to.be.true; + expect(cOpts.payload.newUser).to.be.true; + expect(cOpts.payload.attempts).to.equal('1st'); + + localStorageStub.restore(); + actionBinder.getRedirectUrl.restore(); + }); }); describe('Experiment Data Integration', () => { diff --git a/test/utils/experiment-provider.test.js b/test/utils/experiment-provider.test.js index 0d74b7896..4dfd546b0 100644 --- a/test/utils/experiment-provider.test.js +++ b/test/utils/experiment-provider.test.js @@ -80,25 +80,22 @@ describe('getExperimentData', () => { expect(result).to.deep.equal(mockTargetData); }); - it('should reject when target returns empty decisions', async () => { - await testErrorScenario( - 'Target proposition returned but no valid data for scopes: acom_unity_acrobat_add-comment_us', - () => setupMock(createMockResult(null, [])), - ); + it('should resolve with null when target returns empty decisions', async () => { + setupMock(createMockResult(null, [])); + const result = await getExperimentData(['acom_unity_acrobat_add-comment_us']); + expect(result).to.equal(null); }); - it('should reject when target returns empty items', async () => { - await testErrorScenario( - 'Target proposition returned but no valid data for scopes: acom_unity_acrobat_add-comment_us', - () => setupMock({ decisions: [{ items: [] }] }), - ); + it('should resolve with null when target returns empty items', async () => { + setupMock({ decisions: [{ items: [] }] }); + const result = await getExperimentData(['acom_unity_acrobat_add-comment_us']); + expect(result).to.equal(null); }); - it('should reject when target returns invalid structure', async () => { - await testErrorScenario( - 'Target proposition returned but no valid data for scopes: acom_unity_acrobat_add-comment_us', - () => setupMock({ invalid: 'structure' }), - ); + it('should resolve with null when target returns invalid structure', async () => { + setupMock({ invalid: 'structure' }); + const result = await getExperimentData(['acom_unity_acrobat_add-comment_us']); + expect(result).to.equal(null); }); it('should reject when satellite track throws exception', async () => { @@ -108,18 +105,16 @@ describe('getExperimentData', () => { ); }); - it('should reject when target result is null', async () => { - await testErrorScenario( - 'Target proposition returned but no valid data for scopes: acom_unity_acrobat_add-comment_us', - () => setupMock(null), - ); + it('should resolve with null when target result is null', async () => { + setupMock(null); + const result = await getExperimentData(['acom_unity_acrobat_add-comment_us']); + expect(result).to.equal(null); }); - it('should reject when target result is undefined', async () => { - await testErrorScenario( - 'Target proposition returned but no valid data for scopes: acom_unity_acrobat_add-comment_us', - () => setupMock(undefined), - ); + it('should resolve with null when target result is undefined', async () => { + setupMock(undefined); + const result = await getExperimentData(['acom_unity_acrobat_add-comment_us']); + expect(result).to.equal(null); }); }); diff --git a/unitylibs/core/styles/splash-screen.css b/unitylibs/core/styles/splash-screen.css index 4826160db..5e2baab34 100644 --- a/unitylibs/core/styles/splash-screen.css +++ b/unitylibs/core/styles/splash-screen.css @@ -51,6 +51,15 @@ body > .splash-loader { display: flex; } +.splash-loader.dark .text .con-button { + border-color: white !important; + color: white !important; +} + +.splash-loader.dark .text .con-button:hover { + color: black !important; +} + .progress-holder { width: 100%; } @@ -129,6 +138,10 @@ body > .splash-loader { font-weight: 700; } +.splash-loader.dark .progress-holder .spectrum-ProgressBar--sideLabel .spectrum-ProgressBar-percentage { + color: white !important; +} + .splash-loader .text .icon-area img, .splash-loader .text video { width: 157px; @@ -148,6 +161,10 @@ body > .splash-loader { margin: 0 0 var(--spacing-l) 0; } +.splash-loader.dark .text-block [class^="heading"]:first-of-type { + color: white !important; +} + .splash-loader .text-block p.icon-area { margin-block: var(--spacing-s); } @@ -167,6 +184,10 @@ body > .splash-loader { font-weight: 700; } +.splash-loader.dark .text-block p { + color: white !important; +} + @media screen and (min-width: 600px) { .splash-loader h1, .splash-loader h2, diff --git a/unitylibs/core/styles/styles.css b/unitylibs/core/styles/styles.css index 9b7c3d4e6..03ebc4a95 100644 --- a/unitylibs/core/styles/styles.css +++ b/unitylibs/core/styles/styles.css @@ -174,7 +174,8 @@ } .unity-enabled .interactive-area .alert-holder, -.upload.unity-enabled .alert-holder { +.upload.unity-enabled .alert-holder, +.upload-marquee.unity-enabled .alert-holder { display: none; justify-content: center; align-items: center; @@ -188,12 +189,14 @@ } .unity-enabled .interactive-area .alert-holder.show, -.upload.unity-enabled .alert-holder.show { +.upload.unity-enabled .alert-holder.show, +.upload-marquee.unity-enabled .alert-holder.show { display: flex; } .unity-enabled .interactive-area .alert-holder .alert-toast, -.upload.unity-enabled .alert-holder .alert-toast { +.upload.unity-enabled .alert-holder .alert-toast, +.upload-marquee.unity-enabled .alert-holder .alert-toast { background: var(--alert-red); border-radius: 10px; width: 339px; @@ -206,14 +209,16 @@ } .unity-enabled .interactive-area .alert-holder .alert-toast .alert-content, -.upload.unity-enabled .alert-holder .alert-toast .alert-content { +.upload.unity-enabled .alert-holder .alert-toast .alert-content, +.upload-marquee.unity-enabled .alert-holder .alert-toast .alert-content { display: flex; flex-direction: row; align-items: center; } .unity-enabled .interactive-area .alert-holder .alert-toast .alert-content .alert-icon, -.upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-icon { +.upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-icon, +.upload-marquee.unity-enabled .alert-holder .alert-toast .alert-content .alert-icon { display: flex; width: auto; align-items: center; @@ -222,7 +227,8 @@ .unity-enabled .interactive-area .alert-holder .alert-toast .alert-content .alert-icon svg, .unity-enabled .interactive-area .alert-holder .alert-toast .alert-content .alert-icon img, -.upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-icon svg{ +.upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-icon svg, +.upload-marquee.unity-enabled .alert-holder .alert-toast .alert-content .alert-icon svg { display: flex; align-items: center; height: 20px; @@ -237,12 +243,14 @@ [dir="rtl"] .unity-enabled .interactive-area .alert-holder .alert-toast .alert-content .alert-icon svg, [dir="rtl"] .unity-enabled .interactive-area .alert-holder .alert-toast .alert-content .alert-icon img, -[dir="rtl"] .upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-icon svg { +[dir="rtl"] .upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-icon svg, +[dir="rtl"] .upload-marquee.unity-enabled .alert-holder .alert-toast .alert-content .alert-icon svg { padding: 18px 16px 18px 0; } .unity-enabled .interactive-area .alert-holder .alert-toast .alert-content .alert-text, -.upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-text { +.upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-text, +.upload-marquee.unity-enabled .alert-holder .alert-toast .alert-content .alert-text { width: 239px; min-height: 18px; padding-left: 12px; @@ -251,7 +259,8 @@ align-self: stretch; } -.upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-text { +.upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-text, +.upload-marquee.unity-enabled .alert-holder .alert-toast .alert-content .alert-text { align-content: center; } @@ -263,7 +272,8 @@ margin: 12px 0; } -.upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-text p { +.upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-text p, +.upload-marquee.unity-enabled .alert-holder .alert-toast .alert-content .alert-text p { color: var(--color-white); font-weight: 400; font-size: var(--type-body-xs-size); @@ -272,7 +282,8 @@ } .unity-enabled .interactive-area .alert-holder .alert-toast .alert-content .alert-close, -.upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-close{ +.upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-close, +.upload-marquee.unity-enabled .alert-holder .alert-toast .alert-content .alert-close { display: flex; justify-content: center; align-items: center; @@ -284,12 +295,14 @@ } [dir="rtl"] .unity-enabled .interactive-area .alert-holder .alert-toast .alert-content .alert-close, -.upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-close{ +.upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-close, +.upload-marquee.unity-enabled .alert-holder .alert-toast .alert-content .alert-close { padding: 12px 0 12px 8px; } .unity-enabled .interactive-area .alert-holder .alert-toast .alert-content .alert-close svg, -.upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-close svg { +.upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-close svg, +.upload-marquee.unity-enabled .alert-holder .alert-toast .alert-content .alert-close svg { display: flex; height: 12px; min-height: 12px; @@ -300,7 +313,8 @@ } .unity-enabled .interactive-area .alert-holder .alert-toast .alert-content .alert-close .alert-close-text, -.upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-close .alert-close-text{ +.upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-close .alert-close-text, +.upload-marquee.unity-enabled .alert-holder .alert-toast .alert-content .alert-close .alert-close-text { display: none; } @@ -344,11 +358,13 @@ } @media screen and (max-width: 600px) { - .upload.unity-enabled .alert-holder .alert-toast { + .upload.unity-enabled .alert-holder .alert-toast, + .upload-marquee.unity-enabled .alert-holder .alert-toast { width: 276px; } - .upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-text { + .upload.unity-enabled .alert-holder .alert-toast .alert-content .alert-text, + .upload-marquee.unity-enabled .alert-holder .alert-toast .alert-content .alert-text { width: 179px; } diff --git a/unitylibs/core/workflow/workflow-acrobat/action-binder.js b/unitylibs/core/workflow/workflow-acrobat/action-binder.js index d6284907c..fd08b69e6 100644 --- a/unitylibs/core/workflow/workflow-acrobat/action-binder.js +++ b/unitylibs/core/workflow/workflow-acrobat/action-binder.js @@ -75,6 +75,8 @@ export default class ActionBinder { 'summarize-pdf': ['single', 'allowed-filetypes-pdf-word-ppt-txt', 'page-limit-600', 'max-filesize-100-mb'], 'pdf-ai': ['hybrid', 'allowed-filetypes-pdf-word-ppt-txt', 'page-limit-600', 'max-numfiles-10', 'max-filesize-100-mb'], 'heic-to-pdf': ['hybrid', 'allowed-filetypes-all', 'allowed-filetypes-heic', 'max-filesize-100-mb'], + 'quiz-maker': ['hybrid', 'allowed-filetypes-study-spaces', 'page-limit-600', 'max-numfiles-100', 'max-filesize-100-mb'], + 'flashcard-maker': ['hybrid', 'allowed-filetypes-study-spaces', 'page-limit-600', 'max-numfiles-100', 'max-filesize-100-mb'], }; static ERROR_MAP = { @@ -374,7 +376,11 @@ export default class ActionBinder { for (const file of files) { let fail = false; - if (!this.limits.allowedFileTypes.includes(file.type)) { + const { getExtension } = await import('../../../utils/FileUtils.js'); + const typeCheckFail = this.workflowCfg.enabledFeatures[0] === 'heic-to-pdf' + ? getExtension(file.name).toLowerCase() !== 'heic' && !this.limits.allowedFileTypes.includes(file.type) + : !this.limits.allowedFileTypes.includes(file.type); + if (typeCheckFail) { let errorMessage = errorMessages.UNSUPPORTED_TYPE; if (this.isSameFileType(this.workflowCfg.enabledFeatures[0], file.type)) errorMessage = errorMessages.SAME_FILE_TYPE; if (this.MULTI_FILE) { @@ -662,7 +668,7 @@ export default class ActionBinder { if (this.signedOut === undefined) { if (this.tokenError) { const errorDetails = this.tokenError; - await this.dispatchErrorToast('pre_upload_error_fetching_access_token', null, `Could not fetch access token; Error: ${errorDetails.originalError}`, false, true, { + await this.dispatchErrorToast('pre_upload_error_fetching_access_token', null, `Could not fetch access token; Error: ${JSON.stringify(errorDetails.originalError)}`, false, true, { code: 'pre_upload_error_fetching_access_token', desc: errorDetails, }); diff --git a/unitylibs/core/workflow/workflow-acrobat/limits.json b/unitylibs/core/workflow/workflow-acrobat/limits.json index 02993afaf..4ac878d50 100644 --- a/unitylibs/core/workflow/workflow-acrobat/limits.json +++ b/unitylibs/core/workflow/workflow-acrobat/limits.json @@ -159,5 +159,22 @@ }, "allowed-filetypes-heic": { "allowedFileTypes": ["image/heic"] + }, + "allowed-filetypes-study-spaces": { + "allowedFileTypes": [ + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/msexcel", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/x-vnd.oasis.opendocument.spreadsheet", + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/rtf", + "text/rtf", + "text/plain", + "text/vtt" + ] } } diff --git a/unitylibs/core/workflow/workflow-acrobat/target-config.json b/unitylibs/core/workflow/workflow-acrobat/target-config.json index cde2af50b..f5ff46612 100644 --- a/unitylibs/core/workflow/workflow-acrobat/target-config.json +++ b/unitylibs/core/workflow/workflow-acrobat/target-config.json @@ -1,25 +1,12 @@ { - "verb-widget": { - "selector": ".verb-wrapper", + "_defaults": { "renderWidget": false, - "source": ".verb-wrapper .verb-container", - "target": ".verb-wrapper .verb-container", - "showSplashScreen": true, - "splashScreenConfig": { - "fragmentLink": "/dc-shared/fragments/shared-fragments/frictionless/splash-page/splashscreen", - "fragmentLink-acrobat": "/dc-shared/fragments/shared-fragments/frictionless/splash-page/splashscreen-acrobat", - "splashScreenParent": "body" - }, "domainMap": { "acrobat": [ "^(stage\\.)?acrobat\\.adobe\\.com$", "^.+--dc-frictionless--adobecom\\.aem\\.(page|live)$" ] }, - "actionMap": { - ".verb-wrapper": "upload", - "#file-upload": "upload" - }, "uploadLimits": { "HIGH_END": { "files": 10, "chunks": 10 }, "MID_RANGE": { "files": 5, "chunks": 10 }, @@ -28,8 +15,8 @@ "sendSplunkAnalytics": true, "verbsWithoutMfuToSfuFallback": ["compress-pdf"], "nonpdfMfuFeedbackScreenTypeNonpdf": ["combine-pdf"], - "nonpdfSfuProductScreen": ["word-to-pdf", "jpg-to-pdf", "ppt-to-pdf", "excel-to-pdf", "png-to-pdf", "createpdf", "chat-pdf", "chat-pdf-student", "summarize-pdf", "pdf-ai", "heic-to-pdf"], - "mfuUploadAllowed": ["combine-pdf", "rotate-pages", "chat-pdf", "chat-pdf-student", "summarize-pdf", "pdf-ai"], + "nonpdfSfuProductScreen": ["word-to-pdf", "jpg-to-pdf", "ppt-to-pdf", "excel-to-pdf", "png-to-pdf", "createpdf", "chat-pdf", "chat-pdf-student", "summarize-pdf", "pdf-ai", "heic-to-pdf", "quiz-maker", "flashcard-maker"], + "mfuUploadAllowed": ["combine-pdf", "rotate-pages", "chat-pdf", "chat-pdf-student", "summarize-pdf", "pdf-ai", "quiz-maker", "flashcard-maker"], "mfuUploadOnlyPdfAllowed": ["combine-pdf"], "experimentationOn": ["add-comment"], "fetchApiConfig": { @@ -55,5 +42,34 @@ } } } + }, + "verb-widget": { + "selector": ".verb-wrapper", + "source": ".verb-wrapper .verb-container", + "target": ".verb-wrapper .verb-container", + "showSplashScreen": true, + "splashScreenConfig": { + "fragmentLink": "/dc-shared/fragments/shared-fragments/frictionless/splash-page/splashscreen", + "fragmentLink-acrobat": "/dc-shared/fragments/shared-fragments/frictionless/splash-page/splashscreen-acrobat", + "splashScreenParent": "body" + }, + "actionMap": { + ".verb-wrapper": "upload", + "#file-upload": "upload" + } + }, + "study-marquee": { + "selector": ".foreground", + "source": ".foreground .study-marquee-container", + "target": ".foreground .study-marquee-container", + "showSplashScreen": true, + "splashScreenConfig": { + "fragmentLink": "/dc-shared/fragments/shared-fragments/frictionless/splash-page/students-splashscreen", + "splashScreenParent": "body" + }, + "actionMap": { + ".foreground": "upload", + "#file-upload": "upload" + } } } diff --git a/unitylibs/core/workflow/workflow-acrobat/upload-handler.js b/unitylibs/core/workflow/workflow-acrobat/upload-handler.js index a79a74d30..ba04bef4e 100644 --- a/unitylibs/core/workflow/workflow-acrobat/upload-handler.js +++ b/unitylibs/core/workflow/workflow-acrobat/upload-handler.js @@ -19,13 +19,22 @@ export default class UploadHandler { return feature === 'pdf-ai' ? 'chat-pdf-pdf-ai' : feature; } + async getEffectiveFileType(file) { + const { getExtension } = await import('../../../utils/FileUtils.js'); + const isHeicWithoutMimeType = this.actionBinder.workflowCfg.enabledFeatures[0] === 'heic-to-pdf' + && getExtension(file.name).toLowerCase() === 'heic' + && !file.type; + return isHeicWithoutMimeType ? 'image/heic' : file.type; + } + async createAsset(file, multifile = false, workflowId = null) { + const effectiveFileType = await this.getEffectiveFileType(file); const data = { surfaceId: unityConfig.surfaceId, targetProduct: this.actionBinder.workflowCfg.productName, name: file.name, size: file.size, - format: file.type, + format: effectiveFileType, ...(multifile && { multifile }), ...(workflowId && { workflowId }), }; @@ -346,6 +355,7 @@ export default class UploadHandler { } fileData.assetId = assetData.id; this.actionBinder.setAssetId(assetData.id); + const effectiveFileType = await this.getEffectiveFileType(file); cOpts = { assetId: assetData.id, targetProduct: this.actionBinder.workflowCfg.productName, @@ -357,7 +367,7 @@ export default class UploadHandler { [assetData.id]: { name: file.name, size: file.size, - type: file.type, + type: effectiveFileType, }, }, ...(!isPdf ? { feedback: 'nonpdf' } : {}), @@ -372,7 +382,7 @@ export default class UploadHandler { ({ failedFiles, attemptMap } = await this.chunkPdf( [assetData], [blobData], - [file.type], + [effectiveFileType], maxConcurrentChunks, abortSignal, )); diff --git a/unitylibs/core/workflow/workflow-upload/action-binder.js b/unitylibs/core/workflow/workflow-upload/action-binder.js index 0dcbc6e91..91c43016d 100644 --- a/unitylibs/core/workflow/workflow-upload/action-binder.js +++ b/unitylibs/core/workflow/workflow-upload/action-binder.js @@ -85,6 +85,7 @@ export default class ActionBinder { this.sendAnalyticsToSplunk = null; this.assetId = null; this.filesData = {}; + this.verb = this.getVerbFromDom(); } getApiConfig() { @@ -258,6 +259,13 @@ export default class ActionBinder { } } + getVerbFromDom() { + const verbEl = this.unityEl?.querySelector('[class*="icon-verb-"]'); + if (!verbEl) return undefined; + const verbClass = Array.from(verbEl.classList).find((cls) => cls.startsWith('icon-verb-')); + return verbClass?.slice('icon-verb-'.length); + } + async continueInApp(assetId, file) { const { getCgenQueryParams } = await import(`${getUnityLibs()}/utils/cgen-utils.js`); const queryParams = getCgenQueryParams(this.unityEl); @@ -269,6 +277,7 @@ export default class ActionBinder { }; if (this.workflowCfg.productName.toLowerCase() === 'firefly') { payload.action = 'asset-upload'; + if (this.verb) payload.verb = this.verb; } if (this.workflowCfg.productName.toLowerCase() === 'photoshop') { payload.referer = window.location.href; @@ -340,7 +349,7 @@ export default class ActionBinder { logAnalyticsinSplunk(eventName, data) { if (this.sendAnalyticsToSplunk) { - this.sendAnalyticsToSplunk(eventName, this.workflowCfg.productName, data, `${unityConfig.apiEndPoint}/log`); + this.sendAnalyticsToSplunk(eventName, this.workflowCfg.productName, { ...data, action: 'upload', verb: this.verb }, `${unityConfig.apiEndPoint}/log`); } } @@ -384,6 +393,7 @@ export default class ActionBinder { async loadTransitionScreen() { if (!this.transitionScreen) { try { + this.workflowCfg.theme = this.unityEl.classList.contains('dark') ? 'dark' : null; const { default: TransitionScreen } = await import(`${getUnityLibs()}/scripts/transition-screen.js`); this.transitionScreen = new TransitionScreen(this.splashScreenEl, this.initActionListeners, this.LOADER_LIMIT, this.workflowCfg, this.desktop); await this.transitionScreen.delayedSplashLoader(); @@ -406,6 +416,9 @@ export default class ActionBinder { case 'interrupt': await this.cancelUploadOperation(); break; + case 'redirect': + this.logAnalyticsinSplunk('Edit Photos CTA|UnityWidget', {}); + break; default: break; } @@ -423,8 +436,9 @@ export default class ActionBinder { const actions = { A: (el, key) => { el.addEventListener('click', async (e) => { - e.preventDefault(); - await this.executeActionMaps(actMap[key]); + const action = actMap[key]; + if (action !== 'redirect') e.preventDefault(); + await this.executeActionMaps(action); }); }, DIV: (el, key) => { diff --git a/unitylibs/core/workflow/workflow-upload/target-config.json b/unitylibs/core/workflow/workflow-upload/target-config.json index 49290d177..0648b0df9 100644 --- a/unitylibs/core/workflow/workflow-upload/target-config.json +++ b/unitylibs/core/workflow/workflow-upload/target-config.json @@ -1,5 +1,5 @@ { - "upload": { + "_defaults": { "selector": ".drop-zone", "renderWidget": false, "source": ".drop-zone", @@ -33,12 +33,16 @@ "fragmentLink-photoshop": "/cc-shared/fragments/products/photoshop/unity/splash-page/splashscreen", "fragmentLink-lightroom": "/cc-shared/fragments/products/photoshop-lightroom/unity/splash-page/splashscreen", "fragmentLink-firefly": "/cc-shared/fragments/products/firefly/unity/splash-page", + "fragmentLink-firefly-dark": "/cc-shared/fragments/products/firefly/unity/splash-page-dark", "splashScreenParent": "body" }, "actionMap": { ".drop-zone": "upload", - "#file-upload": "upload" + "#file-upload": "upload", + ".upload-marquee-cta": "redirect" }, "sendSplunkAnalytics": true - } + }, + "upload": {}, + "upload-marquee": {} } diff --git a/unitylibs/core/workflow/workflow.js b/unitylibs/core/workflow/workflow.js index d1ecfc67d..f58ed53be 100644 --- a/unitylibs/core/workflow/workflow.js +++ b/unitylibs/core/workflow/workflow.js @@ -103,7 +103,8 @@ class WfInitiator { async getTarget(rawTargetConfig) { const targetConfig = await rawTargetConfig.json(); const prevElem = this.el.previousElementSibling; - const supportedBlocks = Object.keys(targetConfig); + const supportedBlocks = Object.keys(targetConfig).filter((key) => !key.startsWith('_')); + const defaults = targetConfig._defaults || {}; let targetCfg = null; for (let k = 0; k < supportedBlocks.length; k += 1) { const classes = supportedBlocks[k].split('.'); @@ -118,7 +119,7 @@ class WfInitiator { } } if (hasAllClasses) { - targetCfg = targetConfig[supportedBlocks[k]]; + targetCfg = { ...defaults, ...targetConfig[supportedBlocks[k]] }; break; } } @@ -147,7 +148,7 @@ class WfInitiator { const newPic = asset.cloneNode(true); this.el.querySelector(':scope > div > div').prepend(newPic); } - if (!targetCfg.renderWidget && block.classList.contains('upload')) { + if (!targetCfg.renderWidget && (block.classList.contains('upload') || block.classList.contains('upload-marquee'))) { return block.querySelectorAll(selector); } if (!targetCfg.renderWidget) return null; @@ -208,6 +209,8 @@ class WfInitiator { 'summarize-pdf', 'pdf-ai', 'heic-to-pdf', + 'quiz-maker', + 'flashcard-maker', ]), }, 'workflow-ai': { @@ -241,7 +244,7 @@ class WfInitiator { getEnabledFeatures() { const { supportedFeatures, supportedTexts } = this.workflowCfg; - const verbWidget = this.el.closest('.section')?.querySelector('.verb-widget'); + const verbWidget = this.el.closest('.section')?.querySelector('.verb-widget, .study-marquee'); if (verbWidget) { const verb = [...verbWidget.classList].find((cn) => supportedFeatures.has(cn)); if (verb) this.workflowCfg.enabledFeatures.push(verb); diff --git a/unitylibs/scripts/transition-screen.js b/unitylibs/scripts/transition-screen.js index 36b54e1c2..8af621813 100644 --- a/unitylibs/scripts/transition-screen.js +++ b/unitylibs/scripts/transition-screen.js @@ -82,6 +82,9 @@ export default class TransitionScreen { } const productName = this.workflowCfg.productName.toLowerCase(); if (this.workflowCfg.name === 'workflow-upload') { + const { theme } = this.workflowCfg; + const themedKey = theme ? `fragmentLink-${productName}-${theme}` : null; + if (themedKey && splashScreenConfig[themedKey]) return splashScreenConfig[themedKey]; return splashScreenConfig[`fragmentLink-${productName}`]; } return splashScreenConfig.fragmentLink; @@ -107,6 +110,7 @@ export default class TransitionScreen { } const sections = doc.querySelectorAll('body > div'); const f = createTag('div', { class: 'fragment splash-loader decorate', style: 'display: none', tabindex: '-1', role: 'dialog', 'aria-modal': 'true' }); + if (this.workflowCfg.theme === 'dark') f.classList.add('dark'); f.append(...sections); const splashDiv = document.querySelector( this.workflowCfg.targetCfg.splashScreenConfig.splashScreenParent, diff --git a/unitylibs/scripts/utils.js b/unitylibs/scripts/utils.js index 5e995ce59..579813100 100644 --- a/unitylibs/scripts/utils.js +++ b/unitylibs/scripts/utils.js @@ -44,6 +44,13 @@ async function getRefreshToken() { const { tokenInfo } = window.adobeIMS ? await window.adobeIMS.refreshToken() : {}; return tokenInfo; } catch (e) { + const errorMsg = (e?.message || e?.exception?.message || '').trim(); + if (errorMsg === 'invalid_credentials') { + return { + token: null, + isGuestToken: true + }; + } return { token: null, error: e, diff --git a/unitylibs/utils/experiment-provider.js b/unitylibs/utils/experiment-provider.js index 3a60c0734..32d547013 100644 --- a/unitylibs/utils/experiment-provider.js +++ b/unitylibs/utils/experiment-provider.js @@ -32,10 +32,8 @@ export async function getExperimentData(decisionScopes) { const targetData = TargetPropositionResult?.decisions?.[0]?.items?.[0]?.data?.content; if (targetData) { window._satellite.track('propositionDisplay', TargetPropositionResult.propositions); - resolve(targetData); - } else { - reject(new Error(`Target proposition returned but no valid data for scopes: ${Array.isArray(decisionScopes) ? decisionScopes.join(', ') : decisionScopes}`)); } + resolve(targetData || null); }, }); } catch (e) {