Compare commits

..

9 Commits

Author SHA1 Message Date
Anthony LC 5e8dc8f6b3 🔖(patch) release 3.8.21
Changed
♻️(backend) increase user short_name field length

Fixed
🐛(frontend) fix duplicate document entries in grid
🐛(frontend) show full nested doc names with ajustable bar
🐛(backend) fix trashbin list
2025-10-23 00:03:39 +02:00
Anthony LC a714d5da4c 🐛(backend) fix trashbin list
Fix listing of deleted documents in trashbin for
users without owner access
2025-10-23 00:03:38 +02:00
Manuel Raynaud 2b5a9e1af8 ♻️(backend) increase user short_name field length
The user's short_name field length was set to 20. This is not enought
and we have some users who cannot register because of that. We changed
this length to a higher one, 100, like the full_name.
2025-10-22 11:44:39 +02:00
Cyril a833fdc7a1 (frontend) add resizable left panel on desktop with persistence
mainlayout and leftpanel updated with resizable panel saved in localstorage

Signed-off-by: Cyril <c.gromoff@gmail.com>

(frontend) show full nested doc names with horizontal scroll support

horizontal overflow enabled and opacity used for sticky actions visibility

Signed-off-by: Cyril <c.gromoff@gmail.com>

(frontend) show full nested doc names with horizontal scroll support

horizontal overflow enabled and opacity used for sticky actions visibility

Signed-off-by: Cyril <c.gromoff@gmail.com>

(frontend) add resizable-panels lib also used in our shared ui kit

needed for adaptable ui consistent with our shared ui kit components

Signed-off-by: Cyril <c.gromoff@gmail.com>
2025-10-21 10:59:24 +02:00
Anthony LC b3cc2bf833 🚨(eslint) add missing rules
We recently upgraded to Eslint v9, it seems that
it is missing some rules that we had previously.
We add them back:
- @typescript-eslint/no-inferrable-types
- @typescript-eslint/no-floating-promises
2025-10-20 21:53:10 +02:00
Anthony LC 18feab10cb (e2e) reduce flakinees
- Because of parallel test execution, some tests
were flaky when using goToGridDoc, the title
changed between the time we got the document list
and the time we clicked on the document.
- Improve addChild function.
2025-10-20 10:17:03 +02:00
Anthony LC 2777488d24 🐛(frontend) fix duplicate document entries in grid
The tests e2e were failing sometimes because
the documents list was containing duplicates.
This was happening when multiple users were
modifying the documents list (creation, update, ...).
We now deduplicate documents by their ID
before displaying them.
2025-10-20 10:17:03 +02:00
Anthony LC a11258f778 🔖(patch) release 3.8.2
Fixed:

- 🐛(service-worker) fix sw registration and page reload
  logic
2025-10-17 15:54:56 +02:00
Anthony LC 33647f124f 🐛(service-worker) fix sw registration and page reload logic
When a new service worker is installed, the page
was reloaded to ensure the new service worker took
control, it is not a big issue in normal browsing mode
because the service worker is only updated once in a
while (every release).
However, in incognito mode, the service worker has to be
re-registered on each new session, which means that
the page was reloading each time the user opened a
new incognito window, creating a bad user experience.
We now take in consideration the case where the
service-worker is installed for the first time, and don't
reload if it is this case.
2025-10-17 15:14:04 +02:00
39 changed files with 457 additions and 109 deletions
+3
View File
@@ -75,3 +75,6 @@ db.sqlite3
.vscode/
*.iml
.devcontainer
# Cursor rules
.cursorrules
+22 -2
View File
@@ -6,6 +6,22 @@ and this project adheres to
## [Unreleased]
### Changed
- ♻️(backend) increase user short_name field length
### Fixed
- 🐛(frontend) fix duplicate document entries in grid #1479
- 🐛(frontend) show full nested doc names with ajustable bar #1456
- 🐛(backend) fix trashbin list
## [3.8.2] - 2025-10-17
### Fixed
- 🐛(service-worker) fix sw registration and page reload logic #1500
## [3.8.1] - 2025-10-17
### Fixed
@@ -20,7 +36,6 @@ and this project adheres to
- 🔥(backend) remove treebeard form for the document admin #1470
## [3.8.0] - 2025-10-14
### Added
@@ -28,6 +43,10 @@ and this project adheres to
- ✨(frontend) add pdf block to the editor #1293
- ✨List and restore deleted docs #1450
### Fixed
- 🐛(frontend) show full nested doc names with ajustable bar #1456
### Changed
- ♻️(frontend) Refactor Auth component for improved redirection logic #1461
@@ -803,7 +822,8 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.8.1...main
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.8.2...main
[v3.8.2]: https://github.com/suitenumerique/docs/releases/v3.8.2
[v3.8.1]: https://github.com/suitenumerique/docs/releases/v3.8.1
[v3.8.0]: https://github.com/suitenumerique/docs/releases/v3.8.0
[v3.7.0]: https://github.com/suitenumerique/docs/releases/v3.7.0
+3
View File
@@ -636,6 +636,9 @@ class DocumentViewSet(
.values_list("document__path", flat=True)
)
if not access_documents_paths:
return self.get_response_for_queryset(self.queryset.none())
children_clause = db.Q()
for path in access_documents_paths:
children_clause |= db.Q(path__startswith=path)
@@ -0,0 +1,19 @@
# Generated by Django 5.2.7 on 2025-10-22 06:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0024_add_is_masked_field_to_link_trace"),
]
operations = [
migrations.AlterField(
model_name="user",
name="short_name",
field=models.CharField(
blank=True, max_length=100, null=True, verbose_name="short name"
),
),
]
+3 -1
View File
@@ -148,7 +148,9 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
)
full_name = models.CharField(_("full name"), max_length=100, null=True, blank=True)
short_name = models.CharField(_("short name"), max_length=20, null=True, blank=True)
short_name = models.CharField(
_("short name"), max_length=100, null=True, blank=True
)
email = models.EmailField(_("identity email address"), blank=True, null=True)
@@ -293,3 +293,26 @@ def test_api_documents_trashbin_distinct():
content = response.json()
assert len(content["results"]) == 1
assert content["results"][0]["id"] == str(document.id)
def test_api_documents_trashbin_empty_queryset_bug():
"""
Test that users with no owner role don't see documents.
"""
# Create a new user with no owner access to any document
new_user = factories.UserFactory()
client = APIClient()
client.force_login(new_user)
# Create some deleted documents owned by other users
other_user = factories.UserFactory()
factories.DocumentFactory(users=[(other_user, "owner")], deleted_at=timezone.now())
factories.DocumentFactory(users=[(other_user, "owner")], deleted_at=timezone.now())
factories.DocumentFactory(users=[(other_user, "owner")], deleted_at=timezone.now())
response = client.get("/api/v1.0/documents/trashbin/")
assert response.status_code == 200
content = response.json()
assert content["count"] == 0
assert len(content["results"]) == 0
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "3.8.1"
version = "3.8.21"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { createDoc, goToGridDoc, verifyDocName } from './utils-common';
import { createDoc, verifyDocName } from './utils-common';
import { addNewMember } from './utils-share';
test.beforeEach(async ({ page }) => {
@@ -8,8 +8,14 @@ test.beforeEach(async ({ page }) => {
});
test.describe('Document list members', () => {
test('it checks a big list of members', async ({ page }) => {
const docTitle = await goToGridDoc(page);
test('it checks a big list of members', async ({ page, browserName }) => {
const [docTitle] = await createDoc(
page,
'members-big-members-list',
browserName,
1,
);
await verifyDocName(page, docTitle);
// Get the current URL and extract the last part
@@ -73,7 +79,7 @@ test.describe('Document list members', () => {
await expect(loadMore).toBeHidden();
});
test('it checks a big list of invitations', async ({ page }) => {
test('it checks a big list of invitations', async ({ page, browserName }) => {
await page.route(
/.*\/documents\/.*\/invitations\/\?page=.*/,
async (route) => {
@@ -108,7 +114,12 @@ test.describe('Document list members', () => {
},
);
const docTitle = await goToGridDoc(page);
const [docTitle] = await createDoc(
page,
'members-big-invitation-list',
browserName,
1,
);
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
@@ -102,6 +102,7 @@ test.describe('Doc Trashbin', () => {
page,
browserName,
docParent: subDocName,
docName: 'my-trash-editor-subsubdoc',
});
await verifyDocName(page, subsubDocName);
@@ -1,5 +1,7 @@
import { expect, test } from '@playwright/test';
import { createDoc } from './utils-common';
test.describe('Left panel desktop', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -11,6 +13,53 @@ test.describe('Left panel desktop', () => {
await expect(page.getByTestId('home-button')).toBeVisible();
await expect(page.getByTestId('new-doc-button')).toBeVisible();
});
test('checks resize handle is present and functional on document page', async ({
page,
browserName,
}) => {
// On home page, resize handle should NOT be present
let resizeHandle = page.locator('[data-panel-resize-handle-id]');
await expect(resizeHandle).toBeHidden();
// Create and navigate to a document
await createDoc(page, 'doc-resize-test', browserName, 1);
// Now resize handle should be visible on document page
resizeHandle = page.locator('[data-panel-resize-handle-id]').first();
await expect(resizeHandle).toBeVisible();
const leftPanel = page.getByTestId('left-panel-desktop');
await expect(leftPanel).toBeVisible();
// Get initial panel width
const initialBox = await leftPanel.boundingBox();
expect(initialBox).not.toBeNull();
// Get handle position
const handleBox = await resizeHandle.boundingBox();
expect(handleBox).not.toBeNull();
// Test resize by dragging the handle
await page.mouse.move(
handleBox!.x + handleBox!.width / 2,
handleBox!.y + handleBox!.height / 2,
);
await page.mouse.down();
await page.mouse.move(
handleBox!.x + 100,
handleBox!.y + handleBox!.height / 2,
);
await page.mouse.up();
// Wait for resize to complete
await page.waitForTimeout(200);
// Verify the panel has been resized
const newBox = await leftPanel.boundingBox();
expect(newBox).not.toBeNull();
expect(newBox!.width).toBeGreaterThan(initialBox!.width);
});
});
test.describe('Left panel mobile', () => {
@@ -47,4 +96,12 @@ test.describe('Left panel mobile', () => {
await expect(languageButton).toBeInViewport();
await expect(logoutButton).toBeInViewport();
});
test('checks resize handle is not present on mobile', async ({ page }) => {
await page.goto('/');
// Verify the resize handle is NOT present on mobile
const resizeHandle = page.locator('[data-panel-resize-handle-id]');
await expect(resizeHandle).toBeHidden();
});
});
@@ -48,7 +48,7 @@ export const overrideConfig = async (
export const keyCloakSignIn = async (
page: Page,
browserName: string,
fromHome: boolean = true,
fromHome = true,
) => {
if (fromHome) {
await page.getByRole('button', { name: 'Start Writing' }).first().click();
@@ -79,8 +79,8 @@ export const createDoc = async (
page: Page,
docName: string,
browserName: string,
length: number = 1,
isMobile: boolean = false,
length = 1,
isMobile = false,
) => {
const randomDocs = randomName(docName, browserName, length);
@@ -22,6 +22,6 @@ export const writeInEditor = async ({
text: string;
}) => {
const editor = await getEditor({ page });
editor.locator('.bn-block-outer').last().fill(text);
await editor.locator('.bn-block-outer').last().fill(text);
return editor;
};
@@ -15,7 +15,7 @@ export const addNewMember = async (
page: Page,
index: number,
role: 'Administrator' | 'Owner' | 'Editor' | 'Reader',
fillText: string = 'user.test',
fillText = 'user.test',
) => {
const responsePromiseSearchUser = page.waitForResponse(
(response) =>
@@ -12,7 +12,7 @@ export const createRootSubPage = async (
page: Page,
browserName: BrowserName,
docName: string,
isMobile: boolean = false,
isMobile = false,
) => {
if (isMobile) {
await page
@@ -72,10 +72,12 @@ export const addChild = async ({
page,
browserName,
docParent,
docName,
}: {
page: Page;
browserName: BrowserName;
docParent: string;
docName: string;
}) => {
let item = page.getByTestId('doc-tree-root-item');
@@ -99,7 +101,7 @@ export const addChild = async ({
await item.hover();
await item.getByTestId('doc-tree-item-actions-add-child').click();
const [name] = randomName(docParent, browserName, 1);
const [name] = randomName(docName, browserName, 1);
await updateDocTitle(page, name);
return name;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "3.8.1",
"version": "3.8.21",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "3.8.1",
"version": "3.8.21",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -60,6 +60,7 @@
"react-dom": "*",
"react-i18next": "15.7.3",
"react-intersection-observer": "9.16.0",
"react-resizable-panels": "3.0.6",
"react-select": "5.10.2",
"styled-components": "6.1.19",
"use-debounce": "10.0.6",
+1 -1
View File
@@ -18,5 +18,5 @@ export const backendUrl = () =>
* @param apiVersion - The version of the API (defaults to '1.0').
* @returns The full versioned API base URL as a string.
*/
export const baseApiUrl = (apiVersion: string = '1.0') =>
export const baseApiUrl = (apiVersion = '1.0') =>
`${backendUrl()}/api/v${apiVersion}/`;
@@ -1,6 +1,6 @@
import { baseApiUrl } from '@/api';
export const HOME_URL: string = '/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';
@@ -22,7 +22,7 @@ function isBlock(block: Block): block is Block {
);
}
const recursiveContent = (content: Block[], base: string = '') => {
const recursiveContent = (content: Block[], base = '') => {
let fullContent = base;
for (const innerContent of content) {
if (innerContent.type === 'text') {
@@ -56,7 +56,7 @@ const LinkSelected = ({ url, title }: LinkSelectedProps) => {
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
router.push(url);
void router.push(url);
};
return (
@@ -163,6 +163,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
aria-label={`${t('Open document {{title}}', { title: docTitle })}`}
$css={css`
text-align: left;
min-width: 0;
`}
>
<Box $width="16px" $height="16px">
@@ -180,8 +181,10 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
display: flex;
flex-direction: row;
width: 100%;
min-width: 0;
gap: 0.5rem;
align-items: center;
overflow: hidden;
`}
>
<Text $css={ItemTextCss} $size="sm" $variation="1000">
@@ -184,7 +184,6 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
/* Remove outline from TreeViewItem wrapper elements */
.c__tree-view--row {
outline: none !important;
&:focus-visible {
outline: none !important;
}
@@ -241,7 +240,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
}
}
&:hover,
&:focus-within {
&:focus-visible {
.doc-tree-root-item-actions {
opacity: 1;
}
@@ -1,4 +1,5 @@
import { Button } from '@openfun/cunningham-react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { InView } from 'react-intersection-observer';
import { css } from 'styled-components';
@@ -36,7 +37,19 @@ export const DocsGrid = ({
hasNextPage,
} = useDocsQuery(target);
const docs = data?.pages.flatMap((page) => page.results) ?? [];
const docs = useMemo(() => {
const allDocs = data?.pages.flatMap((page) => page.results) ?? [];
// Deduplicate documents by ID to prevent the same doc appearing multiple times
// This can happen when a multiple users are impacting the docs list (creation, update, ...)
const seenIds = new Set<string>();
return allDocs.filter((doc) => {
if (seenIds.has(doc.id)) {
return false;
}
seenIds.add(doc.id);
return true;
});
}, [data?.pages]);
const loading = isFetching || isLoading;
const hasDocs = data?.pages.some((page) => page.results.length > 0);
@@ -64,8 +64,8 @@ describe('DocsGridItemDate', () => {
});
});
it(`should render rendered the updated_at field in the correct language`, () => {
i18next.changeLanguage('fr');
it(`should render rendered the updated_at field in the correct language`, async () => {
await i18next.changeLanguage('fr');
render(
<DocsGridItemDate
@@ -83,7 +83,7 @@ describe('DocsGridItemDate', () => {
expect(screen.getByRole('link')).toBeInTheDocument();
expect(screen.getByText('il y a 5 jours')).toBeInTheDocument();
i18next.changeLanguage('en');
await i18next.changeLanguage('en');
});
[
@@ -39,12 +39,10 @@ export const LeftPanel = () => {
{isDesktop && (
<Box
data-testid="left-panel-desktop"
$css={`
$css={css`
height: calc(100vh - ${HEADER_HEIGHT}px);
width: 300px;
min-width: 300px;
width: 100%;
overflow: hidden;
border-right: 1px solid ${colorsTokens['greyscale-200']};
background-color: ${colorsTokens['greyscale-000']};
`}
className="--docs--left-panel-desktop"
@@ -0,0 +1,110 @@
import { useEffect, useRef, useState } from 'react';
import {
ImperativePanelHandle,
Panel,
PanelGroup,
PanelResizeHandle,
} from 'react-resizable-panels';
import { createGlobalStyle } from 'styled-components';
import { useCunninghamTheme } from '@/cunningham';
interface PanelStyleProps {
$isResizing: boolean;
}
const PanelStyle = createGlobalStyle<PanelStyleProps>`
${({ $isResizing }) => $isResizing && `body * { transition: none !important; }`}
`;
// Convert a target pixel width to a percentage of the current viewport width.
// react-resizable-panels expects sizes in %, not px.
const calculateDefaultSize = (targetWidth: number) => {
const windowWidth = window.innerWidth;
return (targetWidth / windowWidth) * 100;
};
type ResizableLeftPanelProps = {
leftPanel: React.ReactNode;
children: React.ReactNode;
minPanelSizePx?: number;
maxPanelSizePx?: number;
};
export const ResizableLeftPanel = ({
leftPanel,
children,
minPanelSizePx = 300,
maxPanelSizePx = 450,
}: ResizableLeftPanelProps) => {
const [isResizing, setIsResizing] = useState(false);
const { colorsTokens } = useCunninghamTheme();
const ref = useRef<ImperativePanelHandle>(null);
const resizeTimeoutRef = useRef<number | undefined>(undefined);
const [minPanelSize, setMinPanelSize] = useState(0);
const [maxPanelSize, setMaxPanelSize] = useState(0);
// Single resize listener that handles both panel size updates and transition disabling
useEffect(() => {
const handleResize = () => {
// Update panel sizes (px -> %)
const min = Math.round(calculateDefaultSize(minPanelSizePx));
const max = Math.round(
Math.min(calculateDefaultSize(maxPanelSizePx), 40),
);
setMinPanelSize(min);
setMaxPanelSize(max);
// Temporarily disable transitions to avoid flicker
setIsResizing(true);
if (resizeTimeoutRef.current) {
clearTimeout(resizeTimeoutRef.current);
}
resizeTimeoutRef.current = window.setTimeout(() => {
setIsResizing(false);
}, 150);
};
handleResize();
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
if (resizeTimeoutRef.current) {
clearTimeout(resizeTimeoutRef.current);
}
};
}, [minPanelSizePx, maxPanelSizePx]);
return (
<>
<PanelStyle $isResizing={isResizing} />
<PanelGroup
autoSaveId="docs-left-panel-persistence"
direction="horizontal"
>
<Panel
ref={ref}
order={0}
defaultSize={minPanelSize}
minSize={minPanelSize}
maxSize={maxPanelSize}
>
{leftPanel}
</Panel>
<PanelResizeHandle
style={{
borderRightWidth: '1px',
borderRightStyle: 'solid',
borderRightColor: colorsTokens['greyscale-200'],
width: '1px',
cursor: 'col-resize',
}}
/>
<Panel order={1}>{children}</Panel>
</PanelGroup>
</>
);
};
@@ -1 +1,2 @@
export * from './LeftPanel';
export * from './ResizableLeftPanel';
@@ -3,51 +3,69 @@ import { useEffect } from 'react';
export const useSWRegister = () => {
useEffect(() => {
if (
'serviceWorker' in navigator &&
process.env.NEXT_PUBLIC_SW_DEACTIVATED !== 'true'
!('serviceWorker' in navigator) ||
process.env.NEXT_PUBLIC_SW_DEACTIVATED === 'true'
) {
navigator.serviceWorker
.register(`/service-worker.js`)
.then((registration) => {
registration.onupdatefound = () => {
const newWorker = registration.installing;
if (!newWorker) {
return;
return;
}
const hadControllerAtStart = !!navigator.serviceWorker.controller;
navigator.serviceWorker
.register(`/service-worker.js`)
.then((registration) => {
registration.onupdatefound = () => {
const newWorker = registration.installing;
if (!newWorker) {
return;
}
newWorker.onstatechange = () => {
if (
newWorker.state === 'installed' &&
navigator.serviceWorker.controller
) {
newWorker.postMessage({ type: 'SKIP_WAITING' });
}
newWorker.onstatechange = () => {
if (
newWorker.state === 'installed' &&
navigator.serviceWorker.controller
) {
newWorker.postMessage({ type: 'SKIP_WAITING' });
}
};
};
})
.catch((err) => {
console.error('Service worker registration failed:', err);
});
};
})
.catch((err) => {
console.error('Service worker registration failed:', err);
});
let refreshing = false;
const onControllerChange = () => {
if (refreshing) {
return;
}
refreshing = true;
let refreshing = false;
const onControllerChange = () => {
if (!hadControllerAtStart || refreshing) {
return;
}
refreshing = true;
if (document.visibilityState === 'visible') {
window.location.reload();
return;
}
const onVisible = () => {
if (document.visibilityState === 'visible') {
window.location.reload();
}
};
navigator.serviceWorker.addEventListener(
document.addEventListener('visibilitychange', onVisible, { once: true });
};
navigator.serviceWorker.addEventListener(
'controllerchange',
onControllerChange,
);
return () => {
navigator.serviceWorker.removeEventListener(
'controllerchange',
onControllerChange,
);
return () => {
navigator.serviceWorker.removeEventListener(
'controllerchange',
onControllerChange,
);
};
}
};
}, []);
};
@@ -6,23 +6,21 @@ import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Header } from '@/features/header';
import { HEADER_HEIGHT } from '@/features/header/conf';
import { LeftPanel } from '@/features/left-panel';
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
import { LeftPanel, ResizableLeftPanel } from '@/features/left-panel';
import { useResponsiveStore } from '@/stores';
import { MAIN_LAYOUT_ID } from './conf';
type MainLayoutProps = {
backgroundColor?: 'white' | 'grey';
enableResizablePanel?: boolean;
};
export function MainLayout({
children,
backgroundColor = 'white',
enableResizablePanel = false,
}: PropsWithChildren<MainLayoutProps>) {
const { isDesktop } = useResponsiveStore();
const { colorsTokens } = useCunninghamTheme();
const currentBackgroundColor = !isDesktop ? 'white' : backgroundColor;
const { t } = useTranslation();
return (
<Box className="--docs--main-layout">
<Header />
@@ -30,33 +28,90 @@ export function MainLayout({
$direction="row"
$margin={{ top: `${HEADER_HEIGHT}px` }}
$width="100%"
$height={`calc(100dvh - ${HEADER_HEIGHT}px)`}
>
<LeftPanel />
<Box
as="main"
role="main"
aria-label={t('Main content')}
id={MAIN_LAYOUT_ID}
$align="center"
$flex={1}
$width="100%"
$height={`calc(100dvh - ${HEADER_HEIGHT}px)`}
$padding={{
all: isDesktop ? 'base' : '0',
}}
$background={
currentBackgroundColor === 'white'
? colorsTokens['greyscale-000']
: colorsTokens['greyscale-050']
}
$css={css`
overflow-y: auto;
overflow-x: clip;
`}
<MainLayoutContent
backgroundColor={backgroundColor}
enableResizablePanel={enableResizablePanel}
>
{children}
</Box>
</MainLayoutContent>
</Box>
</Box>
);
}
export interface MainLayoutContentProps {
backgroundColor: 'white' | 'grey';
enableResizablePanel?: boolean;
}
export function MainLayoutContent({
children,
backgroundColor,
enableResizablePanel = false,
}: PropsWithChildren<MainLayoutContentProps>) {
const { isDesktop } = useResponsiveStore();
const { colorsTokens } = useCunninghamTheme();
const { t } = useTranslation();
const currentBackgroundColor = !isDesktop ? 'white' : backgroundColor;
const mainContent = (
<Box
as="main"
role="main"
aria-label={t('Main content')}
id={MAIN_LAYOUT_ID}
$align="center"
$flex={1}
$width="100%"
$height={`calc(100dvh - ${HEADER_HEIGHT}px)`}
$padding={{
all: isDesktop ? 'base' : '0',
}}
$background={
currentBackgroundColor === 'white'
? colorsTokens['greyscale-000']
: colorsTokens['greyscale-050']
}
$css={css`
overflow-y: auto;
overflow-x: clip;
`}
>
{children}
</Box>
);
if (!isDesktop) {
return (
<>
<LeftPanel />
{mainContent}
</>
);
}
if (enableResizablePanel) {
return (
<ResizableLeftPanel leftPanel={<LeftPanel />}>
{mainContent}
</ResizableLeftPanel>
);
}
return (
<>
<Box
$css={css`
width: 300px;
min-width: 300px;
border-right: 1px solid ${colorsTokens['greyscale-200']};
`}
>
<LeftPanel />
</Box>
{mainContent}
</>
);
}
@@ -47,7 +47,7 @@ export function DocLayout() {
return subPageToTree(doc.results);
}}
>
<MainLayout>
<MainLayout enableResizablePanel={true}>
<DocPage id={id} />
</MainLayout>
</TreeProvider>
@@ -115,7 +115,9 @@ const DocPage = ({ id }: DocProps) => {
// Invalidate when provider store reports a lost connection
useEffect(() => {
if (hasLostConnection && doc?.id) {
queryClient.invalidateQueries({ queryKey: [KEY_DOC, { id: doc.id }] });
void queryClient.invalidateQueries({
queryKey: [KEY_DOC, { id: doc.id }],
});
resetLostConnection();
}
}, [hasLostConnection, doc?.id, queryClient, resetLostConnection]);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "impress",
"version": "3.8.1",
"version": "3.8.21",
"private": true,
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-docs",
"version": "3.8.1",
"version": "3.8.21",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -22,7 +22,9 @@ const typescriptConfig = {
'@typescript-eslint': typescriptEslint,
},
rules: {
'@typescript-eslint/no-inferrable-types': 'error',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/no-unsafe-argument': 'error',
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "3.8.1",
"version": "3.8.21",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "server-y-provider",
"version": "3.8.1",
"version": "3.8.21",
"description": "Y.js provider for docs",
"repository": "https://github.com/suitenumerique/docs",
"license": "MIT",
+5
View File
@@ -13793,6 +13793,11 @@ react-resizable-panels@2.1.7:
resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz#afd29d8a3d708786a9f95183a38803c89f13c2e7"
integrity sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==
react-resizable-panels@3.0.6:
version "3.0.6"
resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-3.0.6.tgz#8183132ea13a09821e9c93962ed49f240cdcfd3f"
integrity sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==
react-select@5.10.2:
version "5.10.2"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.10.2.tgz#8dffc69dfd7d74684d9613e6eb27204e3b99e127"
+2 -2
View File
@@ -1,10 +1,10 @@
environments:
dev:
values:
- version: 3.8.1
- version: 3.8.21
feature:
values:
- version: 3.8.1
- version: 3.8.21
feature: ci
domain: example.com
imageTag: demo
+1 -1
View File
@@ -1,5 +1,5 @@
apiVersion: v2
type: application
name: docs
version: 3.8.1
version: 3.8.21
appVersion: latest
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "3.8.1",
"version": "3.8.21",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {