Skip to content

Commit 3cb144f

Browse files
authored
1618 fix platform related issues (exelearning#1655)
* fix: fetch ELP from platform * feat: hide menu elements when ELPx is opened from the platform * feat: hide save options * test coverage
1 parent 87dfc56 commit 3cb144f

7 files changed

Lines changed: 179 additions & 15 deletions

File tree

public/app/workarea/menus/navbar/items/navbarFile.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,11 @@ export default class NavbarFile {
351351
if (capabilities && !capabilities.storage.remote) {
352352
return;
353353
}
354+
355+
// Hide templates if this is an LMS session
356+
if (eXeLearning?.config?.platformIntegration) {
357+
return;
358+
}
354359

355360
try {
356361
// Get current locale from eXeLearning config or default to 'en'

public/app/workarea/menus/navbar/items/navbarFile.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,25 @@ describe('NavbarFile', () => {
650650
expect(li.classList.contains('d-none')).toBe(true);
651651
});
652652

653+
it('checkAndShowNewFromTemplateButton should keep button hidden and return early when platform integration is enabled', async () => {
654+
const li = document.createElement('li');
655+
li.classList.add('d-none');
656+
li.appendChild(mockButtons.newFromTemplateButton);
657+
navbarElement.appendChild(li);
658+
659+
global.eXeLearning.config.platformIntegration = true;
660+
global.fetch = vi.fn();
661+
662+
await navbarFile.checkAndShowNewFromTemplateButton();
663+
664+
// Should not fetch and stay hidden
665+
expect(global.fetch).not.toHaveBeenCalled();
666+
expect(li.classList.contains('d-none')).toBe(true);
667+
668+
// Clean up config
669+
global.eXeLearning.config.platformIntegration = false;
670+
});
671+
653672
it('setSaveProjectEvent should add click listener', () => {
654673
navbarFile.setSaveProjectEvent();
655674
expect(mockButtons.saveButton.addEventListener).toHaveBeenCalledWith('click', expect.any(Function));

public/app/workarea/project/projectManager.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,76 @@ export default class projectManager {
426426
*/
427427
async checkAndImportElp() {
428428
const urlParams = new URLSearchParams(window.location.search);
429+
430+
// =====================================================
431+
// PLATFORM INTEGRATION: Fetch ELP from LMS if requested
432+
// =====================================================
433+
const fetchPlatformElp = urlParams.get('fetchPlatformElp') === '1';
434+
const jwtToken = urlParams.get('jwt_token');
435+
436+
if (jwtToken && fetchPlatformElp) {
437+
Logger.log('[ProjectManager] Detected platform integration new project, fetching ELP from platform...');
438+
try {
439+
if (this.app?.modals?.loader) {
440+
this.app.modals.loader.show({ message: window._ ? window._('Downloading from platform...') : 'Downloading from platform...' });
441+
}
442+
443+
const basePath = window.eXeLearning?.config?.basePath || '';
444+
const response = await fetch(`${basePath}/api/platform/integration/openPlatformElp`, {
445+
method: 'POST',
446+
headers: { 'Content-Type': 'application/json' },
447+
body: JSON.stringify({ jwt_token: jwtToken })
448+
});
449+
450+
if (response.ok) {
451+
const data = await response.json();
452+
453+
if (data.responseMessage === 'OK' && data.elpFile) {
454+
Logger.log('[ProjectManager] ELP file fetched from platform, base64 size:', data.elpFile.length);
455+
456+
// Convert base64 to Blob/File
457+
const binaryString = atob(data.elpFile);
458+
const len = binaryString.length;
459+
const bytes = new Uint8Array(len);
460+
for (let i = 0; i < len; i++) {
461+
bytes[i] = binaryString.charCodeAt(i);
462+
}
463+
const fileName = data.elpFileName || 'platform_import.elpx';
464+
const file = new File([bytes], fileName, { type: 'application/octet-stream' });
465+
466+
const stats = await this.importFromElpxViaYjs(file);
467+
Logger.log('[ProjectManager] Platform ELP import complete:', stats);
468+
469+
try {
470+
await this._yjsBridge.documentManager.saveToServer();
471+
Logger.log('[ProjectManager] Document saved to server after platform import');
472+
} catch (saveError) {
473+
console.warn('[ProjectManager] Failed to save to server after platform import', saveError);
474+
}
475+
476+
} else {
477+
Logger.log('[ProjectManager] No ELP file returned by platform or error:', data.error);
478+
}
479+
} else {
480+
console.warn(`[ProjectManager] Platform API responded with status ${response.status}`);
481+
}
482+
} catch (error) {
483+
// Do not block the user, just log and continue
484+
console.error('[ProjectManager] Failed to fetch ELP from platform:', error);
485+
} finally {
486+
if (this.app?.modals?.loader) {
487+
this.app.modals.loader.hide();
488+
}
489+
490+
// Cleanup fetchPlatformElp url parameter
491+
const newUrl = new URL(window.location.href);
492+
newUrl.searchParams.delete('fetchPlatformElp');
493+
window.history.replaceState({}, '', newUrl.toString());
494+
}
495+
496+
return; // We handled the platform import, skip subsequent checks
497+
}
498+
429499
const importPathFromUrl = urlParams.get('import');
430500
const importPathFromEmbedding = this.app?.runtimeConfig?.embeddingConfig?.initialProjectUrl || '';
431501
const importPath = importPathFromUrl || importPathFromEmbedding;

public/app/workarea/project/projectManager.test.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,75 @@ describe('ProjectManager', () => {
754754
});
755755
});
756756

757+
describe('checkAndImportElp', () => {
758+
let originalLocation;
759+
760+
beforeEach(() => {
761+
global.fetch = vi.fn();
762+
projectManager.importFromElpxViaYjs = vi.fn().mockResolvedValue('stats');
763+
projectManager._yjsBridge = {
764+
documentManager: {
765+
saveToServer: vi.fn().mockResolvedValue()
766+
}
767+
};
768+
mockApp.modals.loader = {
769+
show: vi.fn(),
770+
hide: vi.fn(),
771+
};
772+
vi.stubGlobal('history', { replaceState: vi.fn() });
773+
});
774+
775+
afterEach(() => {
776+
vi.unstubAllGlobals();
777+
});
778+
779+
it('does nothing if no fetchPlatformElp param', async () => {
780+
vi.stubGlobal('location', { href: 'http://localhost:8080/', search: '' });
781+
await projectManager.checkAndImportElp();
782+
expect(global.fetch).not.toHaveBeenCalledWith(expect.stringContaining('openPlatformElp'), expect.anything());
783+
});
784+
785+
it('fetches platform ELP when jwt_token and fetchPlatformElp are present', async () => {
786+
vi.stubGlobal('location', {
787+
href: 'http://localhost:8080/?jwt_token=12345&fetchPlatformElp=1',
788+
search: '?jwt_token=12345&fetchPlatformElp=1'
789+
});
790+
global.fetch.mockResolvedValue({
791+
ok: true,
792+
json: () => Promise.resolve({ responseMessage: 'OK', elpFile: btoa('test data'), elpFileName: 'test.elpx' })
793+
});
794+
795+
await projectManager.checkAndImportElp();
796+
797+
expect(global.fetch).toHaveBeenCalledWith(
798+
expect.stringContaining('/api/platform/integration/openPlatformElp'),
799+
expect.objectContaining({ method: 'POST' })
800+
);
801+
expect(projectManager.importFromElpxViaYjs).toHaveBeenCalled();
802+
expect(projectManager._yjsBridge.documentManager.saveToServer).toHaveBeenCalled();
803+
expect(mockApp.modals.loader.show).toHaveBeenCalled();
804+
});
805+
806+
it('handles fetch platform ELP failure gracefully', async () => {
807+
vi.stubGlobal('location', {
808+
href: 'http://localhost:8080/?jwt_token=12345&fetchPlatformElp=1',
809+
search: '?jwt_token=12345&fetchPlatformElp=1'
810+
});
811+
global.fetch.mockResolvedValue({
812+
ok: false,
813+
status: 500
814+
});
815+
816+
await projectManager.checkAndImportElp();
817+
818+
expect(global.fetch).toHaveBeenCalledWith(
819+
expect.stringContaining('/api/platform/integration/openPlatformElp'),
820+
expect.objectContaining({ method: 'POST' })
821+
);
822+
expect(projectManager.importFromElpxViaYjs).not.toHaveBeenCalled();
823+
});
824+
});
825+
757826
describe('resetProject', () => {
758827
it('sets _forceStructureImport flag', () => {
759828
projectManager.resetProject();

src/routes/pages.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,7 @@ export function createPagesRoutes(deps: PagesDependencies = defaultDependencies)
655655
const basePath = getBasePath();
656656
let redirectUrl = `${basePath}/workarea?project=${newSessionId}`;
657657
if (jwtTokenParam) {
658-
redirectUrl += `&jwt_token=${encodeURIComponent(jwtTokenParam)}`;
658+
redirectUrl += `&jwt_token=${encodeURIComponent(jwtTokenParam)}&fetchPlatformElp=1`;
659659
}
660660
return Response.redirect(redirectUrl, 302);
661661
} catch (error) {
@@ -764,12 +764,13 @@ export function createPagesRoutes(deps: PagesDependencies = defaultDependencies)
764764

765765
// If no project UUID, create a new project and redirect
766766
if (!projectUuid) {
767+
// Helper function to build redirect URL with preserved jwt_token
767768
// Helper function to build redirect URL with preserved jwt_token
768769
const buildRedirectUrl = (sessionId: string): string => {
769770
const basePath = getBasePath();
770771
let url = `${basePath}/workarea?project=${sessionId}`;
771772
if (jwtTokenParam) {
772-
url += `&jwt_token=${encodeURIComponent(jwtTokenParam)}`;
773+
url += `&jwt_token=${encodeURIComponent(jwtTokenParam)}&fetchPlatformElp=1`;
773774
}
774775
return url;
775776
};

views/workarea/menus/menuHeadTop.njk

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<span class="auto-icon" aria-hidden="true">download</span>
99
<span class="btn-label">{{ t.download or 'Download' }}</span>
1010
</button>
11-
<button id="head-top-save-button" class="btn button-display d-flex justify-content-center align-items-center me-2" title="{{ t.save or 'Save' }}" data-testid="save-button">
11+
<button id="head-top-save-button" class="btn button-display d-flex justify-content-center align-items-center me-2 {% if config.platformIntegration %}d-none{% endif %}" title="{{ t.save or 'Save' }}" data-testid="save-button">
1212
<div class="small-icon save-icon-white"></div>
1313
<span>{{ t.save or 'Save' }}</span>
1414
</button>
@@ -22,7 +22,7 @@
2222
<span class="medium-icon settings-icon"></span>
2323
</button>
2424
<button id="head-top-share-button"
25-
class="btn button-secondary button-share-link d-flex justify-content-center align-items-center exe-online ms-1"
25+
class="btn button-secondary button-share-link d-flex justify-content-center align-items-center exe-online ms-1 {% if config.platformIntegration %}d-none{% endif %}"
2626
title="{{ t.share or 'Share' }}"
2727
aria-label="{{ t.share or 'Share' }}">
2828
<span class="share-visibility-indicator d-flex align-items-center">
@@ -56,9 +56,9 @@
5656
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="exeUserMenuToggler">
5757
{# Menú móvil plano: visible solo en móviles (d-md-none) #}
5858
<li class="d-md-none mobile-menu-header"><small class="dropdown-header">{{ t.file or 'File' }}</small></li>
59-
<li class="d-md-none"><a class="dropdown-item" id="mobile-navbar-button-new" href="#">{{ t.new or 'New' }}</a></li>
60-
<li class="d-md-none exe-online"><a class="dropdown-item" id="mobile-navbar-button-openuserodefiles" href="#">{{ t.open or 'Open' }}</a></li>
61-
<li class="d-md-none exe-online"><a class="dropdown-item" id="mobile-navbar-button-save" href="#">{{ t.save or 'Save' }}</a></li>
59+
<li class="d-md-none {% if config.platformIntegration %}d-none{% endif %}"><a class="dropdown-item" id="mobile-navbar-button-new" href="#">{{ t.new or 'New' }}</a></li>
60+
<li class="d-md-none exe-online {% if config.platformIntegration %}d-none{% endif %}"><a class="dropdown-item" id="mobile-navbar-button-openuserodefiles" href="#">{{ t.open or 'Open' }}</a></li>
61+
<li class="d-md-none exe-online {% if config.platformIntegration %}d-none{% endif %}"><a class="dropdown-item" id="mobile-navbar-button-save" href="#">{{ t.save or 'Save' }}</a></li>
6262
{# Hidden to reduce Open/Import confusion (issue #1223) #}
6363
<li class="d-none"><a class="dropdown-item" id="mobile-navbar-button-import-elp" href="#">{{ t.import_elpx or 'Import (.elpx…)' }}</a></li>
6464
<li class="dropdown-divider d-md-none"></li>
@@ -68,7 +68,7 @@
6868
<li class="d-md-none"><a class="dropdown-item" id="mobile-navbar-button-export-epub3" href="#">ePub3</a></li>
6969
<li class="dropdown-divider d-md-none"></li>
7070
<li class="d-md-none"><a class="dropdown-item" id="mobile-navbar-button-settings" href="#">{{ t.project_properties or 'Project Properties' }}</a></li>
71-
<li class="d-md-none exe-online"><a class="dropdown-item" id="mobile-navbar-button-share" href="#">{{ t.share or 'Share' }}</a></li>
71+
<li class="d-md-none exe-online {% if config.platformIntegration %}d-none{% endif %}"><a class="dropdown-item" id="mobile-navbar-button-share" href="#">{{ t.share or 'Share' }}</a></li>
7272
<li class="dropdown-divider d-md-none"></li>
7373
<li class="d-md-none mobile-menu-header"><small class="dropdown-header">{{ t.utilities or 'Utilities' }}</small></li>
7474
<li class="d-md-none exe-advanced"><a class="dropdown-item" id="mobile-navbar-button-filemanager" href="#">{{ t.file_manager or 'File manager' }}</a></li>

views/workarea/menus/menuNavbar.njk

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
{{ t.file or 'File' }}
1313
</a>
1414
<ul class="dropdown-menu" aria-labelledby="dropdownFile">
15-
<li><a class="dropdown-item" id="navbar-button-new" href="{{ basePath }}/workarea" data-shortcut="Mod+Alt+N" aria-keyshortcuts="Control+Alt+N Meta+Alt+N"><span class="small-icon new-icon-green"></span> {{ t.new or 'New' }}</a></li>
15+
<li class="{% if config.platformIntegration %}d-none{% endif %}"><a class="dropdown-item" id="navbar-button-new" href="{{ basePath }}/workarea" data-shortcut="Mod+Alt+N" aria-keyshortcuts="Control+Alt+N Meta+Alt+N"><span class="small-icon new-icon-green"></span> {{ t.new or 'New' }}</a></li>
1616
<li class="d-none"><a class="dropdown-item" id="navbar-button-new-from-template" href="#"><span class="small-icon new-icon"></span> {{ t.new_from_template or 'New from Template...' }}</a></li>
17-
<li class="exe-online"><a class="dropdown-item" id="navbar-button-openuserodefiles" href="#" data-shortcut="Mod+O" aria-keyshortcuts="Control+O Meta+O"><span class="small-icon open-icon"></span> {{ t.open or 'Open' }}</a></li>
18-
<li class="dropdown dropend exe-online">
17+
<li class="exe-online {% if config.platformIntegration %}d-none{% endif %}"><a class="dropdown-item" id="navbar-button-openuserodefiles" href="#" data-shortcut="Mod+O" aria-keyshortcuts="Control+O Meta+O"><span class="small-icon open-icon"></span> {{ t.open or 'Open' }}</a></li>
18+
<li class="dropdown dropend exe-online {% if config.platformIntegration %}d-none{% endif %}">
1919
<a class="dropdown-item dropdown-toggle" href="#" id="navbar-button-dropdown-recent-projects" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
2020
<span class="small-icon recent-icon-green"></span> {{ t.recent_projects or 'Recent projects' }}
2121
</a>
@@ -27,8 +27,8 @@
2727
{# Hidden to reduce Open/Import confusion (issue #1223) #}
2828
<li class="d-none"><a id="navbar-button-import-elp" class="dropdown-item" href="#"><span class="small-icon import-icon-green"></span> {{ t.import_elpx or 'Import (.elpx…)' }}</a></li>
2929
{% endif %}
30-
<li class="dropdown-divider exe-online"></li>
31-
<li class="exe-online"><a id="navbar-button-save" class="dropdown-item" href="#" data-shortcut="Mod+S" aria-keyshortcuts="Control+S Meta+S"><span class="small-icon save-icon-green"></span> {{ t.save or 'Save' }}</a></li>
30+
<li class="dropdown-divider exe-online {% if config.platformIntegration %}d-none{% endif %}"></li>
31+
<li class="exe-online {% if config.platformIntegration %}d-none{% endif %}"><a id="navbar-button-save" class="dropdown-item" href="#" data-shortcut="Mod+S" aria-keyshortcuts="Control+S Meta+S"><span class="small-icon save-icon-green"></span> {{ t.save or 'Save' }}</a></li>
3232
<li class="dropdown dropend exe-online">
3333
<a class="dropdown-item dropdown-toggle" href="#" id="dropdownExportAs" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><span class="small-icon download-icon-green"></span> {{ t.download_as or 'Download as...' }}</a>
3434
<ul class="dropdown-menu" aria-labelledby="dropdownExportAs">
@@ -41,8 +41,8 @@
4141
<li><a id="navbar-button-export-epub3" class="dropdown-item" href="#">ePub3</a></li>
4242
</ul>
4343
</li>
44-
<li class="dropdown-divider"></li>
45-
<li class="exe-online"><a class="dropdown-item" id="navbar-button-share" href="#" data-shortcut="Mod+Alt+S" aria-keyshortcuts="Control+Alt+S Meta+Alt+S"><span class="small-icon share-icon"></span> {{ t.share or 'Share' }}</a></li>
44+
<li class="dropdown-divider {% if config.platformIntegration %}d-none{% endif %}"></li>
45+
<li class="exe-online {% if config.platformIntegration %}d-none{% endif %}"><a class="dropdown-item" id="navbar-button-share" href="#" data-shortcut="Mod+Alt+S" aria-keyshortcuts="Control+Alt+S Meta+Alt+S"><span class="small-icon share-icon"></span> {{ t.share or 'Share' }}</a></li>
4646
{% if config.isOfflineInstallation %}
4747
<li><a class="dropdown-item" id="navbar-button-open-offline" href="#" data-shortcut="Mod+O" aria-keyshortcuts="Control+O Meta+O"><span class="small-icon open-icon"></span> {{ t.open or 'Open' }}</a></li>
4848
{# Hidden to reduce Open/Import confusion (issue #1223) #}

0 commit comments

Comments
 (0)