Compare commits

..

18 Commits

Author SHA1 Message Date
Anthony LC 64ce2d449a 📱(frontend) add touch detection
We rely on the size of the screen to determine
if we are on a touch device, but this is not
always accurate, for example on laptops
when we resize the window to a smaller size,
it could be misinterpreted as a touch device.
To improve this, we add detection for touch input
so we can more accurately determine if the user
is using touch or mouse input.
2026-01-07 12:03:14 +01:00
Anthony LC 2be5cda88e 📱(frontend) toolbar to the bottom when mobile
On mobile devices, the mobile toolbar is often
positioned above the Blocknote toolbar, making
it hard to access.
This commit adjusts the toolbar position to
the bottom of the screen for better accessibility
when using mobile devices.
2026-01-07 12:02:16 +01:00
Anthony LC 1211e04a45 ️(frontend) improve visual focus on some elements
Improve visual focus indication on
MainLayout content and PdfBlock editor.
2026-01-07 11:13:40 +01:00
Anthony LC ea3a4a6da3 (project) add custom js support via config
From the config, we can add custom JS file URL
to be included in the frontend.
2026-01-05 15:06:53 +01:00
Anthony LC b78ad27a71 🐛(frontend) fix children not display when first resize
When we resize the window for the first time, then
open the panel, the children were not displayed.
This fix this issue.
2026-01-05 13:21:54 +01:00
Anthony LC e4b8ffb304 ✈️(frontend) pause Posthog when offline
Posthog keeps trying to send events when the user
is offline, causing the network request queue to fill up
and slowing down the app. This commit pauses Posthog
when the user is offline and resumes it when back online.
2026-01-05 12:07:47 +01:00
Anthony LC 78c7ab247b 🦺(frontend) check content type pdf on PdfBlock
Pdfblock was quite permissive on the content type
it was accepting. Now it checks that the content
type is exactly 'application/pdf' before rendering
the PDF viewer.
2026-01-05 11:47:55 +01:00
Anthony LC b0bd6e2c01 🥅(frontend) intercept 401 error on GET threads
We intercept 401 errors on GET /threads to avoid
spamming Sentry with authentication errors
when users are not logged in.
2026-01-05 11:23:43 +01:00
Anthony LC 37527416f2 🩹(frontend) small ui improvement
- center initial loader before app load
- add name on input to remove warning
- fix hover on interlinking link
2026-01-05 11:02:30 +01:00
Anthony LC 30bc959340 ⬆️(dependency) fix CVE by bumping qs dependency
Fix CVE by bumping qs from 6.14.0 to 6.14.1
2026-01-05 10:20:17 +01:00
Anthony LC a73d9c1c78 📱(frontend) add comments for smaller device
Add comments support for mobile devices by
removing the desktop-only restriction and
ensuring the UI adapts well to smaller screens.
2026-01-05 10:04:37 +01:00
Anthony LC a920daf05b ⬆️(dependencies) bump to blocknote 0.45.0
Bump to BlockNote 0.45.0 to get the latest
features and fixes.
This release includes the fix for the table
deletion that breaks the editor when
deleting tables.
2025-12-22 10:53:12 +01:00
Anthony LC ff88465398 ⬇️(dependencies) downgrade next from 16.0.10 to 15.5.9
Passing the Next 16 will need more work to be compatible
with our application. We will do this upgrade later
in a dedicated PR.
We add it in the renovate.json to avoid having Renovate
trying to upgrade it again.
2025-12-22 10:52:03 +01:00
renovate[bot] 3617e4f7b8 ⬆️(dependencies) update js dependencies 2025-12-22 10:52:02 +01:00
Anthony LC efaec45bfd (helm) create ingress-redirects template
Create a new Helm template for ingress redirects
and update the values.yaml file accordingly.
We will be able to manage ingress redirects
through Helm charts easily.
2025-12-22 10:11:54 +01:00
Anthony LC 715d88ba3c ♻️(frontend) replace auth redirect logic for home
To be intercepted by ingress redirects, we need
to redirect using window.location instead of
using Next.js router. The Next.js router does not
trigger a full page reload, so the ingress
redirect logic is not executed.
2025-12-22 10:09:13 +01:00
Anthony LC 7d64d79eeb 🐛(helm) fix OIDC authentication with standard scopes
"usual_name" does not seem to be standard,
it gives error during login.
We replace "usual_name" by "family_name".
2025-12-22 09:24:44 +01:00
Anthony LC 2e66b87dab 🔧(helm) add OIDC_REDIRECT_ALLOWED_HOSTS to fix authentication flow
Add OIDC_REDIRECT_ALLOWED_HOSTS setting to dev and
feature environments to properly allow Keycloak
redirect callbacks after authentication.
2025-12-22 09:24:44 +01:00
51 changed files with 1482 additions and 1524 deletions
+19
View File
@@ -6,6 +6,25 @@ and this project adheres to
## [Unreleased]
### Added
- ✨(helm) redirecting system #1697
- 📱(frontend) add comments for smaller device #1737
- ✨(project) add custom js support via config #1759
### Changed
- 🥅(frontend) intercept 401 error on GET threads #1754
- 🦺(frontend) check content type pdf on PdfBlock #1756
- ✈️(frontend) pause Posthog when offline #1755
- 📱(frontend) toolbar to the bottom when mobile #1774
### Fixed
- 🐛(frontend) fix tables deletion #1752
- 🐛(frontend) fix children not display when first resize #1753
## [4.2.0] - 2025-12-17
### Added
+1
View File
@@ -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 |
+56 -1
View File
@@ -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 = '<button>Custom Menu</button>';
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.
+3
View File
@@ -30,8 +30,11 @@
"groupName": "ignored js dependencies",
"matchManagers": ["npm"],
"matchPackageNames": [
"@next/eslint-plugin-next",
"docx",
"eslint-config-next",
"fetch-mock",
"next",
"node",
"node-fetch",
"workbox-webpack-plugin"
+1
View File
@@ -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",
@@ -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"],
+3
View File
@@ -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"),
@@ -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,
@@ -153,7 +153,7 @@ test.describe('Doc Comments', () => {
await thread.getByRole('menuitem', { name: 'Edit comment' }).click();
const commentEditor = thread.getByText('This is a comment').first();
await commentEditor.fill('This is an edited comment');
const saveBtn = thread.getByRole('button', { name: 'Save' });
const saveBtn = thread.locator('button[data-test="save"]').first();
await saveBtn.click();
await expect(saveBtn).toBeHidden();
await expect(
@@ -163,7 +163,8 @@ test.describe('Doc Comments', () => {
// Add second comment
await thread.getByRole('paragraph').last().fill('This is a second comment');
await thread.getByRole('button', { name: 'Save' }).click();
await saveBtn.click();
await expect(saveBtn).toBeHidden();
await expect(
thread.getByText('This is an edited comment').first(),
).toBeVisible();
@@ -371,7 +372,7 @@ test.describe('Doc Comments', () => {
test.describe('Doc Comments mobile', () => {
test.use({ viewport: { width: 500, height: 1200 } });
test('Comments are not visible on mobile', async ({ page, browserName }) => {
test('Can comments on mobile', async ({ page, browserName }) => {
const [title] = await createDoc(
page,
'comment-mobile',
@@ -387,7 +388,16 @@ test.describe('Doc Comments mobile', () => {
// Checks add react reaction
const editor = await writeInEditor({ page, text: 'Hello' });
await editor.getByText('Hello').selectText();
await expect(page.getByRole('button', { name: 'Comment' })).toBeHidden();
await expect(page.getByRole('button', { name: 'Paragraph' })).toBeVisible();
await page.getByRole('button', { name: 'Comment' }).click();
const thread = page.locator('.bn-thread');
await thread.getByRole('paragraph').first().fill('This is a comment');
await thread.locator('[data-test="save"]').click();
await expect(thread.getByText('This is a comment').first()).toBeHidden();
await editor.first().click();
await editor.getByText('Hello').click();
await expect(thread.getByText('This is a comment').first()).toBeVisible();
});
});
@@ -880,14 +880,15 @@ test.describe('Doc Editor', () => {
// Wait for the interlink to be created and rendered
const editor = await getEditor({ page });
const interlinkChild2 = editor.getByRole('button', {
name: docChild2,
});
const interlinkChild = editor
.locator('.--docs--interlinking-link-inline-content')
.first();
await expect(interlinkChild2).toBeVisible({ timeout: 10000 });
await expect(interlinkChild2).toContainText('😀');
await expect(interlinkChild2.locator('svg').first()).toBeHidden();
await interlinkChild2.click();
await expect(interlinkChild).toBeVisible({ timeout: 10000 });
await expect(interlinkChild).toContainText('😀');
await expect(interlinkChild).toContainText(docChild2);
await expect(interlinkChild.locator('svg').first()).toBeHidden();
await interlinkChild.click();
await verifyDocName(page, docChild2);
@@ -897,11 +898,9 @@ test.describe('Doc Editor', () => {
await input.fill(docChild1);
await searchContainer.getByText(docChild1).click();
const interlinkChild1 = editor.getByRole('button', {
name: docChild1,
});
await expect(interlinkChild1).toBeVisible({ timeout: 10000 });
await expect(interlinkChild1.locator('svg').first()).toBeVisible();
await expect(interlinkChild).toContainText(docChild1);
await expect(interlinkChild).toBeVisible({ timeout: 10000 });
await expect(interlinkChild.locator('svg').first()).toBeVisible();
await page.keyboard.press('@');
@@ -961,13 +960,35 @@ test.describe('Doc Editor', () => {
test('it embeds PDF', async ({ page, browserName }) => {
await createDoc(page, 'doc-toolbar', browserName, 1);
await page.getByRole('button', { name: 'Share' }).click();
await updateShareLink(page, 'Public', 'Reading');
await page.getByRole('button', { name: 'Close the share modal' }).click();
await openSuggestionMenu({ page });
await page.getByText('Embed a PDF file').click();
const pdfBlock = page.locator('div[data-content-type="pdf"]').first();
const pdfBlock = page.locator('div[data-content-type="pdf"]').last();
await expect(pdfBlock).toBeVisible();
// Try with invalid PDF first
await page.getByText(/Add (PDF|file)/).click();
await page.locator('[data-test="embed-tab"]').click();
await page
.locator('[data-test="embed-input"]')
.fill('https://example.test/test.test');
await page.locator('[data-test="embed-input-button"]').click();
await expect(page.getByText('Invalid or missing PDF file')).toBeVisible();
await openSuggestionMenu({ page });
await page.getByText('Embed a PDF file').click();
// Now with a valid PDF
await page.getByText(/Add (PDF|file)/).click();
const fileChooserPromise = page.waitForEvent('filechooser');
const downloadPromise = page.waitForEvent('download');
@@ -992,7 +1013,7 @@ test.describe('Doc Editor', () => {
await expect(pdfEmbed).toHaveAttribute('role', 'presentation');
// Check download with original filename
await page.locator('.bn-block-content[data-content-type="pdf"]').click();
await pdfBlock.click();
await page.locator('[data-test="downloadfile"]').click();
const download = await downloadPromise;
@@ -527,7 +527,7 @@ test.describe('Doc Export', () => {
await verifyDocName(page, docChild);
await page.locator('.bn-block-outer').last().fill('/');
const editor = await openSuggestionMenu({ page });
await page.getByText('Link a doc').first().click();
const input = page.locator(
@@ -544,12 +544,11 @@ test.describe('Doc Export', () => {
await searchContainer.getByText(randomDoc).click();
// Search the interlinking link in the editor (not in the document tree)
const editor = page.locator('.ProseMirror.bn-editor');
const interlink = editor.getByRole('button', {
name: randomDoc,
});
const interlink = editor
.locator('.--docs--interlinking-link-inline-content')
.first();
await expect(interlink).toBeVisible();
await expect(interlink).toContainText(randomDoc);
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${docChild}.pdf`);
@@ -593,7 +592,7 @@ test.describe('Doc Export', () => {
await verifyDocName(page, docChild);
await page.locator('.bn-block-outer').last().fill('/');
const editor = await openSuggestionMenu({ page });
await page.getByText('Link a doc').first().click();
const input = page.locator(
@@ -610,12 +609,11 @@ test.describe('Doc Export', () => {
await searchContainer.getByText(randomDoc).click();
// Search the interlinking link in the editor (not in the document tree)
const editor = page.locator('.ProseMirror.bn-editor');
const interlink = editor.getByRole('button', {
name: randomDoc,
});
const interlink = editor
.locator('.--docs--interlinking-link-inline-content')
.first();
await expect(interlink).toBeVisible();
await expect(interlink).toContainText(randomDoc);
await page
.getByRole('button', {
@@ -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',
+1 -1
View File
@@ -15,7 +15,7 @@
"test:ui::chromium": "yarn test:ui --project=chromium"
},
"devDependencies": {
"@playwright/test": "1.56.1",
"@playwright/test": "1.57.0",
"@types/node": "*",
"@types/pdf-parse": "1.1.5",
"eslint-plugin-docs": "*",
+25 -25
View File
@@ -19,14 +19,14 @@
},
"dependencies": {
"@ag-media/react-pdf-table": "2.0.3",
"@blocknote/code-block": "0.44.2",
"@blocknote/core": "0.44.2",
"@blocknote/mantine": "0.44.2",
"@blocknote/react": "0.44.2",
"@blocknote/xl-docx-exporter": "0.44.2",
"@blocknote/xl-multi-column": "0.44.2",
"@blocknote/xl-odt-exporter": "0.44.2",
"@blocknote/xl-pdf-exporter": "0.44.2",
"@blocknote/code-block": "0.45.0",
"@blocknote/core": "0.45.0",
"@blocknote/mantine": "0.45.0",
"@blocknote/react": "0.45.0",
"@blocknote/xl-docx-exporter": "0.45.0",
"@blocknote/xl-multi-column": "0.45.0",
"@blocknote/xl-odt-exporter": "0.45.0",
"@blocknote/xl-pdf-exporter": "0.45.0",
"@dnd-kit/core": "6.3.1",
"@dnd-kit/modifiers": "9.0.0",
"@emoji-mart/data": "1.2.1",
@@ -35,14 +35,14 @@
"@fontsource-variable/material-symbols-outlined": "5.2.30",
"@fontsource/material-icons": "5.2.7",
"@gouvfr-lasuite/integration": "1.0.3",
"@gouvfr-lasuite/ui-kit": "0.18.0",
"@hocuspocus/provider": "3.4.0",
"@mantine/core": "8.3.9",
"@mantine/hooks": "8.3.9",
"@gouvfr-lasuite/ui-kit": "0.18.4",
"@hocuspocus/provider": "3.4.3",
"@mantine/core": "8.3.10",
"@mantine/hooks": "8.3.10",
"@openfun/cunningham-react": "4.0.0",
"@react-pdf/renderer": "4.3.1",
"@sentry/nextjs": "10.27.0",
"@tanstack/react-query": "5.90.10",
"@sentry/nextjs": "10.30.0",
"@tanstack/react-query": "5.90.12",
"@tiptap/extensions": "*",
"canvg": "4.0.3",
"clsx": "2.1.1",
@@ -52,17 +52,17 @@
"emoji-datasource-apple": "16.0.0",
"emoji-mart": "5.6.0",
"emoji-regex": "10.6.0",
"i18next": "25.6.3",
"i18next": "25.7.2",
"i18next-browser-languagedetector": "8.2.0",
"idb": "8.0.3",
"lodash": "4.17.21",
"luxon": "3.7.2",
"next": "15.5.9",
"posthog-js": "1.298.0",
"posthog-js": "1.306.1",
"react": "*",
"react-aria-components": "1.13.0",
"react-dom": "*",
"react-i18next": "16.3.5",
"react-i18next": "16.5.0",
"react-intersection-observer": "10.0.0",
"react-resizable-panels": "3.0.6",
"react-select": "5.10.2",
@@ -70,11 +70,11 @@
"use-debounce": "10.0.6",
"y-protocols": "1.0.6",
"yjs": "*",
"zustand": "5.0.8"
"zustand": "5.0.9"
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
"@tanstack/react-query-devtools": "5.91.0",
"@tanstack/react-query-devtools": "5.91.1",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.0",
@@ -84,21 +84,21 @@
"@types/node": "*",
"@types/react": "*",
"@types/react-dom": "*",
"@vitejs/plugin-react": "5.1.1",
"@vitejs/plugin-react": "5.1.2",
"copy-webpack-plugin": "13.0.1",
"cross-env": "10.1.0",
"dotenv": "17.2.3",
"eslint-plugin-docs": "*",
"fetch-mock": "9.11.0",
"jsdom": "27.2.0",
"jsdom": "27.3.0",
"node-fetch": "2.7.0",
"prettier": "3.6.2",
"stylelint": "16.26.0",
"prettier": "3.7.4",
"stylelint": "16.26.1",
"stylelint-config-standard": "39.0.1",
"stylelint-prettier": "5.0.3",
"typescript": "*",
"vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.13",
"vite-tsconfig-paths": "6.0.1",
"vitest": "4.0.15",
"webpack": "5.103.0",
"workbox-webpack-plugin": "7.1.0"
},
@@ -52,14 +52,17 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
const { theme } = useCunninghamTheme();
const { replace } = useRouter();
const initializeResizeListener = useResponsiveStore(
(state) => state.initializeResizeListener,
);
const { initializeResizeListener, initializeInputDetection } =
useResponsiveStore();
useEffect(() => {
return initializeResizeListener();
}, [initializeResizeListener]);
useEffect(() => {
return initializeInputDetection();
}, [initializeInputDetection]);
/**
* Update the global router replace function
* This allows us to use the router replace function globally
@@ -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) => {
<link rel="stylesheet" href={conf?.FRONTEND_CSS_URL} />
</Head>
)}
{conf?.FRONTEND_JS_URL && (
<Script src={conf?.FRONTEND_JS_URL} strategy="afterInteractive" />
)}
<AnalyticsProvider>
<CrispProvider websiteId={conf?.CRISP_WEBSITE_ID}>
{children}
@@ -21,6 +21,7 @@ export interface ConfigResponse {
ENVIRONMENT: string;
FRONTEND_CSS_URL?: string;
FRONTEND_HOMEPAGE_FEATURE_ENABLED?: boolean;
FRONTEND_JS_URL?: string;
FRONTEND_THEME?: Theme;
LANGUAGES: [string, string][];
LANGUAGE_CODE: string;
@@ -44,7 +44,7 @@ export const Auth = ({ children }: PropsWithChildren) => {
if (config?.FRONTEND_HOMEPAGE_FEATURE_ENABLED) {
if (pathname !== HOME_URL) {
setIsRedirecting(true);
void replace(HOME_URL).then(() => setIsRedirecting(false));
window.location.replace(HOME_URL);
}
return;
@@ -1,6 +1,6 @@
import { baseApiUrl } from '@/api';
export const HOME_URL = '/home';
export const HOME_URL = '/home/';
export const LOGIN_URL = `${baseApiUrl()}authenticate/`;
export const LOGOUT_URL = `${baseApiUrl()}logout/`;
export const PATH_AUTH_LOCAL_STORAGE = 'docs-path-auth';
@@ -16,13 +16,13 @@ import { HocuspocusProvider } from '@hocuspocus/provider';
import { useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import type { Awareness } from 'y-protocols/awareness';
import * as Y from 'yjs';
import { Box, TextErrors } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Doc, useProviderStore } from '@/docs/doc-management';
import { avatarUrlFromName, useAuth } from '@/features/auth';
import { useResponsiveStore } from '@/stores';
import {
useHeadings,
@@ -85,12 +85,11 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
const { setEditor } = useEditorStore();
const { t } = useTranslation();
const { themeTokens } = useCunninghamTheme();
const { isDesktop } = useResponsiveStore();
const { isSynced: isConnectedToCollabServer } = useProviderStore();
const refEditorContainer = useRef<HTMLDivElement>(null);
const canSeeComment = doc.abilities.comment;
// Determine if comments should be visible in the UI
const showComments = canSeeComment && isDesktop;
const showComments = canSeeComment;
useSaveDoc(doc.id, provider.document, isConnectedToCollabServer);
const { i18n } = useTranslation();
@@ -117,7 +116,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
const editor: DocsBlockNoteEditor = useCreateBlockNote(
{
collaboration: {
provider: provider,
provider: provider as { awareness?: Awareness | undefined },
fragment: provider.document.getXmlFragment('document-store'),
user: {
name: cursorName,
@@ -163,8 +162,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
},
dictionary: {
...locales[lang as keyof typeof locales],
multi_column:
multiColumnLocales?.[lang as keyof typeof multiColumnLocales],
...(multiColumnLocales && {
multi_column:
multiColumnLocales[lang as keyof typeof multiColumnLocales],
}),
},
pasteHandler: ({ event, defaultPasteHandler }) => {
// Get clipboard data
@@ -1,15 +1,26 @@
import { FormattingToolbarExtension } from '@blocknote/core/extensions';
import {
ExperimentalMobileFormattingToolbarController,
FormattingToolbar,
FormattingToolbarController,
blockTypeSelectItems,
getFormattingToolbarItems,
useBlockNoteEditor,
useDictionary,
useExtensionState,
} from '@blocknote/react';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box } from '@/components';
import { useConfig } from '@/core/config/api';
import { useResponsiveStore } from '@/stores';
import {
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema,
} from '../../types';
import { CommentToolbarButton } from '../comments/CommentToolbarButton';
import { getCalloutFormattingToolbarItems } from '../custom-blocks';
@@ -24,6 +35,7 @@ export const BlockNoteToolbar = () => {
const [onConfirm, setOnConfirm] = useState<() => void | Promise<void>>();
const { t } = useTranslation();
const { data: conf } = useConfig();
const { isTablet, isInputTouch } = useResponsiveStore();
const toolbarItems = useMemo(() => {
let toolbarItems = getFormattingToolbarItems([
@@ -84,7 +96,13 @@ export const BlockNoteToolbar = () => {
return (
<>
<FormattingToolbarController formattingToolbar={formattingToolbar} />
{isInputTouch && isTablet ? (
<MobileFormattingToolbarController
formattingToolbar={formattingToolbar}
/>
) : (
<FormattingToolbarController formattingToolbar={formattingToolbar} />
)}
{confirmOpen && (
<ModalConfirmDownloadUnsafe
onClose={() => setIsConfirmOpen(false)}
@@ -94,3 +112,38 @@ export const BlockNoteToolbar = () => {
</>
);
};
const MobileFormattingToolbarController = ({
formattingToolbar,
}: {
formattingToolbar: () => React.ReactNode;
}) => {
const editor = useBlockNoteEditor<
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema
>();
const show = useExtensionState(FormattingToolbarExtension, {
editor,
});
if (!show) {
return null;
}
return (
<Box
$position="absolute"
$css={`
& > div {
left: 50%;
transform: translate(0px, 0px) scale(1) translateX(-50%)!important;
}
`}
>
<ExperimentalMobileFormattingToolbarController
formattingToolbar={formattingToolbar}
/>
</Box>
);
};
@@ -74,7 +74,9 @@ export function MarkdownButton() {
};
const show = useMemo(() => {
return !!selectedBlocks.find((block) => block.content !== undefined);
return (
selectedBlocks.filter((block) => block.content !== undefined).length !== 0
);
}, [selectedBlocks]);
if (!show || !editor.isEditable || !Components) {
@@ -18,7 +18,6 @@ import { css } from 'styled-components';
import { Box, Icon } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { useDocStore } from '@/features/docs/doc-management';
import { useResponsiveStore } from '@/stores';
import {
DocsBlockSchema,
@@ -31,7 +30,6 @@ export const CommentToolbarButton = () => {
const { currentDoc } = useDocStore();
const { t } = useTranslation();
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const { isDesktop } = useResponsiveStore();
const comments = useExtension('comments') as unknown as ReturnType<
ReturnType<typeof CommentsExtension>
>;
@@ -61,7 +59,6 @@ export const CommentToolbarButton = () => {
if (
!comments ||
!isDesktop ||
!show ||
!editor.isEditable ||
!Components ||
@@ -37,7 +37,8 @@ export class DocsThreadStore extends ThreadStore {
if (docAuth.canSee) {
this.awareness = awareness;
this.awareness?.on('update', this.onAwarenessUpdate);
void this.refreshThreads();
this.refreshThreads();
}
}
@@ -98,9 +99,9 @@ export class DocsThreadStore extends ThreadStore {
// If we know the threadId, schedule a targeted refresh. Otherwise, fall back to full refresh.
if (ping.threadId) {
await this.refreshThread(ping.threadId);
void this.refreshThread(ping.threadId);
} else {
await this.refreshThreads();
this.refreshThreads();
}
}
};
@@ -280,7 +281,7 @@ export class DocsThreadStore extends ThreadStore {
}),
);
await this.refreshThreads();
this.refreshThreads();
return;
}
@@ -298,7 +299,17 @@ export class DocsThreadStore extends ThreadStore {
this.notifySubscribers();
}
public async refreshThreads(): Promise<void> {
public refreshThreads() {
this.initThreads().catch((e) => {
if (!(e instanceof APIError) || e.status !== 401) {
throw e;
}
return;
});
}
public async initThreads(): Promise<void> {
const response = await fetchAPI(`documents/${this.docId}/threads/`, {
method: 'GET',
});
@@ -484,7 +495,7 @@ export class DocsThreadStore extends ThreadStore {
);
}
await this.refreshThreads();
this.refreshThreads();
this.ping(threadId);
};
@@ -26,13 +26,16 @@ export const cssComments = (
// Thread modal
.bn-thread {
width: 400px;
width: 100%;
min-width: calc(min(400px, 90vw));
max-width: calc(min(400px, 90vw));
max-height: calc(min(500px, 60vh));
padding: 8px;
box-shadow: 0px 6px 18px 0px #00001229;
margin-left: 20px;
margin-right: 20px;
gap: 0;
overflow: auto;
max-height: 500px;
.bn-default-styles {
font-family: var(--c--globals--font--families--base);
@@ -13,12 +13,13 @@ import {
createReactBlockSpec,
} from '@blocknote/react';
import { TFunction } from 'i18next';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { createGlobalStyle } from 'styled-components';
import { createGlobalStyle, css } from 'styled-components';
import { Box, Icon } from '@/components';
import { Box, Icon, Loading } from '@/components';
import { ANALYZE_URL } from '../../conf';
import { DocsBlockNoteEditor } from '../../types';
const PDFBlockStyle = createGlobalStyle`
@@ -58,14 +59,13 @@ interface PdfBlockComponentProps {
>;
}
const PdfBlockComponent = ({
editor,
block,
contentRef,
}: PdfBlockComponentProps) => {
const PdfBlockComponent = ({ editor, block }: PdfBlockComponentProps) => {
const pdfUrl = block.props.url;
const { i18n, t } = useTranslation();
const lang = i18n.resolvedLanguage;
const [isPDFContent, setIsPDFContent] = useState<boolean | null>(null);
const [isPDFContentLoading, setIsPDFContentLoading] =
useState<boolean>(false);
useEffect(() => {
if (lang && locales[lang as keyof typeof locales]) {
@@ -82,8 +82,61 @@ const PdfBlockComponent = ({
}
}, [lang, t]);
useEffect(() => {
if (!pdfUrl || pdfUrl.includes(ANALYZE_URL)) {
return;
}
const validatePDFContent = async () => {
setIsPDFContentLoading(true);
try {
const response = await fetch(pdfUrl, {
credentials: 'include',
});
const contentType = response.headers.get('content-type');
if (response.ok && contentType?.includes('application/pdf')) {
setIsPDFContent(true);
} else {
setIsPDFContent(false);
}
} catch {
setIsPDFContent(false);
} finally {
setIsPDFContentLoading(false);
}
};
void validatePDFContent();
}, [pdfUrl]);
if (isPDFContentLoading) {
return <Loading />;
}
if (!isPDFContentLoading && isPDFContent !== null && !isPDFContent) {
return (
<Box
$align="center"
$justify="center"
$color="#666"
$background="#f5f5f5"
$border="1px solid #ddd"
$height="300px"
$css={css`
text-align: center;
`}
$width="100%"
contentEditable={false}
onClick={() => editor.setTextCursorPosition(block)}
>
{t('Invalid or missing PDF file.')}
</Box>
);
}
return (
<Box ref={contentRef} className="bn-file-block-content-wrapper">
<>
<PDFBlockStyle />
<ResizableFileBlockWrapper
buttonIcon={
@@ -92,20 +145,23 @@ const PdfBlockComponent = ({
block={block as unknown as FileBlockBlock}
editor={editor as unknown as FileBlockEditor}
>
<Box
className="bn-visual-media"
role="presentation"
as="embed"
$width="100%"
$height="450px"
type="application/pdf"
src={pdfUrl}
contentEditable={false}
draggable={false}
onClick={() => editor.setTextCursorPosition(block)}
/>
{!isPDFContentLoading && isPDFContent && (
<Box
as="embed"
className="bn-visual-media"
role="presentation"
$width="100%"
$height="450px"
type="application/pdf"
src={pdfUrl}
aria-label={block.props.name || t('PDF document')}
contentEditable={false}
draggable={false}
onClick={() => editor.setTextCursorPosition(block)}
/>
)}
</ResizableFileBlockWrapper>
</Box>
</>
);
};
@@ -79,10 +79,12 @@ const LinkSelected = ({ url, title }: LinkSelectedProps) => {
return (
<BoxButton
as="span"
className="--docs--interlinking-link-inline-content"
onClick={handleClick}
draggable="false"
$css={css`
display: contents;
display: inline;
padding: 0.1rem 0.4rem;
border-radius: 4px;
& svg {
@@ -91,7 +93,9 @@ const LinkSelected = ({ url, title }: LinkSelectedProps) => {
margin-right: 0.2rem;
}
&:hover {
background-color: ${colorsTokens['gray-100']};
background-color: var(
--c--contextuals--background--semantic--contextual--primary
);
}
transition: background-color var(--c--globals--transitions--duration)
var(--c--globals--transitions--ease-out);
@@ -171,6 +171,7 @@ export const SearchPage = ({
{trigger}
<Box
as="input"
name="doc-search-input"
$padding={{ left: '3px' }}
$css={inputStyle}
ref={inputRef}
@@ -0,0 +1 @@
export const ANALYZE_URL = 'media-check';
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
import { backendUrl } from '@/api';
import { useCreateDocAttachment } from '../api';
import { ANALYZE_URL } from '../conf';
import { DocsBlockNoteEditor } from '../types';
export const useUploadFile = (docId: string) => {
@@ -46,7 +47,6 @@ export const useUploadFile = (docId: string) => {
* @param editor
*/
export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
const ANALYZE_URL = 'media-check';
const { t } = useTranslation();
/**
@@ -180,6 +180,9 @@ export const cssEditor = css`
& .bn-editor {
padding-right: 36px;
}
& .bn-toolbar {
max-width: 100vw;
}
}
@media screen and (width <= 560px) {
@@ -172,7 +172,10 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
emoji={emoji}
withEmojiPicker={doc.abilities.partial_update}
defaultIcon={
<SubPageIcon color="var(--c--contextuals--content--semantic--info--tertiary)" />
<SubPageIcon
color="var(--c--contextuals--content--semantic--info--tertiary)"
style={{ flexShrink: 0 }}
/>
}
$size="sm"
docId={doc.id}
@@ -63,13 +63,11 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
});
treeContext?.treeData.handleMove(result);
};
/**
* This function resets the tree states.
*/
const resetStateTree = useCallback(() => {
if (!treeContext?.root?.id) {
return;
}
treeContext?.setRoot(null);
setInitialOpenState(undefined);
}, [treeContext]);
@@ -29,6 +29,7 @@ export const Header = () => {
<SkipToContent />
<Box
as="header"
className="--docs--header"
role="banner"
$css={css`
position: fixed;
@@ -45,7 +46,6 @@ export const Header = () => {
border-bottom: 1px solid
var(--c--contextuals--border--surface--primary);
`}
className="--docs--header"
>
{!isDesktop && <ButtonTogglePanel />}
<StyledLink
@@ -88,7 +88,12 @@ export const Header = () => {
<LaGaufre />
</Box>
) : (
<Box $align="center" $gap={spacingsTokens['sm']} $direction="row">
<Box
className="--docs--header-block-right"
$align="center"
$gap={spacingsTokens['sm']}
$direction="row"
>
<ButtonLogin />
<LanguagePicker />
<LaGaufre />
@@ -120,7 +120,7 @@ const MainContent = ({
$css={css`
overflow-y: auto;
overflow-x: clip;
&:focus {
&:focus-visible {
outline: 3px solid ${colorsTokens['brand-400']};
outline-offset: -3px;
}
@@ -10,6 +10,12 @@ body {
padding: 0;
}
/* stylelint-disable-next-line selector-id-pattern */
body > #__next > .c__app > div:has(> .c__loader) {
min-height: 100vh;
width: 100vw;
}
* {
box-sizing: border-box;
}
@@ -1,10 +1,7 @@
import { useRouter } from 'next/router';
import { HOME_URL } from '@/features/auth';
const Page = () => {
const { replace } = useRouter();
void replace(HOME_URL);
window.location.replace(HOME_URL);
};
export default Page;
@@ -3,6 +3,7 @@ import posthog from 'posthog-js';
import { PostHogProvider as PHProvider } from 'posthog-js/react';
import { JSX, PropsWithChildren, ReactNode, useEffect } from 'react';
import { useIsOffline } from '@/features/service-worker/hooks/useOffline';
import { AbstractAnalytic, AnalyticEvent } from '@/libs/';
export class PostHogAnalytic extends AbstractAnalytic {
@@ -45,6 +46,8 @@ export function PostHogProvider({
children,
conf,
}: PropsWithChildren<PostHogProviderProps>) {
const isOffline = useIsOffline((state) => state.isOffline);
useEffect(() => {
if (!conf?.id || !conf?.host || posthog.__loaded) {
return;
@@ -53,9 +56,9 @@ export function PostHogProvider({
posthog.init(conf.id, {
api_host: conf.host,
person_profiles: 'always',
loaded: (posthog) => {
loaded: (posthogInstance) => {
if (process.env.NODE_ENV === 'development') {
posthog.debug();
posthogInstance.debug();
}
},
capture_pageview: false,
@@ -71,5 +74,14 @@ export function PostHogProvider({
};
}, [conf?.host, conf?.id]);
// Disable PostHog when offline to prevent retry requests
useEffect(() => {
if (isOffline) {
posthog.opt_out_capturing();
} else {
posthog.opt_in_capturing();
}
}, [isOffline]);
return <PHProvider client={posthog}>{children}</PHProvider>;
}
@@ -1,6 +1,7 @@
import { create } from 'zustand';
export type ScreenSize = 'small-mobile' | 'mobile' | 'tablet' | 'desktop';
export type InputMethod = 'touch' | 'mouse' | 'unknown';
export interface UseResponsiveStore {
isMobile: boolean;
@@ -10,7 +11,11 @@ export interface UseResponsiveStore {
screenWidth: number;
setScreenSize: (size: ScreenSize) => void;
isDesktop: boolean;
isTouchCapable: boolean;
isInputTouch: boolean;
inputMethod: InputMethod;
initializeResizeListener: () => () => void;
initializeInputDetection: () => () => void;
}
const initialState = {
@@ -20,6 +25,9 @@ const initialState = {
isDesktop: false,
screenSize: 'desktop' as ScreenSize,
screenWidth: 0,
isTouchCapable: false,
isInputTouch: false,
inputMethod: 'unknown' as InputMethod,
};
export const useResponsiveStore = create<UseResponsiveStore>((set) => ({
@@ -29,6 +37,9 @@ export const useResponsiveStore = create<UseResponsiveStore>((set) => ({
isTablet: initialState.isTablet,
screenSize: initialState.screenSize,
screenWidth: initialState.screenWidth,
isTouchCapable: initialState.isTouchCapable,
isInputTouch: initialState.isInputTouch,
inputMethod: initialState.inputMethod,
setScreenSize: (size: ScreenSize) => set(() => ({ screenSize: size })),
initializeResizeListener: () => {
const resizeHandler = () => {
@@ -84,4 +95,32 @@ export const useResponsiveStore = create<UseResponsiveStore>((set) => ({
window.removeEventListener('resize', debouncedResizeHandler);
};
},
initializeInputDetection: () => {
// Detect if device has touch capability
const isTouchCapable =
'ontouchstart' in window ||
navigator.maxTouchPoints > 0 ||
// @ts-ignore - for older browsers
navigator.msMaxTouchPoints > 0;
set({ isTouchCapable });
// Track actual input method being used
const handleTouchStart = () => {
set({ inputMethod: 'touch', isInputTouch: true });
};
const handleMouseMove = () => {
set({ inputMethod: 'mouse', isInputTouch: false });
};
// Listen for first interaction to determine input method
window.addEventListener('touchstart', handleTouchStart, { once: false });
window.addEventListener('mousemove', handleMouseMove, { once: false });
return () => {
window.removeEventListener('touchstart', handleTouchStart);
window.removeEventListener('mousemove', handleMouseMove);
};
},
}));
+7 -6
View File
@@ -31,14 +31,15 @@
"server:test": "yarn COLLABORATION_SERVER run test"
},
"resolutions": {
"@tiptap/extensions": "3.11.0",
"@types/node": "24.10.1",
"@types/react": "19.2.6",
"@tiptap/extensions": "3.13.0",
"@types/node": "24.10.4",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"docx": "9.5.0",
"eslint": "9.39.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"eslint": "9.39.2",
"prosemirror-view": "1.41.3",
"react": "19.2.3",
"react-dom": "19.2.3",
"typescript": "5.9.3",
"wrap-ansi": "9.0.2",
"yjs": "13.6.27"
@@ -17,21 +17,21 @@
"eslint": ">=9.0.0"
},
"dependencies": {
"@next/eslint-plugin-next": "16.0.3",
"@next/eslint-plugin-next": "15.5.9",
"@tanstack/eslint-plugin-query": "5.91.2",
"@typescript-eslint/eslint-plugin": "8.47.0",
"@typescript-eslint/parser": "8.47.0",
"@vitest/eslint-plugin": "1.4.3",
"eslint-config-next": "16.0.3",
"@typescript-eslint/eslint-plugin": "8.49.0",
"@typescript-eslint/parser": "8.49.0",
"@vitest/eslint-plugin": "1.5.2",
"eslint-config-next": "15.5.9",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-jest": "29.2.1",
"eslint-plugin-jest": "29.5.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-playwright": "2.3.0",
"eslint-plugin-playwright": "2.4.0",
"eslint-plugin-prettier": "5.5.4",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-testing-library": "7.13.5",
"prettier": "3.6.2"
"eslint-plugin-testing-library": "7.13.6",
"prettier": "3.7.4"
},
"packageManager": "yarn@1.22.22"
}
+1 -1
View File
@@ -21,7 +21,7 @@
"eslint-plugin-import": "2.32.0",
"i18next-parser": "9.3.0",
"jest": "30.2.0",
"ts-jest": "29.4.5",
"ts-jest": "29.4.6",
"typescript": "*",
"yargs": "18.0.0"
},
+9 -9
View File
@@ -16,24 +16,24 @@
"node": ">=22"
},
"dependencies": {
"@blocknote/server-util": "0.44.2",
"@hocuspocus/server": "3.4.0",
"@sentry/node": "10.26.0",
"@sentry/profiling-node": "10.26.0",
"@blocknote/server-util": "0.45.0",
"@hocuspocus/server": "3.4.3",
"@sentry/node": "10.30.0",
"@sentry/profiling-node": "10.30.0",
"@tiptap/extensions": "*",
"axios": "1.13.2",
"cors": "2.8.5",
"express": "5.1.0",
"express": "5.2.1",
"express-ws": "5.0.2",
"uuid": "13.0.0",
"y-protocols": "1.0.6",
"yjs": "*"
},
"devDependencies": {
"@blocknote/core": "0.44.2",
"@hocuspocus/provider": "3.4.0",
"@blocknote/core": "0.45.0",
"@hocuspocus/provider": "3.4.3",
"@types/cors": "2.8.19",
"@types/express": "5.0.5",
"@types/express": "5.0.6",
"@types/express-ws": "3.0.6",
"@types/node": "*",
"@types/supertest": "6.0.3",
@@ -45,7 +45,7 @@
"ts-node": "10.9.2",
"tsc-alias": "1.8.16",
"typescript": "*",
"vitest": "4.0.13",
"vitest": "4.0.15",
"vitest-mock-extended": "3.1.0",
"ws": "8.18.3"
},
+706 -1169
View File
File diff suppressed because it is too large Load Diff
@@ -31,17 +31,18 @@ backend:
LOGGING_LEVEL_HANDLERS_CONSOLE: ERROR
LOGGING_LEVEL_LOGGERS_ROOT: INFO
LOGGING_LEVEL_LOGGERS_APP: INFO
OIDC_USERINFO_SHORTNAME_FIELD: "given_name"
OIDC_USERINFO_FULLNAME_FIELDS: "given_name,usual_name"
OIDC_USERINFO_SHORTNAME_FIELD: "first_name"
OIDC_USERINFO_FULLNAME_FIELDS: "name"
OIDC_OP_JWKS_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/certs
OIDC_OP_AUTHORIZATION_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/auth
OIDC_OP_TOKEN_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/token
OIDC_OP_USER_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/userinfo
OIDC_OP_LOGOUT_ENDPOINT: https://docs-keycloak.127.0.0.1.nip.io/realms/docs/protocol/openid-connect/logout
OIDC_REDIRECT_ALLOWED_HOSTS: "docs.127.0.0.1.nip.io"
OIDC_RP_CLIENT_ID: docs
OIDC_RP_CLIENT_SECRET: ThisIsAnExampleKeyForDevPurposeOnly
OIDC_RP_SIGN_ALGO: RS256
OIDC_RP_SCOPES: "openid email given_name usual_name"
OIDC_RP_SCOPES: "openid email profile"
LOGIN_REDIRECT_URL: https://docs.127.0.0.1.nip.io
LOGIN_REDIRECT_URL_FAILURE: https://docs.127.0.0.1.nip.io
LOGOUT_REDIRECT_URL: https://docs.127.0.0.1.nip.io
@@ -32,17 +32,18 @@ backend:
LOGGING_LEVEL_HANDLERS_CONSOLE: ERROR
LOGGING_LEVEL_LOGGERS_ROOT: INFO
LOGGING_LEVEL_LOGGERS_APP: INFO
OIDC_USERINFO_SHORTNAME_FIELD: "given_name"
OIDC_USERINFO_FULLNAME_FIELDS: "given_name,usual_name"
OIDC_USERINFO_SHORTNAME_FIELD: "first_name"
OIDC_USERINFO_FULLNAME_FIELDS: "name"
OIDC_OP_JWKS_ENDPOINT: https://{{ .Values.feature }}-docs-keycloak.{{ .Values.domain }}/realms/docs/protocol/openid-connect/certs
OIDC_OP_AUTHORIZATION_ENDPOINT: https://{{ .Values.feature }}-docs-keycloak.{{ .Values.domain }}/realms/docs/protocol/openid-connect/auth
OIDC_OP_TOKEN_ENDPOINT: https://{{ .Values.feature }}-docs-keycloak.{{ .Values.domain }}/realms/docs/protocol/openid-connect/token
OIDC_OP_USER_ENDPOINT: https://{{ .Values.feature }}-docs-keycloak.{{ .Values.domain }}/realms/docs/protocol/openid-connect/userinfo
OIDC_OP_LOGOUT_ENDPOINT: https://{{ .Values.feature }}-docs-keycloak.{{ .Values.domain }}/realms/docs/protocol/openid-connect/logout
OIDC_REDIRECT_ALLOWED_HOSTS: "{{ .Values.feature }}-docs.{{ .Values.domain }}"
OIDC_RP_CLIENT_ID: docs
OIDC_RP_CLIENT_SECRET: ThisIsAnExampleKeyForDevPurposeOnly
OIDC_RP_SIGN_ALGO: RS256
OIDC_RP_SCOPES: "openid email given_name usual_name"
OIDC_RP_SCOPES: "openid email profile"
LOGIN_REDIRECT_URL: https://{{ .Values.feature }}-docs.{{ .Values.domain }}
LOGIN_REDIRECT_URL_FAILURE: https://{{ .Values.feature }}-docs.{{ .Values.domain }}
LOGOUT_REDIRECT_URL: https://{{ .Values.feature }}-docs.{{ .Values.domain }}
+14 -6
View File
@@ -39,6 +39,14 @@
| `ingressCollaborationWS.annotations.nginx.ingress.kubernetes.io/proxy-read-timeout` | | `86400` |
| `ingressCollaborationWS.annotations.nginx.ingress.kubernetes.io/proxy-send-timeout` | | `86400` |
| `ingressCollaborationWS.annotations.nginx.ingress.kubernetes.io/upstream-hash-by` | | `$arg_room` |
| `ingressRedirects.enabled` | whether to enable the Ingress Redirects or not | `false` |
| `ingressRedirects.className` | IngressClass to use for the Ingress Redirects | `nil` |
| `ingressRedirects.host` | Host for the Ingress Redirects | `impress.example.com` |
| `ingressRedirects.tls.enabled` | Weather to enable TLS for the Ingress Redirects | `true` |
| `ingressRedirects.tls.secretName` | Secret name for TLS config | `nil` |
| `ingressRedirects.tls.additional[].secretName` | Secret name for additional TLS config | |
| `ingressRedirects.tls.additional[].hosts[]` | Hosts for additional TLS config | |
| `ingressRedirects.rules` | Rules for the Ingress Redirects | `[]` |
| `ingressCollaborationApi.enabled` | whether to enable the Ingress or not | `false` |
| `ingressCollaborationApi.className` | IngressClass to use for the Ingress | `nil` |
| `ingressCollaborationApi.host` | Host for the Ingress | `impress.example.com` |
@@ -113,15 +121,15 @@
| `backend.job.annotations` | Annotations to add to the job [default: argocd.argoproj.io/hook: PostSync] | |
| `backend.cronjobs` | Cronjob name, schedule, command | `[]` |
| `backend.probes.liveness.path` | Configure path for backend HTTP liveness probe | `/__heartbeat__` |
| `backend.probes.liveness.targetPort` | Configure port for backend HTTP liveness probe | `undefined` |
| `backend.probes.liveness.targetPort` | Configure port for backend HTTP liveness probe | `nil` |
| `backend.probes.liveness.initialDelaySeconds` | Configure initial delay for backend liveness probe | `10` |
| `backend.probes.liveness.initialDelaySeconds` | Configure timeout for backend liveness probe | `10` |
| `backend.probes.startup.path` | Configure path for backend HTTP startup probe | `undefined` |
| `backend.probes.startup.targetPort` | Configure port for backend HTTP startup probe | `undefined` |
| `backend.probes.startup.initialDelaySeconds` | Configure initial delay for backend startup probe | `undefined` |
| `backend.probes.startup.initialDelaySeconds` | Configure timeout for backend startup probe | `undefined` |
| `backend.probes.startup.path` | Configure path for backend HTTP startup probe | `nil` |
| `backend.probes.startup.targetPort` | Configure port for backend HTTP startup probe | `nil` |
| `backend.probes.startup.initialDelaySeconds` | Configure initial delay for backend startup probe | `nil` |
| `backend.probes.startup.initialDelaySeconds` | Configure timeout for backend startup probe | `nil` |
| `backend.probes.readiness.path` | Configure path for backend HTTP readiness probe | `/__lbheartbeat__` |
| `backend.probes.readiness.targetPort` | Configure port for backend HTTP readiness probe | `undefined` |
| `backend.probes.readiness.targetPort` | Configure port for backend HTTP readiness probe | `nil` |
| `backend.probes.readiness.initialDelaySeconds` | Configure initial delay for backend readiness probe | `10` |
| `backend.probes.readiness.initialDelaySeconds` | Configure timeout for backend readiness probe | `10` |
| `backend.resources` | Resource requirements for the backend container | `{}` |
@@ -0,0 +1,63 @@
{{- if .Values.ingressRedirects.enabled }}
{{- $fullName := include "impress.fullname" . -}}
{{- $ns := .Release.Namespace -}}
{{- range $i, $r := .Values.ingressRedirects.rules }}
{{- $host := $r.host | default $.Values.ingressRedirects.host -}}
{{- $from := $r.from | default "/home" -}}
{{- $to := required (printf "ingressRedirects.rules[%d].to is required" $i) $r.to -}}
{{- $name := printf "%s-redirect-%s" $fullName (replace "/" "-" (trimAll "/" $from)) | trunc 63 | trimSuffix "-" -}}
{{- if $i }}
---
{{- end }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ $name }}
namespace: {{ $ns }}
annotations:
{{- if or (not $r.code) (eq (toString $r.code) "301") }}
nginx.ingress.kubernetes.io/permanent-redirect: "{{ $to }}"
{{- else }}
nginx.ingress.kubernetes.io/temporal-redirect: "{{ $to }}"
nginx.ingress.kubernetes.io/temporal-redirect-code: "{{ $r.code }}"
{{- end }}
spec:
{{- if $.Values.ingressRedirects.className }}
ingressClassName: {{ $.Values.ingressRedirects.className }}
{{- end }}
{{- if $.Values.ingressRedirects.tls.enabled }}
tls:
{{- if $host }}
- secretName: {{ $.Values.ingressRedirects.tls.secretName | default (printf "%s-tls" $fullName) | quote }}
hosts:
- {{ $host | quote }}
{{- end }}
{{- range $.Values.ingressRedirects.tls.additional }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
- host: {{ $host }}
http:
paths:
- path: {{ $from }}
pathType: Exact
backend:
service:
name: {{ include "impress.frontend.fullname" $ }}
port:
number: {{ $.Values.frontend.service.port }}
- path: {{ printf "%s/" (trimSuffix "/" $from) }}
pathType: Exact
backend:
service:
name: {{ include "impress.frontend.fullname" $ }}
port:
number: {{ $.Values.frontend.service.port }}
{{- end }}
{{- end }}
+19
View File
@@ -85,6 +85,25 @@ ingressCollaborationWS:
nginx.ingress.kubernetes.io/proxy-send-timeout: "86400"
nginx.ingress.kubernetes.io/upstream-hash-by: $arg_room
## @param ingressRedirects.enabled whether to enable the Ingress Redirects or not
## @param ingressRedirects.className IngressClass to use for the Ingress Redirects
## @param ingressRedirects.host Host for the Ingress Redirects
ingressRedirects:
enabled: false
className: null
host: impress.example.com
## @param ingressRedirects.tls.enabled Weather to enable TLS for the Ingress Redirects
## @param ingressRedirects.tls.secretName Secret name for TLS config
## @skip ingressRedirects.tls.additional
## @extra ingressRedirects.tls.additional[].secretName Secret name for additional TLS config
## @extra ingressRedirects.tls.additional[].hosts[] Hosts for additional TLS config
tls:
enabled: true
secretName: null
additional: []
## @param ingressRedirects.rules Rules for the Ingress Redirects
rules: []
## @param ingressCollaborationApi.enabled whether to enable the Ingress or not
## @param ingressCollaborationApi.className IngressClass to use for the Ingress
## @param ingressCollaborationApi.host Host for the Ingress
+1 -1
View File
@@ -5,7 +5,7 @@
"type": "module",
"dependencies": {
"@html-to/text-cli": "0.5.4",
"mjml": "4.17.1"
"mjml": "4.18.0"
},
"private": true,
"scripts": {
+193 -193
View File
@@ -581,46 +581,46 @@ minimatch@^9.0.3, minimatch@^9.0.4:
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
mjml-accordion@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-accordion/-/mjml-accordion-4.17.1.tgz#6ee3c016ea78a5a0ed29f3ec28c17d181028994f"
integrity sha512-xl9oUbMp8aju6b1OZYqv3orE287ofGNEv09h2mFmzRTJxug7nJBFXs2I9v7dUVuWIBRk940PjnIVSuW+9bPvCA==
mjml-accordion@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-accordion/-/mjml-accordion-4.18.0.tgz#4695289161fc631bd3f4c9546fedb84b42712ef8"
integrity sha512-9PUmy2JxIOGgAaVHvgVYX21nVAo3o/+wJckTTF/YTLGAqB+nm+44buxRzaXxVk7qXRwbCNfE8c8mlGVNh7vB1g==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-body@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-body/-/mjml-body-4.17.1.tgz#a4e4d2ee34abfbb45b74999ee49356b35830a0dc"
integrity sha512-EfvVVfutARRjJ1jsOxxf2DY/ufqWswv9JKjbwu/Fu8h4havAcdmw2BDmX3vwXEzatqpL1l//YWOKlqUe9ZEs+A==
mjml-body@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-body/-/mjml-body-4.18.0.tgz#20731dbbc4a92813b4c4f6ce417fc79e7a0b307f"
integrity sha512-34AwX70/7NkRIajPsa5j6NySRiNrlLatTKhiLwTVFiVtrEFlfCcbeMNmdVixI3Ldvs8209ZC6euaAnXDRyR1zw==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-button@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-button/-/mjml-button-4.17.1.tgz#1cbdf444802690329ea59ea524d088607666fa5f"
integrity sha512-A9xQwgccPzrwr11izorBsA92THpkrviWkCwlYMxL9V3wgt5YJYDrt4r023HCveqN7b6iTkvqkXeDoIPX/kEDDQ==
mjml-button@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-button/-/mjml-button-4.18.0.tgz#654a1fb6ade98c32f8c7c8e90c729edde81eeefb"
integrity sha512-ZsWMI0j7EcFCMqbqdVwMWhmsVc03FhmypWXokKopGhwySn4IAB4AOURonRmFrO7k6sDeQ+iJ9QtTu7jA+S8wmg==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-carousel@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-carousel/-/mjml-carousel-4.17.1.tgz#c395842741f55420dd7a3c08a76cd3d3a73e49ba"
integrity sha512-pWo/aIgRL3XduckOBVEvbpph3vR4f9maRrbJ8Jfu4NVI6Ws3PQ6wt7HPXHmJlzJlK0gTiAF9f4+I076RVHPG7A==
mjml-carousel@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-carousel/-/mjml-carousel-4.18.0.tgz#56e61a7873b91d9549d9992f517a471cdb433030"
integrity sha512-wY4g1CHCOoVSZuar7CLFon/qkPbICu71IT+6pa4BDwkAiaAMAemZPyy+a+iIUgdc8kHgSuHGsGf6PQzBSMWRZA==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-cli@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-cli/-/mjml-cli-4.17.1.tgz#0bc278762bc2391b6c61d16784156ee429cd104f"
integrity sha512-1cMWP+yDDBUIjDYnjiKhIPW3NYJrt/W5rqOiB3zOTZQBT722Uh055S3BoLUikKxc+1sQPww4d9dH371zX2HaXA==
mjml-cli@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-cli/-/mjml-cli-4.18.0.tgz#779902569f609ca6c1f26fadced10770a286b6fd"
integrity sha512-N6CnA4o/q/VRnGPxTzvVnjAEcF7WUVVQGYfS9SPAp0qwyf7RysMmewdS9yN8GwXwZV6L2sKdn+3ANNi2FNsJ7w==
dependencies:
"@babel/runtime" "^7.28.4"
chokidar "^3.0.0"
@@ -629,25 +629,25 @@ mjml-cli@4.17.1:
js-beautify "^1.6.14"
lodash "^4.17.21"
minimatch "^9.0.3"
mjml-core "4.17.1"
mjml-migrate "4.17.1"
mjml-parser-xml "4.17.1"
mjml-validator "4.17.1"
mjml-core "4.18.0"
mjml-migrate "4.18.0"
mjml-parser-xml "4.18.0"
mjml-validator "4.18.0"
yargs "^17.7.2"
mjml-column@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-column/-/mjml-column-4.17.1.tgz#3aa64972bb827fd96b596c71ce583e9ef720aa41"
integrity sha512-S+oNZaWFP1/TCEgVwVcwqYIyHwwVZWSKLKj4fcWIMUCaHWKuojYrexOGfULDAwTjYEDhZaRDrrq96ulF12wJeQ==
mjml-column@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-column/-/mjml-column-4.18.0.tgz#d8357449af5804e6eb506b8cf87691787e959cf5"
integrity sha512-0QZ1whxbHUmJaRT8tW+wmr3fWZ/kpsHKAd24c7Z/N1Otm/U2G0T/FFEFJ6cB25X6ZN0K40QZ8L9gdLfiSVuRbA==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-core@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-core/-/mjml-core-4.17.1.tgz#a303e8e4f94f5124284843bebd70c1d91a9085a6"
integrity sha512-u2aHbBxFA2uJdS6T1A1ZTGYryPNebHMByRrMPCbe5W8Os+sGiC5gKLhZgjavZteKiMS+09swkvfneNLGYwzBKg==
mjml-core@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-core/-/mjml-core-4.18.0.tgz#36d9ef9ff26cb010f81bef5342cf643b4fdf8043"
integrity sha512-yey72LszXvIo5p0R6DB+YU8er/nP2wPsqpLKQCB0H8vG0WRT1sbSUvnCUOkKGn7subuyWDTdzHKbQO3XYIOmvg==
dependencies:
"@babel/runtime" "^7.28.4"
cheerio "1.0.0-rc.12"
@@ -656,263 +656,263 @@ mjml-core@4.17.1:
js-beautify "^1.6.14"
juice "^10.0.0"
lodash "^4.17.21"
mjml-migrate "4.17.1"
mjml-parser-xml "4.17.1"
mjml-validator "4.17.1"
mjml-migrate "4.18.0"
mjml-parser-xml "4.18.0"
mjml-validator "4.18.0"
mjml-divider@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-divider/-/mjml-divider-4.17.1.tgz#1219c4f25d9e6f963de9438a5adf85064c7e9f01"
integrity sha512-KUWvcx1cIDwkN/gDuY37e9Vv+0U5U+xOVOfXRGloSnapYcP0IvmLtLTJeBwvGhwoN30wBiHDGwO8p/7B6CyxqQ==
mjml-divider@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-divider/-/mjml-divider-4.18.0.tgz#36877b43836d7d05b190c109df5af73a781b6a80"
integrity sha512-FmGUVJqi4RYroh7y85vDx0aUKZgECkxHtMQ4pkLGQbZ2g93/Qt0Ek88DVCNJ5XwUAQQkE/TvrGMLHp3CIqpQ9Q==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-group@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-group/-/mjml-group-4.17.1.tgz#38b4da7e67151c2c8c84378ef176432a135e11e2"
integrity sha512-0vOcLm7l4ptLM5rqC6DhCafxIw5+WUrSYLcdUSJxO3AZMGJMxU7fkWeWGowE+PQdgqh6ee1/4RYc2qJDWtHW5A==
mjml-group@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-group/-/mjml-group-4.18.0.tgz#09ec6c78e36c5c074260d888505efde645bc3da9"
integrity sha512-28ABkXsKljBqj7XCC8GkQ94xz8HEU2XTyD+9LTlkDafzGp/MGJb8DcLh/7IkxCwqkQWyeMiDNLf1djsQ909Vxw==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-head-attributes@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-head-attributes/-/mjml-head-attributes-4.17.1.tgz#198ca06a6a9b9148af1b6508aab38a41c2cf9b30"
integrity sha512-p+g33eI4xyHb9Yv28zIXnNdsXQYvoGex/GvoGumwyxu6O8OOvPk1mIV87SjDISQHosJJMcTiZVd/RfBlwnZpGA==
mjml-head-attributes@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-head-attributes/-/mjml-head-attributes-4.18.0.tgz#bf311924af6d2e0784df9bb95629bdc02583c0a7"
integrity sha512-nLzix1wrMnojE0RPGhk4iKqSRwHKjie2EPzgKT7CDzfqN+Ref03E5Q19x3cQTLgxvq3C3CnvCQBfnhoS3Eakug==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-head-breakpoint@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-head-breakpoint/-/mjml-head-breakpoint-4.17.1.tgz#4fafa9cc176c052a2796d904ff84172290826adb"
integrity sha512-vjsNAgdLnwqmkVlIENbH6odK6ZARiNQvsm+1o8CLo9ymw82WhIEbOnAeCfoddumZ6h2ywbZuBZzS23jJi13MUQ==
mjml-head-breakpoint@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-head-breakpoint/-/mjml-head-breakpoint-4.18.0.tgz#4d9d2e35ec0bdb2fd2c530241c38df9c19106dfb"
integrity sha512-k6rwff+7i+vTQYJ/CjBfE20qNqPaW60IRH2x2oEPuCzmwDmoVWOcplJIuotSqIAdfwF9hLkICknisp1BpczVlQ==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-head-font@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-head-font/-/mjml-head-font-4.17.1.tgz#0984862ebae07fbf2427e61c3d997ac155d56bd2"
integrity sha512-Xeih/vqocR1BoBLbh8Sn67kNkfLsyHeZ7Z/3nyNz7TriZ//TGAR/PGTFFghQlXyX1BCtSx/eFoxMkKKswLYReA==
mjml-head-font@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-head-font/-/mjml-head-font-4.18.0.tgz#f153a8b81275a3f25fa28142e3536e3dd4606c51"
integrity sha512-ao8HB5nf+Dmxw4GO6lMMOlnj1lNZONai0GC9RobrZgPlghZw6hpURWGpkON7pQcy6XnOHwYwkV7Go/npzA2i7w==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-head-html-attributes@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-head-html-attributes/-/mjml-head-html-attributes-4.17.1.tgz#cb4b74210257d9bb7ba9b23ca5cc409516456b8b"
integrity sha512-O7YzEAFtSELB7wVYV808g6JcxXrzHk5glDdzzCEhDR4bjPHewSUpkrYOqvt0BdfdFsvqH4zm4vsJImUMW692HQ==
mjml-head-html-attributes@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-head-html-attributes/-/mjml-head-html-attributes-4.18.0.tgz#167527f926919ff0cffdc8456e5db17953c81a11"
integrity sha512-xaQE1rthe0RrNotwEr71X1tE+QQ489Yc0ynMm3oNMrohDI/TaCeazx8GAHPMM7VLduDA8D4A5wkZ6PuEvlJu4w==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-head-preview@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-head-preview/-/mjml-head-preview-4.17.1.tgz#63e52bae35b43bdc43da838a0c3e85f3131439ef"
integrity sha512-XL+8N9yrADJSw4gX9lvDcp31ghGy8WavenVO8UhxPyhLu/sMJ9lFXLbTB4z5JU1z4t/HPEp/GtgMGxAbr+QrQQ==
mjml-head-preview@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-head-preview/-/mjml-head-preview-4.18.0.tgz#6469215ee85c15ffb58801d5aea3a9f1db337d2c"
integrity sha512-2JvYqhbLyU/+Te6/1AXxzTNoHYCDYhXOVZP7wMvU4t7K34pXqyRUNO405atyHUY1MRafrl6RJ8cIx0x5vUX7PA==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-head-style@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-head-style/-/mjml-head-style-4.17.1.tgz#29a1cb440ae36e0029af6cf1adc9eeb181ada09f"
integrity sha512-YTjtqZAG0hD0aYwk02/8hS1W+T4nDUhVCBFmcxL/aTSrRbJQew0dSVtCvqNpAsbIJCUg/mUxx6pKKzRPdN+FtA==
mjml-head-style@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-head-style/-/mjml-head-style-4.18.0.tgz#21746637547d8a2b9b1f9337349be4f1f95b3b20"
integrity sha512-nEwDHkAqY3Fm7QWeAZc/a7MakZpXh6THfrE8/AWrfpgzTHrD/wihNUc09ztNpr6z/K1+JWgQfSF2BRc+X3P46g==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-head-title@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-head-title/-/mjml-head-title-4.17.1.tgz#def969e9ac58e975bc686fc95273ddc2d23925f1"
integrity sha512-cUO4b7tDuX1BLu6XYnPgG40o3pBUCkT+Yzu5DGsvRxvCWougJFN68ocF6zcc7OOanmLgBYlJevQKUyT6W5Rp0g==
mjml-head-title@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-head-title/-/mjml-head-title-4.18.0.tgz#82f43cf4ca30f31756ce6fc4c7b71aba14d59041"
integrity sha512-0Hm8o50rPMUQLSCOOa4D4pz9NajmCDccLvBYE4fwKdeUXjSJ6bwAYeMpveel8oNZMDUVJ4Hx+PskisEGHMHM2w==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-head@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-head/-/mjml-head-4.17.1.tgz#516d6039e103424d05ec5b55202b79a2b9a440f4"
integrity sha512-+DBJ6UvkpYkKJGJKqo8luucDGbg9+rQZKytl/4VOGTE8bmbrKFixY3lkfmBrSkQ7/t6L4dDVSXywl6H91JsL+g==
mjml-head@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-head/-/mjml-head-4.18.0.tgz#93dd1194fea0f51f23454f24835e7b3428b40872"
integrity sha512-DS0adpIAsVMDIk2DOsHzjg+RNjQU0fF8jiVP9BmdRHVGrLPmpL9wIHZk2KvsKvZe7VaXXBijFt3DZ5/CQ/+D7Q==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-hero@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-hero/-/mjml-hero-4.17.1.tgz#d4f7ad9e29cb11107843f68a906f9389acb6a230"
integrity sha512-WDmNVJ4+xHLrkYOrGrq23hUYDVG3iFSyk/vIC/KlcG5Kebu5vVWbe6n3ZEucatPuYn/EUVV1ofIJM6dnXXfkGQ==
mjml-hero@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-hero/-/mjml-hero-4.18.0.tgz#13f6cb1763c3cdbafbbf5b7b1cbd6e4d76e25f61"
integrity sha512-rujm0ROM4QGWw77vnl3NaVaCKXrT4xTSHeAnkHKiY5AuRf6HPTgEtutq5pdel/y6Q9GrmxvN3HRESum7tpJCJw==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-image@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-image/-/mjml-image-4.17.1.tgz#9a427d719caf664b3a60b8f6cfb10e91dabdcb5d"
integrity sha512-ZIFXmP2Fb77vvX8SBQYbrAPPvkqx5GqJ7AqVWteQk4iz6nJf8GspZiotWyL4LvgZzf0B81aQCB11y7+RvAfVvw==
mjml-image@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-image/-/mjml-image-4.18.0.tgz#c909279b7742c8e75e2b146b8f1c565e1a0ad503"
integrity sha512-e09NkoYwvzMcTv7V6H5doWD6Te2E1y2EvOLQJoXKVdQpDwyBWGdfnZke0scJGdA58HLAB+0mLYogpLwmfLaP5Q==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-migrate@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-migrate/-/mjml-migrate-4.17.1.tgz#d50dd85f5f964d2e860741e657da03078209eeb4"
integrity sha512-Rb66BdvuV8fGYdQJzvLK0naWGI8G9smzm1OJDjdhcCrQU3BfTW/BiTS9FP5G0W73kFJe//vlHCDZ3uBIr6REAA==
mjml-migrate@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-migrate/-/mjml-migrate-4.18.0.tgz#1b08c93a9358b45f5b607043872fad216ef0ff9e"
integrity sha512-qfNCgW9zhJIsbPyXFA5RT/WY4mlje3N0WhHHOsHc0nY89Q01DenyslUy9nLLGXwi4K5FHS58oCjwWbMhwDcj1w==
dependencies:
"@babel/runtime" "^7.28.4"
js-beautify "^1.6.14"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-parser-xml "4.17.1"
mjml-core "4.18.0"
mjml-parser-xml "4.18.0"
yargs "^17.7.2"
mjml-navbar@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-navbar/-/mjml-navbar-4.17.1.tgz#215e1dc8546dc9658af59770113ac0f9b3eae47e"
integrity sha512-SWtovALlb+tM2lu2stlsKItrM/Tc/YxWiCm+UtLuOvkBmouBX/vASufaFab3VPAq/pGJKF9nFGX2eWoJCGA4rA==
mjml-navbar@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-navbar/-/mjml-navbar-4.18.0.tgz#b367eac34f22aa05e5506c07db6a0273b6255c67"
integrity sha512-uho/MS2tfNAe+V9u2X7NoCco34MDbdp30ETA8009Qo1VCP/D8lZ+s69WGRPu6hvN/Y2pzBgZly++CMg3qFZqBQ==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-parser-xml@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-parser-xml/-/mjml-parser-xml-4.17.1.tgz#7b497c20bf1bb343fe49e2c79b24aa5ae926a4a8"
integrity sha512-8cc1+cI1+ymeKmiaioZMaIzg8K9SmCErr0WOdS0n90pnt5eLqGQEh3RQJv7VoucO5aoJXgAnCSGeCstVXvZykg==
mjml-parser-xml@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-parser-xml/-/mjml-parser-xml-4.18.0.tgz#53f78f6d5710eee9fcf4bf110d19cd4b300d66bf"
integrity sha512-sHSsZg4afY1heThuJzxa1Kvfh/QzB7/9P5fFUHeVnnxb07ZTXnhXWA6YbobdND5/l9+5yjN5/UgqDZm3tIT4Uw==
dependencies:
"@babel/runtime" "^7.28.4"
detect-node "2.1.0"
htmlparser2 "^9.1.0"
lodash "^4.17.21"
mjml-preset-core@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-preset-core/-/mjml-preset-core-4.17.1.tgz#7826184b7ca57383e47597c1593e492a9a5b4102"
integrity sha512-cFfelKeRJNG+WZv+kGWjjHrQam5PiHH8JaC3vvjl1eEwLcR2nbaYArlnLTIzgG+M3+cBlIl0Ru3Say5ZqWAcxw==
mjml-preset-core@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-preset-core/-/mjml-preset-core-4.18.0.tgz#183b79cf51847e898c6e0c70601328665e85e2ef"
integrity sha512-x3l8vMVtsaqM/jauMeZIN7HFD2t5A28J4U0o4849yIlRxiWguLFV5l3BL8Byol+YLkoLuT9PjaZs9RYv+FGfeg==
dependencies:
"@babel/runtime" "^7.28.4"
mjml-accordion "4.17.1"
mjml-body "4.17.1"
mjml-button "4.17.1"
mjml-carousel "4.17.1"
mjml-column "4.17.1"
mjml-divider "4.17.1"
mjml-group "4.17.1"
mjml-head "4.17.1"
mjml-head-attributes "4.17.1"
mjml-head-breakpoint "4.17.1"
mjml-head-font "4.17.1"
mjml-head-html-attributes "4.17.1"
mjml-head-preview "4.17.1"
mjml-head-style "4.17.1"
mjml-head-title "4.17.1"
mjml-hero "4.17.1"
mjml-image "4.17.1"
mjml-navbar "4.17.1"
mjml-raw "4.17.1"
mjml-section "4.17.1"
mjml-social "4.17.1"
mjml-spacer "4.17.1"
mjml-table "4.17.1"
mjml-text "4.17.1"
mjml-wrapper "4.17.1"
mjml-accordion "4.18.0"
mjml-body "4.18.0"
mjml-button "4.18.0"
mjml-carousel "4.18.0"
mjml-column "4.18.0"
mjml-divider "4.18.0"
mjml-group "4.18.0"
mjml-head "4.18.0"
mjml-head-attributes "4.18.0"
mjml-head-breakpoint "4.18.0"
mjml-head-font "4.18.0"
mjml-head-html-attributes "4.18.0"
mjml-head-preview "4.18.0"
mjml-head-style "4.18.0"
mjml-head-title "4.18.0"
mjml-hero "4.18.0"
mjml-image "4.18.0"
mjml-navbar "4.18.0"
mjml-raw "4.18.0"
mjml-section "4.18.0"
mjml-social "4.18.0"
mjml-spacer "4.18.0"
mjml-table "4.18.0"
mjml-text "4.18.0"
mjml-wrapper "4.18.0"
mjml-raw@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-raw/-/mjml-raw-4.17.1.tgz#0422013a4b8c6f35afdc624e56b47039c6c174f2"
integrity sha512-CnfgXh+c8u/jOuVjmv9N6Hxal5U4PPJFVY1JFRRJr/7Tcxl8aJUF03mBjqW9zAzoYO1bRcgyG3clchyEwwXQ8g==
mjml-raw@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-raw/-/mjml-raw-4.18.0.tgz#8683b716c592a12560175697407e5130373cf8ac"
integrity sha512-F/kViAwXm3ccPP52kw++/mHQbcYbYYxC8JH15TZxH8GLVZkX5CGKgcBrHhDK7WoIlfEIsVRZ6IZdlHjH8vgyxw==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-section@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-section/-/mjml-section-4.17.1.tgz#10339354719e7e2c02911e56510811fb5bf9fa5b"
integrity sha512-YrkvcBgJw2NBnPirjuVU4AoqwySZzOovm5sfryID9I59EmmG+lbBJOnv/v/5wXQSlw2a4n1+VX2sCUcH5/O5sA==
mjml-section@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-section/-/mjml-section-4.18.0.tgz#ca4257bdd83c6202b03ace84f7a6246f453f9669"
integrity sha512-bB8My9zvIEkTOxej+TrjEeaeRT0lsypGeRADtdrRZXeqUClkkuCnCXlsNKSLGT8ZRqjUqWRc5z8ubDOvGk2+Gg==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-social@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-social/-/mjml-social-4.17.1.tgz#52e9f28799a1992ae291b1b7000b7c2b58cb23be"
integrity sha512-Agp6CHJn7SwD+cckCxibZ/32luTzAiDJDlKH0SjQ+9NvSoGskkhii3yOqtYnJ+t3NmQkxpRkXOnUN4GEbupghA==
mjml-social@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-social/-/mjml-social-4.18.0.tgz#c9eeb1116f77120431fa27e583e3513852e0c487"
integrity sha512-iAQc9g59L6L3VHDd55BxeIvk/zHkxflxmvuyYyOOvpmmKAvUBC//ULfpxiiM4yupofsThqFfrO+wc8d4kTRkbQ==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-spacer@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-spacer/-/mjml-spacer-4.17.1.tgz#5a81281872f3f2556c1828bf24e8df475ac71463"
integrity sha512-TxXDosuRzuoQNdceG47TKy+NWbwIGZmVDV/4XRtkcPHEvlsHpIIzn2+zzj+xrA6qh5Z+zlXL+x8ZpWMqrUoKfQ==
mjml-spacer@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-spacer/-/mjml-spacer-4.18.0.tgz#ab55416f943ca1b3e515bba6203d81b88eab8337"
integrity sha512-FK/0f5IBiONgaRpwNBs7G8EbLdAbmYqcIfHR8O8tP4LipAChLQKHO9vX3vrRMGLBZZNTESLObcFSVWmA40Mfpw==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-table@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-table/-/mjml-table-4.17.1.tgz#0955b75ff86eb80a511cbcd7a37befca8c41101c"
integrity sha512-AcAcsNrpzTOsNc0X0i0+5+iNNGEnYjwn9qodF/413yuWDSH9p7SL8vFuI3Snmgv9s1dR+BKDiF8uPt4XTOMlzA==
mjml-table@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-table/-/mjml-table-4.18.0.tgz#3c57741f3aff14428921157d63912709aa005adc"
integrity sha512-vJysCPUL3CHcsQDAFpW+skzBtY0RYsmMBYswI4WX0B05GLKlOjXqpYOwcmAupWeGoBVL5r/t28ynu2PqnOlN3w==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-text@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-text/-/mjml-text-4.17.1.tgz#77e1598c1e4d98c10490d242c9928ec3aa6e3663"
integrity sha512-pOrz8tRU3hReKd+K69dJmiVndC0+gB5IfVKIK3fdvYMb9laZBAstkXW0j5wn/0Af4FZSlJkDRLM7Ylxbh1+fqQ==
mjml-text@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-text/-/mjml-text-4.18.0.tgz#f4855611975643cef33bf1b334e1734956994d8e"
integrity sha512-hBLmF3JgveUKktKQFWHqHAr7qr92j1CxAvq7mtpDUgiWgyPFzqRX8mUsFYgZ7DmRxG4UE+Kzpt8/YFd9+E98lw==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-core "4.18.0"
mjml-validator@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-validator/-/mjml-validator-4.17.1.tgz#d73fb08bc368763f6bf0898a88b6b8452573b2d2"
integrity sha512-0Au5L5fIfAzOJpQG4PkpFeV0mbzCgjCTu5XbG7pJX4Wup72TGYwrA6Aq2yAdlx17kFPWThSZxeB3Xpd3/kwqOg==
mjml-validator@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-validator/-/mjml-validator-4.18.0.tgz#6f5fb9804a6bb5910aa9cf0b64896fb6f9ae0132"
integrity sha512-JmpWAsNTUlAxJOz2zHYfF8Vod8OzM3Qp5JXtrVw5tivZQzq88ZfqVGuqsas51z0pp1/ilfD4lC17YGfGwKGyhA==
dependencies:
"@babel/runtime" "^7.28.4"
mjml-wrapper@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml-wrapper/-/mjml-wrapper-4.17.1.tgz#8832cd7da08a32478189041b8b1c6538f204022a"
integrity sha512-c0bCgXCwffI4krnQYU0Zp8ifGkYMgE7a65NAWXlV3AWEfVmjDlhCcD8LBfZ8UfY8zR3Che8pnunowPZfwh0Nxg==
mjml-wrapper@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml-wrapper/-/mjml-wrapper-4.18.0.tgz#73e62fa05f307c6b763233284e8f27a16c3c03cd"
integrity sha512-TZeOvLjIhXEK60rjWNiYhEYNlv5GKYahE+96ifcT5OGkWkRA0DsQDfp+6VI32OS5VxsfKq2h/UdERPlQijjpAQ==
dependencies:
"@babel/runtime" "^7.28.4"
lodash "^4.17.21"
mjml-core "4.17.1"
mjml-section "4.17.1"
mjml-core "4.18.0"
mjml-section "4.18.0"
mjml@4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/mjml/-/mjml-4.17.1.tgz#fe77de5258f31b42532f601ccb10058a2d95bdee"
integrity sha512-aqy5EVZuwXIINl+d7vC1Fn+MzMfIU4qxCx2TUHnGJxYONrtNIgSQEDlgB2ns2oK8a8WgPuEJCZBYwRE+5ZFcng==
mjml@4.18.0:
version "4.18.0"
resolved "https://registry.yarnpkg.com/mjml/-/mjml-4.18.0.tgz#9e4b37d26c1ad689ca26f889048b884e47e26fff"
integrity sha512-rQM4aqFRrNvV1k733e8hJSopBjZvoSdBpRYzNTMAN+As0jqJsO5eN0wTT2IFtfe4PREzzu5b06RkPiUQdd0IIg==
dependencies:
"@babel/runtime" "^7.28.4"
mjml-cli "4.17.1"
mjml-core "4.17.1"
mjml-migrate "4.17.1"
mjml-preset-core "4.17.1"
mjml-validator "4.17.1"
mjml-cli "4.18.0"
mjml-core "4.18.0"
mjml-migrate "4.18.0"
mjml-preset-core "4.18.0"
mjml-validator "4.18.0"
no-case@^2.2.0:
version "2.3.2"