Compare commits

..

13 Commits

Author SHA1 Message Date
Manuel Raynaud 4e1a9b6a27 📝(contributing) add signed commits paragraph
Add a paragraph to inform that only signed commits are accepted now.
2025-04-14 18:14:41 +02:00
Anthony LC ecd06560c6 🚚(frontend) Display homepage on /home url
The homepage is now accessible at the /home URL.
Before the homepage was accessible on the /login URL.
We still keep the /login URL for backward compatibility.
2025-04-13 13:25:40 +02:00
Anthony LC e9ab099ce0 🚩(frontend) integrate homepage feature flag
If the homepage feature flag is enabled,
the homepage will be displayed.
2025-04-13 13:25:40 +02:00
Anthony LC 67b69d05f7 🚩(backend) add homepage feature flag
Add a homepage feature flag that we will
propagate to the frontend.
It will be used to enable or disable the
homepage at runtime.
2025-04-13 13:25:40 +02:00
virgile-deville f429eb053a 📝(readme) remove preprod account
Having a preprod account using a yopmail account was a security bad practice

Co-authored-by: Samuel Paccoud <sampaccoud@users.noreply.github.com>
2025-04-11 22:29:22 +02:00
Olivier Laurendeau ad11b7f554 📝(docs) init architecture documentation
- Add docs about architecture
- Add ADR about the CRDT choice
2025-04-10 15:15:15 +02:00
Anthony LC 3d5adad227 🔖(minor) release 3.1.0
Added:
- 🚩(backend) add feature flag for the footer
- 🔧(backend) add view to manage footer json
- (frontend) add custom css style
- 🚩(frontend) conditionally render AI button only
  when feature is enabled

Changed:
- 🚨(frontend) block button when creating doc

Fixed:
- 🐛(back) validate document content in serializer
- 🐛(frontend) fix selection click
  past end of content
2025-04-08 12:41:38 +02:00
Anthony LC de8e812f2f 🥚(frontend) remove easter egg
Remove the easter egg from the console.
2025-04-07 13:18:04 +02:00
Nathan Panchout 7a1601c682 (frontend) update favicon files and links
- Added new favicon files: favicon-dark.png and favicon.png.
- Updated the _app.tsx file to link to the new favicon files, supporting
both light and dark color schemes.
2025-04-07 13:18:04 +02:00
Nathan Panchout 0537572542 ♻️(frontend) icon component refactoring
- Add variant to IconComponent and remove $isMaterialIcon prop
- Replace all Text component used as icon with the Icon component.
2025-04-07 13:18:04 +02:00
Anthony LC 8aab007ad1 🐛(frontend) do not display firefox modal if not necessary
It is necessary to display the firefox modal only
if the user has something to save.
2025-04-07 13:18:04 +02:00
Anthony LC cde3de43f7 ♻️(frontend) misc improvements
- add opacity props on Box
- rename to cunningham-style.css
- update illustration-docs-empty.png
- smaller tooltip
2025-04-07 13:18:04 +02:00
Anthony LC 8c0c3c2f44 🐛(frontend) fix selection click past end of content
On Chrome, when we click at the end of a line,
the cursor is placed at the beginning of the line.
We fix this behavior, now the cursor is placed
at the end of the line.
2025-04-07 13:18:04 +02:00
57 changed files with 3099 additions and 2968 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
node-version: ${{ inputs.node_version }}
- name: Install dependencies
if: steps.front-node_modules.outputs.cache-hit != 'true'
run: cd src/frontend/ && AUTO_INSTALL_EXTRA_DEPS=true yarn install --frozen-lockfile
run: cd src/frontend/ && yarn install --frozen-lockfile
- name: Cache install frontend
if: steps.front-node_modules.outputs.cache-hit != 'true'
uses: actions/cache@v4
+12 -3
View File
@@ -10,8 +10,17 @@ and this project adheres to
## Added
- 🚩 add homepage feature flag #861
## [3.1.0] - 2025-04-07
## Added
- 🚩(backend) add feature flag for the footer #841
- 🔧(backend) add view to manage footer json #841
- ✨(frontend) add custom css style #771
- 🚩(frontend) conditionally render AI button only when feature is enabled #814
## Changed
@@ -20,13 +29,13 @@ and this project adheres to
## Fixed
- 🐛(back) validate document content in serializer #822
- 🐛(frontend) fix selection click past end of content #840
## [3.0.0] - 2025-03-28
## Added
- 📄(legal) Require contributors to sign a DCO #779
- ✨(frontend) add custom css style #771
## Changed
@@ -35,7 +44,6 @@ and this project adheres to
## Fixed
- 🐛(frontend) conditionally render AI button only when feature is enabled #814
- 🐛(backend) compute ancestor_links in get_abilities if needed #725
- 🔒️(back) restrict access to document accesses #801
@@ -521,7 +529,8 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/numerique-gouv/impress/compare/v3.0.0...main
[unreleased]: https://github.com/numerique-gouv/impress/compare/v3.1.0...main
[v3.1.0]: https://github.com/numerique-gouv/impress/releases/v3.1.0
[v3.0.0]: https://github.com/numerique-gouv/impress/releases/v3.0.0
[v2.6.0]: https://github.com/numerique-gouv/impress/releases/v2.6.0
[v2.5.0]: https://github.com/numerique-gouv/impress/releases/v2.5.0
+4
View File
@@ -48,6 +48,10 @@ All commit messages must adhere to the following format:
Implemented login and signup features, and integrated OAuth2 for social login.
```
## Signing commits
Only signed commits are accepted. They can be signed using a SSH or GPG key. Github documentation about signing commits contains all the information you need : https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification#about-commit-signature-verification
## Changelog Update
Please add a line to the changelog describing your development. The changelog entry should include a brief summary of the changes, this helps in tracking changes effectively and keeping everyone informed. We usually include the title of the pull request, followed by the pull request ID to finish the log entry. The changelog line should be less than 80 characters in total.
+1 -6
View File
@@ -48,12 +48,7 @@ Docs is a collaborative text editor designed to address common challenges in kno
### Test it
Test Docs on your browser by logging in on this [environment](https://impress-preprod.beta.numerique.gouv.fr/)
```
email: test.docs@yopmail.com
password: I'd<3ToTestDocs
```
Test Docs on your browser by visiting this [demo document](https://impress-preprod.beta.numerique.gouv.fr/docs/6ee5aac4-4fb9-457d-95bf-bb56c2467713/)
### Run it locally
-1
View File
@@ -158,7 +158,6 @@ services:
Y_PROVIDER_URL: "ws://localhost:4444"
MEDIA_URL: "http://localhost:8083"
SW_DEACTIVATED: "true"
AUTO_INSTALL_EXTRA_DEPS: "true"
image: impress:frontend-development
ports:
- "3000:3000"
@@ -0,0 +1,193 @@
## Decision TLDR;
We will use Yjs a CRDT-based library for the collaborative editing of the documents.
## Status
Accepted
## Context
We need to implement a collaborative editing feature for the documents that supports real-time collaboration, offline capabilities, and seamless integration with our Django backend.
## Considered alternatives
### ProseMirror
A robust toolkit for building rich-text editors with collaboration capabilities.
| Pros | Cons |
| --- | --- |
| Mature ecosystem | Complex integration with Django |
| Rich text editing features | Steeper learning curve |
| Used by major companies | More complex to implement offline support |
| Large community | |
### ShareDB
Real-time database backend based on Operational Transformation.
| Pros | Cons |
| --- | --- |
| Battle-tested in production | Complex setup required |
| Strong consistency model | Requires specific backend architecture |
| Good documentation | Less flexible with different backends |
| | Higher latency compared to CRDTs |
### Convergence
Complete enterprise solution for real-time collaboration.
| Pros | Cons |
| --- | --- |
| Full-featured solution | Commercial licensing |
| Built-in presence features | Less community support |
| Enterprise support | More expensive |
| Good offline support | Overkill for basic needs |
### CRDT-based Solutions Comparison
A CRDT-based library specifically designed for real-time collaboration.
| Category | Pros | Cons |
|----------|------|------|
| Technical Implementation | • Native real-time collaboration<br>• No central conflict resolution needed<br>• Works well with Django backend<br>• Automatic state synchronization | • Learning curve for CRDT concepts<br>• More complex initial setup<br>• Additional metadata overhead |
| User Experience | • Instant local updates<br>• Works offline by default<br>• Low latency<br>• Smooth concurrent editing | • Eventual consistency might cause brief inconsistencies<br>• UI must handle temporary conflicts |
| Performance | • Excellent scaling with multiple users<br>• Reduced server load<br>• Efficient network usage<br>• Good memory optimization (especially Yjs) | • Slightly higher memory usage<br>• Initial state sync can be larger |
| Development | • No need to build conflict resolution<br>• Simple integration with text editors<br>• Future-proof architecture | • Team needs to learn new concepts<br>• Fewer ready-made solutions<br>• May need to build some features from scratch |
| Maintenance | • Less server infrastructure<br>• Simpler deployment<br>• Fewer points of failure | • Debugging can be more complex<br>• State management requires careful handling |
| Business Impact | • Better offline support for users<br>• Scales well as user base grows<br>• No licensing costs (with Yjs) | • Initial development time might be longer<br>• Team training required |
#### Yjs
- **Type**: State-based CRDT
- **Implementation**: JavaScript/TypeScript
- **Features**:
- Rich text collaboration
- Shared types (Array, Map, XML)
- Binary encoding
- P2P support
- **Performance**: Excellent for text editing
- **Memory Usage**: Optimized
- **License**: MIT
#### Automerge
- **Type**: Operation-based CRDT
- **Implementation**: JavaScript/Rust
- **Features**:
- JSON-like data structures
- Change history
- Undo/Redo
- Binary format
- **Performance**: Good, with Rust backend
- **Memory Usage**: Higher than Yjs
- **License**: MIT
#### Legion
- **Type**: State-based CRDT
- **Implementation**: Rust with JS bindings
- **Features**:
- High performance
- Memory efficient
- Binary protocol
- **Performance**: Excellent
- **Memory Usage**: Very efficient
- **License**: Apache 2.0
#### Diamond Types
- **Type**: Operation-based CRDT
- **Implementation**: TypeScript
- **Features**:
- Specialized for text
- Small memory footprint
- Simple API
- **Performance**: Good for text
- **Memory Usage**: Efficient
- **License**: MIT
Comparison Table:
| Feature | Yjs | Automerge | Legion | Diamond Types |
|---------|-----|-----------|--------|---------------|
| Text Editing | ✅ Excellent | ✅ Good | ⚠️ Basic | ✅ Excellent |
| Structured Data | ✅ | ✅ | ✅ | ⚠️ |
| Memory Efficiency | ✅ High | ⚠️ Medium | ✅ Very High | ✅ High |
| Network Efficiency | ✅ | ⚠️ | ✅ | ✅ |
| Maturity | ✅ | ✅ | ⚠️ | ⚠️ |
| Community Size | ✅ Large | ✅ Large | ⚠️ Small | ⚠️ Small |
| Documentation | ✅ | ✅ | ⚠️ | ⚠️ |
| Backend Options | ✅ Many | ✅ Many | ⚠️ Limited | ⚠️ Limited |
Key Differences:
1. **Implementation Approach**:
- Yjs: Optimized for text and rich-text editing
- Automerge: General-purpose JSON CRDT
- Legion: Performance-focused with Rust
- Diamond Types: Specialized for text collaboration
2. **Performance Characteristics**:
- Yjs: Best for text editing scenarios
- Automerge: Good all-around performance
- Legion: Excellent raw performance
- Diamond Types: Optimized for text
3. **Ecosystem Integration**:
- Yjs: Wide range of integrations
- Automerge: Good JavaScript ecosystem
- Legion: Limited but growing
- Diamond Types: Focused on text editors
This analysis reinforces our choice of Yjs for the CRDT-based option as it provides:
- Best-in-class text editing performance
- Mature ecosystem
- Active community
- Excellent documentation
- Wide range of backend options
## Decision
After evaluating the alternatives, we choose Yjs for the following reasons:
1. **Technical Fit:**
- Native CRDT support ensures reliable collaboration
- Excellent offline capabilities
- Good performance characteristics
- Flexible backend integration options
2. **Project Requirements Match:**
- Easy integration with our Django backend
- Supports our core collaborative features
- Manageable learning curve for the team
3. **Community & Support:**
- Active development
- Growing community
- Good documentation
- Open source with MIT license
### Comparison of Key Features:
| Feature | Yjs (CRDT) | ProseMirror | ShareDB | Convergence |
|---------|-----|-------------|----------|-------------|
| Real-time Collaboration | ✅ | ✅ | ✅ | ✅ |
| Offline Support | ✅ | ⚠️ | ⚠️ | ✅ |
| Django Integration | Easy | Complex | Complex | Moderate |
| Learning Curve | Medium | High | High | Medium |
| Cost | Free | Free | Free | Paid |
| Community Size | Growing | Large | Medium | Small |
## Consequences
### Positive
- Simplified implementation of real-time collaboration
- Good developer experience
- Future-proof technology choice
- No licensing costs
### Negative
- Team needs to learn CRDT concepts
- Newer technology compared to alternatives
- May need to build some features available out-of-the-box in other solutions
### Risks
- Community support might not grow as expected
- May discover limitations as we scale
+19
View File
@@ -0,0 +1,19 @@
## Architecture
### Global system architecture
```mermaid
flowchart TD
User -- HTTP --> Front("Frontend (NextJS SPA)")
Front -- REST API --> Back("Backend (Django)")
Front -- WebSocket --> Yserver("Microservice Yjs (Express)") -- WebSocket --> CollaborationServer("Collaboration server (Hocuspocus)") -- REST API <--> Back
Front -- OIDC --> Back -- OIDC ---> OIDC("Keycloak / ProConnect")
Back -- REST API --> Yserver
Back --> DB("Database (PostgreSQL)")
Back <--> Celery --> DB
Back ----> S3("Minio (S3)")
```
### Architecture decision records
- [ADR-0001-20250106-use-yjs-for-docs-editing](./adr/ADR-0001-20250106-use-yjs-for-docs-editing.md)
+5
View File
@@ -47,6 +47,11 @@ These are the environmental variables you can set for the impress-backend contai
| COLLABORATION_API_URL | collaboration api host | |
| COLLABORATION_SERVER_SECRET | collaboration api secret | |
| COLLABORATION_WS_URL | collaboration websocket url | |
| FRONTEND_CSS_URL | To add a external css file to the app | |
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | frontend feature flag to display the homepage | false |
| FRONTEND_FOOTER_FEATURE_ENABLED | frontend feature flag to display the footer | false |
| FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT | Cache duration of the json footer | 86400 |
| FRONTEND_URL_JSON_FOOTER | Url with a json to configure the footer | |
| FRONTEND_THEME | frontend theme to use | |
| POSTHOG_KEY | posthog key for analytics | |
| CRISP_WEBSITE_ID | crisp website id for support | |
+1
View File
@@ -64,5 +64,6 @@ COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/
# Frontend
FRONTEND_THEME=default
FRONTEND_HOMEPAGE_FEATURE_ENABLED=True
FRONTEND_FOOTER_FEATURE_ENABLED=True
FRONTEND_URL_JSON_FOOTER=http://frontend:3000/contents/footer-demo.json
+1
View File
@@ -1692,6 +1692,7 @@ class ConfigView(drf.views.APIView):
"CRISP_WEBSITE_ID",
"ENVIRONMENT",
"FRONTEND_CSS_URL",
"FRONTEND_HOMEPAGE_FEATURE_ENABLED",
"FRONTEND_FOOTER_FEATURE_ENABLED",
"FRONTEND_THEME",
"MEDIA_BASE_URL",
@@ -19,6 +19,7 @@ pytestmark = pytest.mark.django_db
COLLABORATION_WS_URL="http://testcollab/",
CRISP_WEBSITE_ID="123",
FRONTEND_CSS_URL="http://testcss/",
FRONTEND_HOMEPAGE_FEATURE_ENABLED=True,
FRONTEND_FOOTER_FEATURE_ENABLED=True,
FRONTEND_THEME="test-theme",
MEDIA_BASE_URL="http://testserver/",
@@ -41,6 +42,7 @@ def test_api_config(is_authenticated):
"CRISP_WEBSITE_ID": "123",
"ENVIRONMENT": "test",
"FRONTEND_CSS_URL": "http://testcss/",
"FRONTEND_HOMEPAGE_FEATURE_ENABLED": True,
"FRONTEND_FOOTER_FEATURE_ENABLED": True,
"FRONTEND_THEME": "test-theme",
"LANGUAGES": [
+5
View File
@@ -410,6 +410,11 @@ class Base(Configuration):
FRONTEND_THEME = values.Value(
None, environ_name="FRONTEND_THEME", environ_prefix=None
)
FRONTEND_HOMEPAGE_FEATURE_ENABLED = values.BooleanValue(
default=False,
environ_name="FRONTEND_HOMEPAGE_FEATURE_ENABLED",
environ_prefix=None,
)
FRONTEND_URL_JSON_FOOTER = values.Value(
None, environ_name="FRONTEND_URL_JSON_FOOTER", environ_prefix=None
)
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "3.0.0"
version = "3.1.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
+1 -4
View File
@@ -1,16 +1,13 @@
FROM node:20-alpine AS frontend-deps
ARG AUTO_INSTALL_EXTRA_DEPS=false
WORKDIR /home/frontend/
COPY ./src/frontend/package.json ./package.json
COPY ./src/frontend/bin/conditional-install.js ./bin/conditional-install.js
COPY ./src/frontend/yarn.lock ./yarn.lock
COPY ./src/frontend/apps/impress/package.json ./apps/impress/package.json
COPY ./src/frontend/packages/eslint-config-impress/package.json ./packages/eslint-config-impress/package.json
RUN AUTO_INSTALL_EXTRA_DEPS=${AUTO_INSTALL_EXTRA_DEPS} yarn install --frozen-lockfile
RUN yarn install --frozen-lockfile
COPY .dockerignore ./.dockerignore
COPY ./src/frontend/.prettierrc.js ./.prettierrc.js
@@ -1,5 +1,26 @@
import { Page, expect } from '@playwright/test';
export const CONFIG = {
AI_FEATURE_ENABLED: true,
CRISP_WEBSITE_ID: null,
COLLABORATION_WS_URL: 'ws://localhost:4444/collaboration/ws/',
ENVIRONMENT: 'development',
FRONTEND_CSS_URL: null,
FRONTEND_HOMEPAGE_FEATURE_ENABLED: true,
FRONTEND_FOOTER_FEATURE_ENABLED: true,
FRONTEND_THEME: 'default',
MEDIA_BASE_URL: 'http://localhost:8083',
LANGUAGES: [
['en-us', 'English'],
['fr-fr', 'Français'],
['de-de', 'Deutsch'],
['nl-nl', 'Nederlands'],
],
LANGUAGE_CODE: 'en-us',
POSTHOG_KEY: {},
SENTRY_DSN: null,
};
export const keyCloakSignIn = async (
page: Page,
browserName: string,
@@ -2,27 +2,7 @@ import path from 'path';
import { expect, test } from '@playwright/test';
import { createDoc } from './common';
const config = {
AI_FEATURE_ENABLED: true,
CRISP_WEBSITE_ID: null,
COLLABORATION_WS_URL: 'ws://localhost:4444/collaboration/ws/',
ENVIRONMENT: 'development',
FRONTEND_CSS_URL: null,
FRONTEND_FOOTER_FEATURE_ENABLED: true,
FRONTEND_THEME: 'default',
MEDIA_BASE_URL: 'http://localhost:8083',
LANGUAGES: [
['en-us', 'English'],
['fr-fr', 'Français'],
['de-de', 'Deutsch'],
['nl-nl', 'Nederlands'],
],
LANGUAGE_CODE: 'en-us',
POSTHOG_KEY: {},
SENTRY_DSN: null,
};
import { CONFIG, createDoc } from './common';
test.describe('Config', () => {
test('it checks the config api is called', async ({ page }) => {
@@ -36,7 +16,7 @@ test.describe('Config', () => {
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
expect(await response.json()).toStrictEqual(config);
expect(await response.json()).toStrictEqual(CONFIG);
});
test('it checks that sentry is trying to init from config endpoint', async ({
@@ -47,7 +27,7 @@ test.describe('Config', () => {
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...config,
...CONFIG,
SENTRY_DSN: 'https://sentry.io/123',
},
});
@@ -120,7 +100,7 @@ test.describe('Config', () => {
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...config,
...CONFIG,
AI_FEATURE_ENABLED: false,
},
});
@@ -134,7 +114,7 @@ test.describe('Config', () => {
await createDoc(page, 'doc-ai-feature', browserName, 1);
await page.locator('.bn-block-outer').last().fill('Anything');
await page.getByText('Anything').dblclick();
await page.getByText('Anything').selectText();
expect(
await page.locator('button[data-test="convertMarkdown"]').count(),
).toBe(1);
@@ -151,7 +131,7 @@ test.describe('Config', () => {
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...config,
...CONFIG,
CRISP_WEBSITE_ID: '1234',
},
});
@@ -173,7 +153,7 @@ test.describe('Config', () => {
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...config,
...CONFIG,
FRONTEND_CSS_URL: 'http://localhost:123465/css/style.css',
},
});
@@ -25,7 +25,11 @@ test.describe('Doc Editor', () => {
await editor.click();
await editor.fill('test content');
await editor.getByText('test content').dblclick();
await editor
.getByText('test content', {
exact: true,
})
.selectText();
const toolbar = page.locator('.bn-formatting-toolbar');
await expect(toolbar.locator('button[data-test="bold"]')).toBeVisible();
@@ -126,7 +130,7 @@ test.describe('Doc Editor', () => {
await expect(editor.getByText('[test markdown]')).toBeVisible();
await editor.getByText('[test markdown]').dblclick();
await editor.getByText('[test markdown]').selectText();
await page.locator('button[data-test="convertMarkdown"]').click();
await expect(editor.getByText('[test markdown]')).toBeHidden();
@@ -219,11 +223,8 @@ test.describe('Doc Editor', () => {
await editor.fill('Hello World Doc persisted 2');
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
await page.goto('/');
await goToGridDoc(page, {
title: doc,
});
const urlDoc = page.url();
await page.goto(urlDoc);
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
});
@@ -297,7 +298,7 @@ test.describe('Doc Editor', () => {
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').dblclick();
await editor.getByText('Hello').selectText();
await page.getByRole('button', { name: 'AI' }).click();
@@ -380,7 +381,7 @@ test.describe('Doc Editor', () => {
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').dblclick();
await editor.getByText('Hello').selectText();
/* eslint-disable playwright/no-conditional-expect */
/* eslint-disable playwright/no-conditional-in-test */
@@ -86,7 +86,7 @@ test.describe('Document search', () => {
const editor = page.locator('.ProseMirror');
await editor.click();
await editor.fill('Hello world');
await editor.getByText('Hello world').dblclick();
await editor.getByText('Hello world').selectText();
await page.keyboard.press('Control+k');
await expect(page.getByRole('textbox', { name: 'Edit URL' })).toBeVisible();
@@ -1,5 +1,7 @@
import { expect, test } from '@playwright/test';
import { CONFIG } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/docs/');
});
@@ -50,4 +52,27 @@ test.describe('Home page', () => {
await expect(footer).toBeVisible();
});
test('it checks the homepage feature flag', async ({ page }) => {
await page.route('**/api/v1.0/config/', async (route) => {
const request = route.request();
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...CONFIG,
FRONTEND_HOMEPAGE_FEATURE_ENABLED: false,
},
});
} else {
await route.continue();
}
});
await page.goto('/');
// Keyclock login page
await expect(
page.locator('.login-pf-page-header').getByText('impress'),
).toBeVisible();
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "3.0.0",
"version": "3.1.0",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",
+4 -5
View File
@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "3.0.0",
"version": "3.1.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -19,6 +19,8 @@
"@blocknote/core": "0.23.2-hotfix.0",
"@blocknote/mantine": "0.23.2-hotfix.0",
"@blocknote/react": "0.23.2-hotfix.0",
"@blocknote/xl-docx-exporter": "0.23.2-hotfix.0",
"@blocknote/xl-pdf-exporter": "0.23.2-hotfix.0",
"@fontsource/material-icons": "5.2.5",
"@gouvfr-lasuite/integration": "1.0.2",
"@gouvfr-lasuite/ui-kit": "0.1.3",
@@ -28,6 +30,7 @@
"@sentry/nextjs": "9.3.0",
"@tanstack/react-query": "5.67.1",
"canvg": "4.0.3",
"clsx": "2.1.1",
"cmdk": "1.0.4",
"crisp-sdk-web": "1.0.25",
"docx": "9.1.1",
@@ -77,9 +80,5 @@
"typescript": "*",
"webpack": "5.98.0",
"workbox-webpack-plugin": "7.1.0"
},
"extraDependencies": {
"@blocknote/xl-docx-exporter": "0.23.2-hotfix.0",
"@blocknote/xl-pdf-exporter": "0.23.2-hotfix.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 992 B

@@ -24,6 +24,7 @@ export interface BoxProps {
$hasTransition?: boolean | 'slow';
$height?: CSSProperties['height'];
$justify?: CSSProperties['justifyContent'];
$opacity?: CSSProperties['opacity'];
$overflow?: CSSProperties['overflow'];
$margin?: MarginPadding;
$maxHeight?: CSSProperties['maxHeight'];
@@ -65,6 +66,7 @@ export const Box = styled('div')<BoxProps>`
${({ $minHeight }) => $minHeight && `min-height: ${$minHeight};`}
${({ $maxWidth }) => $maxWidth && `max-width: ${$maxWidth};`}
${({ $minWidth }) => $minWidth && `min-width: ${$minWidth};`}
${({ $opacity }) => $opacity && `opacity: ${$opacity};`}
${({ $overflow }) => $overflow && `overflow: ${$overflow};`}
${({ $padding }) => $padding && stylesPadding($padding)}
${({ $position }) => $position && `position: ${$position};`}
@@ -1,42 +1,24 @@
import clsx from 'clsx';
import { css } from 'styled-components';
import { Text, TextType } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
type IconProps = TextType & {
iconName: string;
variant?: 'filled' | 'outlined';
};
export const Icon = ({ iconName, ...textProps }: IconProps) => {
return (
<Text $isMaterialIcon {...textProps}>
{iconName}
</Text>
);
};
interface IconBGProps extends TextType {
iconName: string;
}
export const IconBG = ({ iconName, ...textProps }: IconBGProps) => {
const { colorsTokens } = useCunninghamTheme();
export const Icon = ({
iconName,
variant = 'outlined',
...textProps
}: IconProps) => {
return (
<Text
$isMaterialIcon
$size="36px"
$theme="primary"
$variation="600"
$background={colorsTokens()['primary-bg']}
$css={`
border: 1px solid ${colorsTokens()['primary-200']};
user-select: none;
`}
$radius="12px"
$padding="4px"
$margin="auto"
{...textProps}
className={`--docs--icon-bg ${textProps.className || ''}`}
className={clsx('--docs--icon-bg', textProps.className, {
'material-icons-filled': variant === 'filled',
'material-icons': variant === 'outlined',
})}
>
{iconName}
</Text>
@@ -49,15 +31,13 @@ type IconOptionsProps = TextType & {
export const IconOptions = ({ isHorizontal, ...props }: IconOptionsProps) => {
return (
<Text
<Icon
{...props}
$isMaterialIcon
iconName={isHorizontal ? 'more_horiz' : 'more_vert'}
$css={css`
user-select: none;
${props.$css}
`}
>
{isHorizontal ? 'more_horiz' : 'more_vert'}
</Text>
/>
);
};
@@ -11,7 +11,6 @@ type TextSizes = keyof typeof sizes;
export interface TextProps extends BoxProps {
as?: 'p' | 'span' | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
$elipsis?: boolean;
$isMaterialIcon?: boolean;
$weight?: CSSProperties['fontWeight'];
$textAlign?: CSSProperties['textAlign'];
$size?: TextSizes | (string & {});
@@ -57,14 +56,14 @@ export const TextStyled = styled(Box)<TextProps>`
`;
const Text = forwardRef<HTMLElement, ComponentPropsWithRef<typeof TextStyled>>(
({ className, $isMaterialIcon, ...props }, ref) => {
({ className, ...props }, ref) => {
return (
<TextStyled
ref={ref}
as="span"
$theme="greyscale"
$variation="text"
className={`${className || ''}${$isMaterialIcon ? ' material-icons' : ''}`}
className={className}
{...props}
/>
);
@@ -5,17 +5,18 @@ import { Theme } from '@/cunningham/';
import { PostHogConf } from '@/services';
interface ConfigResponse {
LANGUAGES: [string, string][];
LANGUAGE_CODE: string;
ENVIRONMENT: string;
AI_FEATURE_ENABLED?: boolean;
COLLABORATION_WS_URL?: string;
CRISP_WEBSITE_ID?: string;
FRONTEND_THEME?: Theme;
ENVIRONMENT: string;
FRONTEND_CSS_URL?: string;
FRONTEND_HOMEPAGE_FEATURE_ENABLED?: boolean;
FRONTEND_THEME?: Theme;
LANGUAGES: [string, string][];
LANGUAGE_CODE: string;
MEDIA_BASE_URL?: string;
POSTHOG_KEY?: PostHogConf;
SENTRY_DSN?: string;
AI_FEATURE_ENABLED?: boolean;
}
export const getConfig = async (): Promise<ConfigResponse> => {
@@ -1,3 +1,6 @@
@import url('@gouvfr-lasuite/ui-kit/style');
@import url('./cunningham-tokens.css');
:root {
/**
* Input
@@ -33,3 +36,10 @@
--c--components--button--border-radius
);
}
/**
* Tooltip
*/
.c__tooltip {
padding: 4px 6px;
}
@@ -3,14 +3,16 @@ import { useRouter } from 'next/router';
import { PropsWithChildren } from 'react';
import { Box } from '@/components';
import { useConfig } from '@/core';
import { useAuth } from '../hooks';
import { getAuthUrl } from '../utils';
import { getAuthUrl, gotoLogin } from '../utils';
export const Auth = ({ children }: PropsWithChildren) => {
const { isLoading, pathAllowed, isFetchedAfterMount, authenticated } =
useAuth();
const { replace, pathname } = useRouter();
const { data: config } = useConfig();
if (isLoading && !isFetchedAfterMount) {
return (
@@ -40,7 +42,11 @@ export const Auth = ({ children }: PropsWithChildren) => {
* If the user is not authenticated and the path is not allowed, we redirect to the login page.
*/
if (!authenticated && !pathAllowed) {
void replace('/login');
if (config?.FRONTEND_HOMEPAGE_FEATURE_ENABLED) {
void replace('/home');
} else {
gotoLogin();
}
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
<Loader />
@@ -49,9 +55,9 @@ export const Auth = ({ children }: PropsWithChildren) => {
}
/**
* If the user is authenticated and the path is the login page, we redirect to the home page.
* If the user is authenticated and the path is the home page, we redirect to the index.
*/
if (pathname === '/login' && authenticated) {
if (pathname === '/home' && authenticated) {
void replace('/');
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
@@ -14,7 +14,7 @@ import { PropsWithChildren, ReactNode, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { isAPIError } from '@/api';
import { Box, Text } from '@/components';
import { Box, Icon } from '@/components';
import { useDocOptions, useDocStore } from '@/docs/doc-management/';
import {
@@ -108,11 +108,7 @@ export function AIGroupButton() {
data-test="ai-actions"
label="AI"
mainTooltip={t('AI Actions')}
icon={
<Text $isMaterialIcon $size="l">
auto_awesome
</Text>
}
icon={<Icon iconName="auto_awesome" $size="l" />}
/>
</Components.Generic.Menu.Trigger>
<Components.Generic.Menu.Dropdown
@@ -124,66 +120,42 @@ export function AIGroupButton() {
<AIMenuItemTransform
action="prompt"
docId={currentDoc.id}
icon={
<Text $isMaterialIcon $size="s">
text_fields
</Text>
}
icon={<Icon iconName="text_fields" $size="s" />}
>
{t('Use as prompt')}
</AIMenuItemTransform>
<AIMenuItemTransform
action="rephrase"
docId={currentDoc.id}
icon={
<Text $isMaterialIcon $size="s">
refresh
</Text>
}
icon={<Icon iconName="refresh" $size="s" />}
>
{t('Rephrase')}
</AIMenuItemTransform>
<AIMenuItemTransform
action="summarize"
docId={currentDoc.id}
icon={
<Text $isMaterialIcon $size="s">
summarize
</Text>
}
icon={<Icon iconName="summarize" $size="s" />}
>
{t('Summarize')}
</AIMenuItemTransform>
<AIMenuItemTransform
action="correct"
docId={currentDoc.id}
icon={
<Text $isMaterialIcon $size="s">
check
</Text>
}
icon={<Icon iconName="check" $size="s" />}
>
{t('Correct')}
</AIMenuItemTransform>
<AIMenuItemTransform
action="beautify"
docId={currentDoc.id}
icon={
<Text $isMaterialIcon $size="s">
draw
</Text>
}
icon={<Icon iconName="draw" $size="s" />}
>
{t('Beautify')}
</AIMenuItemTransform>
<AIMenuItemTransform
action="emojify"
docId={currentDoc.id}
icon={
<Text $isMaterialIcon $size="s">
emoji_emotions
</Text>
}
icon={<Icon iconName="emoji_emotions" $size="s" />}
>
{t('Emojify')}
</AIMenuItemTransform>
@@ -197,9 +169,7 @@ export function AIGroupButton() {
subTrigger={true}
>
<Box $direction="row" $gap="0.6rem">
<Text $isMaterialIcon $size="s">
translate
</Text>
<Icon iconName="translate" $size="s" />
{t('Language')}
</Box>
</Components.Generic.Menu.Item>
@@ -1,7 +1,7 @@
import { Button, Modal, ModalSize } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { Box, Text } from '@/components';
import { Box, Icon, Text } from '@/components';
interface ModalConfirmDownloadUnsafeProps {
onClose: () => void;
@@ -52,9 +52,7 @@ export const ModalConfirmDownloadUnsafe = ({
$variation="1000"
$direction="row"
>
<Text $isMaterialIcon $theme="warning">
warning
</Text>
<Icon iconName="warning" $theme="warning" />
{t('Warning')}
</Text>
}
@@ -2,7 +2,7 @@ import { insertOrUpdateBlock } from '@blocknote/core';
import { createReactBlockSpec } from '@blocknote/react';
import { TFunction } from 'i18next';
import { Box, Text } from '@/components';
import { Box, Icon } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { DocsBlockNoteEditor } from '../../types';
@@ -45,11 +45,7 @@ export const getDividerReactSlashMenuItems = (
},
aliases: ['divider', 'hr', 'horizontal rule', 'line', 'separator'],
group,
icon: (
<Text $isMaterialIcon $size="18px">
remove
</Text>
),
icon: <Icon iconName="remove" $size="18px" />,
subtext: t('Add a horizontal line'),
},
];
@@ -1,9 +1,8 @@
import { defaultProps, insertOrUpdateBlock } from '@blocknote/core';
import { BlockTypeSelectItem, createReactBlockSpec } from '@blocknote/react';
import { TFunction } from 'i18next';
import React from 'react';
import { Box, Text } from '@/components';
import { Box, Icon } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { DocsBlockNoteEditor } from '../../types';
@@ -54,11 +53,7 @@ export const getQuoteReactSlashMenuItems = (
},
aliases: ['quote', 'blockquote', 'citation'],
group,
icon: (
<Text $isMaterialIcon $size="18px">
format_quote
</Text>
),
icon: <Icon iconName="format_quote" $size="18px" />,
subtext: t('Add a quote block'),
},
];
@@ -68,10 +63,6 @@ export const getQuoteFormattingToolbarItems = (
): BlockTypeSelectItem => ({
name: t('Quote'),
type: 'quote',
icon: () => (
<Text $isMaterialIcon $size="16px">
format_quote
</Text>
),
icon: () => <Icon iconName="format_quote" $size="16px" />,
isSelected: (block) => block.type === 'quote',
});
@@ -43,20 +43,22 @@ const useSaveDoc = (docId: string, yDoc: Y.Doc, canSave: boolean) => {
const saveDoc = useCallback(() => {
if (!canSave || !isLocalChange) {
return;
return false;
}
updateDoc({
id: docId,
content: toBase64(Y.encodeStateAsUpdate(yDoc)),
});
return true;
}, [canSave, yDoc, docId, isLocalChange, updateDoc]);
const router = useRouter();
useEffect(() => {
const onSave = (e?: Event) => {
saveDoc();
const isSaving = saveDoc();
/**
* Firefox does not trigger the request everytime the user leaves the page.
@@ -65,7 +67,12 @@ const useSaveDoc = (docId: string, yDoc: Y.Doc, canSave: boolean) => {
* if he wants to leave the page, by adding the popup, we let the time to the
* request to be sent, and intercepted by the service worker (for the offline part).
*/
if (typeof e !== 'undefined' && e.preventDefault && isFirefox()) {
if (
isSaving &&
typeof e !== 'undefined' &&
e.preventDefault &&
isFirefox()
) {
e.preventDefault();
}
};
@@ -105,6 +105,12 @@ export const cssEditor = (readonly: boolean) => css`
padding: 2px;
border-radius: 4px;
}
& .bn-inline-content {
width: 100%;
}
.bn-block-content[data-content-type='checkListItem'] > div {
width: 100%;
}
@media screen and (width <= 768px) {
& .bn-editor {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 293 KiB

@@ -3,7 +3,14 @@ import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next';
import { APIError } from '@/api';
import { Box, BoxButton, InfiniteScroll, Text, TextErrors } from '@/components';
import {
Box,
BoxButton,
Icon,
InfiniteScroll,
Text,
TextErrors,
} from '@/components';
import { Doc } from '@/docs/doc-management';
import { useDate } from '@/hook';
@@ -68,9 +75,7 @@ const VersionListState = ({
causes={error.cause}
icon={
error.status === 502 ? (
<Text $isMaterialIcon $theme="danger">
wifi_off
</Text>
<Icon iconName="wifi_off" $theme="danger" />
) : undefined
}
/>
@@ -76,11 +76,7 @@ export default function HomeBanner() {
) : (
<Button
onClick={() => gotoLogin()}
icon={
<Text $isMaterialIcon $color="white">
bolt
</Text>
}
icon={<Icon iconName="bolt" $color="white" />}
>
{t('Start Writing')}
</Button>
@@ -2,7 +2,7 @@ import { Button } from '@openfun/cunningham-react';
import { Trans, useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, Text } from '@/components';
import { Box, Icon, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Footer } from '@/features/footer';
import { LeftPanel } from '@/features/left-panel';
@@ -155,11 +155,7 @@ export function HomeContent() {
$margin={{ top: 'small' }}
>
<Button
icon={
<Text $isMaterialIcon $color="white">
chat
</Text>
}
icon={<Icon iconName="chat" $color="white" />}
href="https://matrix.to/#/#docs-official:matrix.org"
target="_blank"
>
@@ -3,7 +3,7 @@ import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { DropdownMenu, Text } from '@/components/';
import { DropdownMenu, Icon, Text } from '@/components/';
import { useConfig } from '@/core';
import { useLanguageSynchronizer } from './hooks/useLanguageSynchronizer';
@@ -72,9 +72,7 @@ export const LanguagePicker = () => {
$gap="0.5rem"
className="--docs--language-picker-text"
>
<Text $isMaterialIcon $color="inherit" $size="xl">
translate
</Text>
<Icon iconName="translate" $color="inherit" $size="xl" />
{currentLanguageLabel}
</Text>
</DropdownMenu>
+2 -8
View File
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import img403 from '@/assets/icons/icon-403.png';
import { Box, StyledLink, Text } from '@/components';
import { Box, Icon, StyledLink, Text } from '@/components';
import { PageLayout } from '@/layouts';
import { NextPageWithLayout } from '@/types/next';
@@ -38,13 +38,7 @@ const Page: NextPageWithLayout = () => {
</Text>
<StyledLink href="/">
<StyledButton
icon={
<Text $isMaterialIcon $color="white">
house
</Text>
}
>
<StyledButton icon={<Icon iconName="house" $color="white" />}>
{t('Home')}
</StyledButton>
</StyledLink>
+2 -8
View File
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import Icon404 from '@/assets/icons/icon-404.svg';
import { Box, StyledLink, Text } from '@/components';
import { Box, Icon, StyledLink, Text } from '@/components';
import { PageLayout } from '@/layouts';
import { NextPageWithLayout } from '@/types/next';
@@ -33,13 +33,7 @@ const Page: NextPageWithLayout = () => {
<Box $margin={{ top: 'large' }}>
<StyledLink href="/">
<StyledButton
icon={
<Text $isMaterialIcon $color="white">
house
</Text>
}
>
<StyledButton icon={<Icon iconName="house" $color="white" />}>
{t('Home')}
</StyledButton>
</StyledLink>
+13 -9
View File
@@ -1,6 +1,5 @@
import type { AppProps } from 'next/app';
import Head from 'next/head';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { AppProvider } from '@/core/';
@@ -19,14 +18,6 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page);
const { t } = useTranslation();
useEffect(() => {
console.log(
`%c
\r\n \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \r\n \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \r\n \u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \r\n \u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551 \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \r\n \u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \r\n \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \r\n \r`,
'font-size: 11px;line-height:15px;background-image: linear-gradient(#000091, #005f91);color: transparent;background-clip: text;',
);
}, []);
return (
<>
<Head>
@@ -38,6 +29,19 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) {
)}
/>
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/favicon.png" type="image/png" />
<link
rel="icon"
href="/favicon.png"
type="image/png"
media="(prefers-color-scheme: light)"
/>
<link
rel="icon"
href="/favicon-dark.png"
type="image/png"
media="(prefers-color-scheme: dark)"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<AppProvider>{getLayout(<Component {...pageProps} />)}</AppProvider>
@@ -5,7 +5,7 @@ import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Text, TextErrors } from '@/components';
import { Box, Icon, TextErrors } from '@/components';
import { DocEditor } from '@/docs/doc-editor';
import {
Doc,
@@ -126,9 +126,7 @@ const DocPage = ({ id }: DocProps) => {
causes={error.cause}
icon={
error.status === 502 ? (
<Text $isMaterialIcon $theme="danger">
wifi_off
</Text>
<Icon iconName="wifi_off" $theme="danger" />
) : undefined
}
/>
@@ -1,6 +1,4 @@
@import url('@gouvfr-lasuite/ui-kit/style');
@import url('../cunningham/cunningham-tokens.css');
@import url('../cunningham/cunningham-custom-tokens.css');
@import url('../cunningham/cunningham-style.css');
@import url('@fontsource/material-icons');
body {
@@ -0,0 +1,8 @@
import { HomeContent } from '@/features/home';
import { NextPageWithLayout } from '@/types/next';
const Page: NextPageWithLayout = () => {
return <HomeContent />;
};
export default Page;
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import Icon404 from '@/assets/icons/icon-404.svg';
import { Box, StyledLink, Text } from '@/components';
import { Box, Icon, StyledLink, Text } from '@/components';
import { MainLayout } from '@/layouts';
import { NextPageWithLayout } from '@/types/next';
@@ -31,13 +31,7 @@ const Page: NextPageWithLayout = () => {
<Box $margin={{ top: 'large' }}>
<StyledLink href="/">
<StyledButton
icon={
<Text $isMaterialIcon $color="white">
house
</Text>
}
>
<StyledButton icon={<Icon iconName="house" $color="white" />}>
{t('Home')}
</StyledButton>
</StyledLink>
-139
View File
@@ -1,139 +0,0 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const readline = require('readline');
// Check if AUTO_INSTALL_EXTRA_DEPS is explicitly set to false
if (process.env.AUTO_INSTALL_EXTRA_DEPS === 'false') {
console.log('AUTO_INSTALL_EXTRA_DEPS is set to false, skipping script execution');
process.exit(0);
}
// Get the workspace root directory
const workspaceRoot = process.cwd();
// Check if AUTO_INSTALL_EXTRA_DEPS environment variable is set to bypass prompts
const autoMode = process.env.AUTO_INSTALL_EXTRA_DEPS === 'true';
// Create readline interface for user prompts
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// Function to prompt user for confirmation
function promptUser(question) {
return new Promise((resolve) => {
rl.question(question, (answer) => {
resolve(answer.toLowerCase().startsWith('y'));
});
});
}
// Function to find all package.json files in the apps directory
function findPackageJsonFiles(directory) {
const appsDir = path.join(workspaceRoot, directory);
// Check if the directory exists
if (!fs.existsSync(appsDir)) {
console.log(`Directory ${directory} does not exist, skipping...`);
return [];
}
const packageJsonFiles = [];
// Read all items in the directory
const items = fs.readdirSync(appsDir);
for (const item of items) {
const itemPath = path.join(appsDir, item);
const stat = fs.statSync(itemPath);
if (stat.isDirectory()) {
// Check if this directory has a package.json
const packageJsonPath = path.join(itemPath, 'package.json');
if (fs.existsSync(packageJsonPath)) {
packageJsonFiles.push(packageJsonPath);
// Skip searching in subdirectories once a package.json is found
continue;
}
// Only search subdirectories if no package.json was found in this directory
packageJsonFiles.push(...findPackageJsonFiles(path.join(directory, item)));
}
}
return packageJsonFiles;
}
// Find all package.json files in the apps directory
const packageJsonFiles = findPackageJsonFiles('apps');
if (packageJsonFiles.length === 0) {
console.log('No package.json files found in the apps directory');
process.exit(0);
}
console.log(`Found ${packageJsonFiles.length} package.json files in the apps directory`);
// Process each package.json file
async function processPackageJsonFiles() {
for (const packageJsonPath of packageJsonFiles) {
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
// Check if extraDependencies exists
if (packageJson.extraDependencies && Object.keys(packageJson.extraDependencies).length > 0) {
console.log(`Found extraDependencies in ${packageJsonPath}, installing...`);
// Process each dependency individually
for (const [pkg, version] of Object.entries(packageJson.extraDependencies)) {
const dependencyToInstall = `${pkg}@${version}`;
// Prompt user if not in auto mode
let shouldInstall = autoMode;
if (!autoMode) {
shouldInstall = await promptUser(`
Do you want to install ${dependencyToInstall}? (y/n):
Note that these packages are dual-licensed by Blocknotejs
under AGPL-3.0 or a proprietary license. If you choose
to install them, please ensure you fulfill your licensing
obligations with respect to BlockNoteJS
`);
}
if (shouldInstall) {
// Install the dependency using npm install
try {
console.log(`Installing: ${dependencyToInstall}`);
execSync(`npm install --no-save --ignore-scripts --no-audit ${dependencyToInstall}`, { stdio: 'inherit' });
console.log(`Extra dependency ${dependencyToInstall} installed successfully`);
} catch (error) {
console.error(`Failed to install extra dependency ${dependencyToInstall}:`, error.message);
// Continue with other dependencies even if one fails
}
} else {
console.log(`Skipping installation of ${dependencyToInstall}`);
}
}
} else {
console.log(`No extraDependencies found in ${packageJsonPath}, skipping installation`);
}
} catch (error) {
console.error(`Error reading or parsing ${packageJsonPath}:`, error.message);
// Continue with other package.json files even if one fails
}
}
console.log('Finished processing all package.json files');
rl.close();
}
// Run the async function
processPackageJsonFiles().catch(error => {
console.error('An error occurred:', error);
rl.close();
process.exit(1);
});
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "impress",
"version": "3.0.0",
"version": "3.1.0",
"private": true,
"workspaces": {
"packages": [
@@ -25,8 +25,7 @@
"i18n:deploy": "yarn I18N run format-deploy && yarn APP_IMPRESS prettier",
"i18n:test": "yarn I18N run test",
"test": "yarn server:test && yarn app:test",
"server:test": "yarn COLLABORATION_SERVER run test",
"postinstall": "node ./bin/conditional-install.js"
"server:test": "yarn COLLABORATION_SERVER run test"
},
"resolutions": {
"@types/node": "22.13.9",
@@ -1,6 +1,6 @@
{
"name": "eslint-config-impress",
"version": "3.0.0",
"version": "3.1.0",
"license": "MIT",
"scripts": {
"lint": "eslint --ext .js ."
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "3.0.0",
"version": "3.1.0",
"private": true,
"scripts": {
"extract-translation": "yarn extract-translation:impress",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "server-y-provider",
"version": "3.0.0",
"version": "3.1.0",
"description": "Y.js provider for docs",
"repository": "https://github.com/numerique-gouv/impress",
"license": "MIT",
+2655 -2592
View File
File diff suppressed because it is too large Load Diff
@@ -50,6 +50,7 @@ backend:
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
FRONTEND_HOMEPAGE_FEATURE_ENABLED: true
FRONTEND_FOOTER_FEATURE_ENABLED: true
FRONTEND_URL_JSON_FOOTER: https://impress.127.0.0.1.nip.io/contents/footer-demo.json
POSTGRES_DB: impress
+1 -1
View File
@@ -1,7 +1,7 @@
environments:
dev:
values:
- version: 3.0.0
- version: 3.1.0
---
repositories:
- name: bitnami
+1 -1
View File
@@ -1,5 +1,5 @@
apiVersion: v2
type: application
name: docs
version: 3.0.0
version: 3.1.0
appVersion: latest
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "3.0.0",
"version": "3.1.0",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {