Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e8dc8f6b3 | |||
| a714d5da4c | |||
| 2b5a9e1af8 | |||
| a833fdc7a1 | |||
| b3cc2bf833 | |||
| 18feab10cb | |||
| 2777488d24 | |||
| a11258f778 | |||
| 33647f124f |
@@ -75,3 +75,6 @@ db.sqlite3
|
||||
.vscode/
|
||||
*.iml
|
||||
.devcontainer
|
||||
|
||||
# Cursor rules
|
||||
.cursorrules
|
||||
|
||||
+22
-2
@@ -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
|
||||
|
||||
@@ -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"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "3.8.1",
|
||||
"version": "3.8.21",
|
||||
"repository": "https://github.com/suitenumerique/docs",
|
||||
"author": "DINUM",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
+1
-1
@@ -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') {
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+3
-3
@@ -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,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,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,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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
type: application
|
||||
name: docs
|
||||
version: 3.8.1
|
||||
version: 3.8.21
|
||||
appVersion: latest
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user