diff --git a/CHANGELOG.md b/CHANGELOG.md index ab641e11..10c64acb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to - ✨(helm) redirecting system #1697 - 📱(frontend) add comments for smaller device #1737 +- ✨(project) add custom js support via config #1759 ### Changed diff --git a/docs/env.md b/docs/env.md index 58e9cd92..43c1bade 100644 --- a/docs/env.md +++ b/docs/env.md @@ -60,6 +60,7 @@ These are the environment variables you can set for the `impress-backend` contai | DJANGO_SERVER_TO_SERVER_API_TOKENS | | [] | | DOCUMENT_IMAGE_MAX_SIZE | Maximum size of document in bytes | 10485760 | | FRONTEND_CSS_URL | To add a external css file to the app | | +| FRONTEND_JS_URL | To add a external js file to the app | | | FRONTEND_HOMEPAGE_FEATURE_ENABLED | Frontend feature flag to display the homepage | false | | FRONTEND_THEME | Frontend theme to use | | | LANGUAGE_CODE | Default language | en-us | diff --git a/docs/theming.md b/docs/theming.md index 51f56a89..f67024df 100644 --- a/docs/theming.md +++ b/docs/theming.md @@ -8,7 +8,7 @@ To use this feature, simply set the `FRONTEND_CSS_URL` environment variable to t FRONTEND_CSS_URL=http://anything/custom-style.css ``` -Once you've set this variable, our application will load your custom CSS file and apply the styles to our frontend application. +Once you've set this variable, Docs will load your custom CSS file and apply the styles to our frontend application. ### Benefits @@ -32,6 +32,61 @@ Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom ---- +# Runtime JavaScript Injection 🚀 + +### How to Use + +To use this feature, simply set the `FRONTEND_JS_URL` environment variable to the URL of your custom JavaScript file. For example: + +```javascript +FRONTEND_JS_URL=http://anything/custom-script.js +``` + +Once you've set this variable, Docs will load your custom JavaScript file and execute it in the browser, allowing you to modify the application's behavior at runtime. + +### Benefits + +This feature provides several benefits, including: + +* **Dynamic customization** 🔄: With this feature, you can dynamically modify the behavior and appearance of our application without requiring any code changes. +* **Flexibility** 🌈: You can add custom functionality, modify existing features, or integrate third-party services. +* **Runtime injection** ⏱️: This feature allows you to inject JavaScript into the application at runtime, without requiring a restart or recompilation. + +### Example Use Case + +Let's say you want to add a custom menu to the application header. You can create a custom JavaScript file with the following contents: + +```javascript +(function() { + 'use strict'; + + function initCustomMenu() { + // Wait for the page to be fully loaded + const header = document.querySelector('header'); + if (!header) return false; + + // Create and inject your custom menu + const customMenu = document.createElement('div'); + customMenu.innerHTML = ''; + header.appendChild(customMenu); + + console.log('Custom menu added successfully'); + return true; + } + + // Initialize when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initCustomMenu); + } else { + initCustomMenu(); + } +})(); +``` + +Then, set the `FRONTEND_JS_URL` environment variable to the URL of your custom JavaScript file. Once you've done this, our application will load your custom JavaScript file and execute it, adding your custom menu to the header. + +---- + # **Your Docs icon** 📝 You can add your own Docs icon in the header from the theme customization file. diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index eccbf495..6f959009 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -2197,6 +2197,7 @@ class ConfigView(drf.views.APIView): "ENVIRONMENT", "FRONTEND_CSS_URL", "FRONTEND_HOMEPAGE_FEATURE_ENABLED", + "FRONTEND_JS_URL", "FRONTEND_THEME", "MEDIA_BASE_URL", "POSTHOG_KEY", diff --git a/src/backend/core/tests/test_api_config.py b/src/backend/core/tests/test_api_config.py index 0261125e..e29187b3 100644 --- a/src/backend/core/tests/test_api_config.py +++ b/src/backend/core/tests/test_api_config.py @@ -24,6 +24,7 @@ pytestmark = pytest.mark.django_db COLLABORATION_WS_NOT_CONNECTED_READY_ONLY=True, CRISP_WEBSITE_ID="123", FRONTEND_CSS_URL="http://testcss/", + FRONTEND_JS_URL="http://testjs/", FRONTEND_THEME="test-theme", MEDIA_BASE_URL="http://testserver/", POSTHOG_KEY={"id": "132456", "host": "https://eu.i.posthog-test.com"}, @@ -49,6 +50,7 @@ def test_api_config(is_authenticated): "ENVIRONMENT": "test", "FRONTEND_CSS_URL": "http://testcss/", "FRONTEND_HOMEPAGE_FEATURE_ENABLED": True, + "FRONTEND_JS_URL": "http://testjs/", "FRONTEND_THEME": "test-theme", "LANGUAGES": [ ["en-us", "English"], diff --git a/src/backend/impress/settings.py b/src/backend/impress/settings.py index 4b456ebb..5f74d7d5 100755 --- a/src/backend/impress/settings.py +++ b/src/backend/impress/settings.py @@ -509,6 +509,9 @@ class Base(Configuration): FRONTEND_CSS_URL = values.Value( None, environ_name="FRONTEND_CSS_URL", environ_prefix=None ) + FRONTEND_JS_URL = values.Value( + None, environ_name="FRONTEND_JS_URL", environ_prefix=None + ) THEME_CUSTOMIZATION_FILE_PATH = values.Value( os.path.join(BASE_DIR, "impress/configuration/theme/default.json"), diff --git a/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts index c5410c7c..44470fbb 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts @@ -126,6 +126,20 @@ test.describe('Config', () => { ).toBeAttached(); }); + test('it checks FRONTEND_JS_URL config', async ({ page }) => { + await overrideConfig(page, { + FRONTEND_JS_URL: 'http://localhost:123465/js/script.js', + }); + + await page.goto('/'); + + await expect( + page + .locator('script[src="http://localhost:123465/js/script.js"]') + .first(), + ).toBeAttached(); + }); + test('it checks theme_customization.translations config', async ({ page, }) => { @@ -145,10 +159,6 @@ test.describe('Config', () => { await expect(page.getByText('MyCustomDocs')).toBeAttached(); }); -}); - -test.describe('Config: Not logged', () => { - test.use({ storageState: { cookies: [], origins: [] } }); test('it checks the config api is called', async ({ page }) => { const responsePromise = page.waitForResponse( @@ -168,6 +178,10 @@ test.describe('Config: Not logged', () => { expect(configApi).toStrictEqual(CONFIG_LEFT); }); +}); + +test.describe('Config: Not logged', () => { + test.use({ storageState: { cookies: [], origins: [] } }); test('it checks that theme is configured from config endpoint', async ({ page, diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts index b4fda816..67adf44c 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts @@ -10,6 +10,7 @@ export const CONFIG = { COLLABORATION_WS_NOT_CONNECTED_READY_ONLY: true, ENVIRONMENT: 'development', FRONTEND_CSS_URL: null, + FRONTEND_JS_URL: null, FRONTEND_HOMEPAGE_FEATURE_ENABLED: true, FRONTEND_THEME: null, MEDIA_BASE_URL: 'http://localhost:8083', diff --git a/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx b/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx index 6694f6b9..f6e39993 100644 --- a/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx +++ b/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx @@ -1,5 +1,6 @@ import { Loader } from '@openfun/cunningham-react'; import Head from 'next/head'; +import Script from 'next/script'; import { PropsWithChildren, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; @@ -87,6 +88,9 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => { )} + {conf?.FRONTEND_JS_URL && ( +