Skip to content

Commit 12eb300

Browse files
authored
Fix theme download unifying the usage (exelearning#1219)
1 parent 063d724 commit 12eb300

8 files changed

Lines changed: 296 additions & 69 deletions

File tree

public/app/rest/apiCallManager.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -675,14 +675,12 @@ export default class ApiCallManager {
675675
/**
676676
* Get installed theme zip
677677
*
678-
* @param {*} odeSessionId
679-
* @param {*} $themeDirName
680-
* @returns
678+
* @param {string} themeId - The theme directory name
679+
* @returns {Promise<{zipFileName: string, zipBase64: string}>}
681680
*/
682-
async getThemeZip(odeSessionId, themeDirName) {
681+
async getThemeZip(themeId) {
683682
let url = this.endpoints.api_themes_download.path;
684-
url = url.replace('{odeSessionId}', odeSessionId);
685-
url = url.replace('{themeDirName}', themeDirName);
683+
url = url.replace('{themeId}', themeId);
686684
return await this.func.get(url);
687685
}
688686

public/app/rest/apiCallManager.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -268,13 +268,13 @@ describe('ApiCallManager', () => {
268268

269269
it('should replace params in theme zip download', async () => {
270270
apiManager.endpoints.api_themes_download = {
271-
path: 'http://localhost/themes/{odeSessionId}/{themeDirName}',
271+
path: 'http://localhost/api/themes/{themeId}/download',
272272
};
273273

274-
await apiManager.getThemeZip('session-1', 'theme-1');
274+
await apiManager.getThemeZip('theme-1');
275275

276276
expect(mockFunc.get).toHaveBeenCalledWith(
277-
'http://localhost/themes/session-1/theme-1'
277+
'http://localhost/api/themes/theme-1/download'
278278
);
279279
});
280280

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

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -939,19 +939,37 @@ export default class NavbarFile {
939939
return;
940940
}
941941

942-
// Server themes: use existing API
943-
eXeLearning.app.api
944-
.getThemeZip(eXeLearning.app.project.odeSession, theme.dirName)
945-
.then((response) => {
946-
if (response && response.zipFileName && response.zipBase64) {
947-
let link = document.createElement('a');
948-
link.setAttribute('type', 'hidden');
949-
link.href = 'data:text/plain;base64,' + response.zipBase64;
950-
link.download = response.zipFileName;
951-
link.click();
952-
link.remove();
953-
}
954-
});
942+
// Download from bundled theme ZIPs (works in both online and static mode)
943+
this.downloadThemeFromBundle(theme);
944+
}
945+
946+
/**
947+
* Download a theme from the bundled ZIP files
948+
* Works in both online and static mode since bundles are pre-built
949+
* @param {Object} theme - Theme object with dirName and name properties
950+
*/
951+
async downloadThemeFromBundle(theme) {
952+
try {
953+
const basePath = eXeLearning.config?.basePath || '';
954+
const bundleUrl = `${basePath}/bundles/themes/${theme.dirName}.zip`;
955+
956+
const response = await fetch(bundleUrl);
957+
if (!response.ok) {
958+
throw new Error(`Theme bundle not found: ${response.status}`);
959+
}
960+
961+
const blob = await response.blob();
962+
const url = URL.createObjectURL(blob);
963+
const link = document.createElement('a');
964+
link.href = url;
965+
link.download = `${theme.name || theme.dirName}.zip`;
966+
link.click();
967+
URL.revokeObjectURL(url);
968+
Logger.log(`[NavbarStyles] Theme '${theme.name}' downloaded from bundle`);
969+
} catch (error) {
970+
console.error('[NavbarStyles] Bundle download failed:', error);
971+
this.showElementAlert(_('Failed to download the style'), { error: error.message });
972+
}
955973
}
956974

957975
toggleSidenav() {

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

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ global.eXeLearning = {
2222
themeInfoFieldsConfig: {},
2323
themeEditionFieldsConfig: {},
2424
},
25+
endpoints: {
26+
api_themes_download: { path: '/api/themes/{themeId}/download', methods: ['GET'] },
27+
},
2528
postNewTheme: vi.fn(),
2629
putEditTheme: vi.fn(),
2730
deleteTheme: vi.fn(),
@@ -431,14 +434,26 @@ describe('NavbarStyles', () => {
431434
uploadToIndexedDBSpy.mockRestore();
432435
});
433436

434-
it('downloads theme zip when data is available (server theme)', async () => {
435-
eXeLearning.app.api.getThemeZip.mockResolvedValue({
436-
zipFileName: 'theme.zip',
437-
zipBase64: 'dGVzdA==',
438-
});
437+
it('downloads theme zip from bundle (server theme)', async () => {
438+
const mockBlob = new Blob(['test'], { type: 'application/zip' });
439+
const mockResponse = {
440+
ok: true,
441+
blob: vi.fn().mockResolvedValue(mockBlob),
442+
};
443+
global.fetch = vi.fn().mockResolvedValue(mockResponse);
444+
445+
const createObjectURLSpy = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:test');
446+
const revokeObjectURLSpy = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});
439447
const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
440-
await navbarStyles.downloadThemeZip({ dirName: 'base-1', downloadable: '1' });
448+
449+
navbarStyles.downloadThemeZip({ dirName: 'base-1', name: 'Base Theme', downloadable: '1' });
450+
await new Promise(resolve => setTimeout(resolve, 50));
451+
452+
expect(fetch).toHaveBeenCalledWith('/bundles/themes/base-1.zip');
441453
expect(clickSpy).toHaveBeenCalled();
454+
455+
createObjectURLSpy.mockRestore();
456+
revokeObjectURLSpy.mockRestore();
442457
clickSpy.mockRestore();
443458
});
444459

@@ -497,6 +512,30 @@ describe('NavbarStyles', () => {
497512
expect(alertSpy).toHaveBeenCalledWith(expect.stringContaining('not found'), expect.any(Object));
498513
});
499514

515+
it('shows error when bundle download fails', async () => {
516+
const mockResponse = {
517+
ok: false,
518+
status: 404,
519+
};
520+
global.fetch = vi.fn().mockResolvedValue(mockResponse);
521+
522+
const alertSpy = vi.spyOn(navbarStyles, 'showElementAlert');
523+
524+
navbarStyles.downloadThemeZip({
525+
dirName: 'nonexistent',
526+
name: 'Nonexistent Theme',
527+
downloadable: '1',
528+
});
529+
530+
// Wait for async operations
531+
await new Promise(resolve => setTimeout(resolve, 50));
532+
533+
expect(alertSpy).toHaveBeenCalledWith(
534+
expect.stringContaining('Failed to download'),
535+
expect.objectContaining({ error: expect.stringContaining('not found') })
536+
);
537+
});
538+
500539
describe('makeMenuThemeDownload', () => {
501540
it('shows enabled download button when downloadable is 1', () => {
502541
const theme = { downloadable: '1' };

public/app/workarea/modals/modals/pages/modalStyleManager.js

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,21 +1279,39 @@ export default class ModalStyleManager extends Modal {
12791279

12801280
/**
12811281
* Download/Export theme
1282+
* Downloads from bundled theme ZIPs (works in both online and static mode)
12821283
*
12831284
* @param {*} theme
12841285
*/
12851286
downloadThemeZip(theme) {
1286-
eXeLearning.app.api
1287-
.getThemeZip(eXeLearning.app.project.odeSession, theme.dirName)
1288-
.then((response) => {
1289-
if (response && response.zipFileName && response.zipBase64) {
1290-
let link = document.createElement('a');
1291-
link.setAttribute('type', 'hidden');
1292-
link.href = 'data:text/plain;base64,' + response.zipBase64;
1293-
link.download = response.zipFileName;
1294-
link.click();
1295-
link.remove();
1296-
}
1297-
});
1287+
this.downloadThemeFromBundle(theme);
1288+
}
1289+
1290+
/**
1291+
* Download a theme from the bundled ZIP files
1292+
* Works in both online and static mode since bundles are pre-built
1293+
* @param {Object} theme - Theme object with dirName and name properties
1294+
*/
1295+
async downloadThemeFromBundle(theme) {
1296+
try {
1297+
const basePath = eXeLearning.config?.basePath || '';
1298+
const bundleUrl = `${basePath}/bundles/themes/${theme.dirName}.zip`;
1299+
1300+
const response = await fetch(bundleUrl);
1301+
if (!response.ok) {
1302+
throw new Error(`Theme bundle not found: ${response.status}`);
1303+
}
1304+
1305+
const blob = await response.blob();
1306+
const url = URL.createObjectURL(blob);
1307+
const link = document.createElement('a');
1308+
link.href = url;
1309+
link.download = `${theme.name || theme.dirName}.zip`;
1310+
link.click();
1311+
URL.revokeObjectURL(url);
1312+
} catch (error) {
1313+
console.error('[ModalStyleManager] Bundle download failed:', error);
1314+
this.showElementAlert(_('Failed to download the style'), { error: error?.message });
1315+
}
12981316
}
12991317
}

public/app/workarea/modals/modals/pages/modalStyleManager.test.js

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ describe('ModalStyleManager', () => {
5959
},
6060
},
6161
},
62+
endpoints: {
63+
api_themes_download: { path: '/api/themes/{themeId}/download', methods: ['GET'] },
64+
},
6265
postUploadTheme: vi.fn().mockResolvedValue({ responseMessage: 'ERROR' }),
6366
postNewTheme: vi.fn(),
6467
putEditTheme: vi.fn(),
@@ -952,47 +955,53 @@ describe('ModalStyleManager', () => {
952955
});
953956

954957
describe('downloadThemeZip', () => {
955-
it('should download theme as zip file', async () => {
956-
const response = {
957-
zipFileName: 'theme.zip',
958-
zipBase64: 'base64data',
958+
it('should download theme as zip file from bundle', async () => {
959+
const mockBlob = new Blob(['test'], { type: 'application/zip' });
960+
const mockResponse = {
961+
ok: true,
962+
blob: vi.fn().mockResolvedValue(mockBlob),
959963
};
964+
global.fetch = vi.fn().mockResolvedValue(mockResponse);
960965

961-
window.eXeLearning.app.api.getThemeZip.mockResolvedValue(response);
966+
const createObjectURLSpy = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:test');
967+
const revokeObjectURLSpy = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});
968+
const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
962969

963-
const theme = { dirName: 'test-theme' };
970+
const theme = { dirName: 'test-theme', name: 'Test Theme' };
971+
modal.downloadThemeZip(theme);
964972

965-
const mockLink = {
966-
setAttribute: vi.fn(),
967-
click: vi.fn(),
968-
remove: vi.fn(),
969-
};
970-
vi.spyOn(document, 'createElement').mockReturnValue(mockLink);
973+
// Wait for async operations to complete
974+
await new Promise(resolve => setTimeout(resolve, 50));
971975

972-
await modal.downloadThemeZip(theme);
976+
expect(fetch).toHaveBeenCalledWith('/base/bundles/themes/test-theme.zip');
977+
expect(createObjectURLSpy).toHaveBeenCalled();
978+
expect(clickSpy).toHaveBeenCalled();
979+
expect(revokeObjectURLSpy).toHaveBeenCalled();
973980

974-
expect(window.eXeLearning.app.api.getThemeZip).toHaveBeenCalledWith(
975-
'test-session-123',
976-
'test-theme'
977-
);
978-
expect(mockLink.setAttribute).toHaveBeenCalledWith('type', 'hidden');
979-
expect(mockLink.download).toBe('theme.zip');
980-
expect(mockLink.click).toHaveBeenCalled();
981-
expect(mockLink.remove).toHaveBeenCalled();
981+
createObjectURLSpy.mockRestore();
982+
revokeObjectURLSpy.mockRestore();
983+
clickSpy.mockRestore();
982984
});
983985

984-
it('should not download if response is invalid', async () => {
985-
const response = {};
986-
987-
window.eXeLearning.app.api.getThemeZip.mockResolvedValue(response);
986+
it('should show error when bundle not found', async () => {
987+
const mockResponse = {
988+
ok: false,
989+
status: 404,
990+
};
991+
global.fetch = vi.fn().mockResolvedValue(mockResponse);
988992

989-
const theme = { dirName: 'test-theme' };
993+
const alertSpy = vi.spyOn(modal, 'showElementAlert');
990994

991-
const createElementSpy = vi.spyOn(document, 'createElement');
995+
const theme = { dirName: 'nonexistent', name: 'Nonexistent Theme' };
996+
modal.downloadThemeZip(theme);
992997

993-
await modal.downloadThemeZip(theme);
998+
// Wait for async operations to complete
999+
await new Promise(resolve => setTimeout(resolve, 50));
9941000

995-
expect(createElementSpy).not.toHaveBeenCalled();
1001+
expect(alertSpy).toHaveBeenCalledWith(
1002+
expect.stringContaining('Failed to download'),
1003+
expect.objectContaining({ error: expect.stringContaining('not found') })
1004+
);
9961005
});
9971006
});
9981007
});

0 commit comments

Comments
 (0)