Compare commits

..

1 Commits

Author SHA1 Message Date
Manuel Raynaud 401913297e ⚗️(front) conditionaly install blocknote xl packages
The blocknote xl packages are licensed under AGPL v3. We want to allow a
reuser to build the project without them. For this, in a first step we
don't list these packages in the dependencies but install them using a
postinstall script.
This script is a node script looking in every package.json files under
the apps directory, look if an `extraDependencies` key exists and if yes
hten will install the listed dependencies after prompting the user if
wants or not to install them.
Also an environment variable AUTO_INSTALL_EXTRA_DEPS can be used to
explicitly install all the dependencies or to explicitly not install
them. This will help to build an image with and without these
dependencies. Moreover we also need thisin a CI context.
2025-04-06 14:06:07 +02:00
57 changed files with 2975 additions and 3106 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/ && yarn install --frozen-lockfile
run: cd src/frontend/ && AUTO_INSTALL_EXTRA_DEPS=true yarn install --frozen-lockfile
- name: Cache install frontend
if: steps.front-node_modules.outputs.cache-hit != 'true'
uses: actions/cache@v4
+3 -12
View File
@@ -10,17 +10,8 @@ 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
@@ -29,13 +20,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
@@ -44,6 +35,7 @@ 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
@@ -529,8 +521,7 @@ 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.1.0...main
[v3.1.0]: https://github.com/numerique-gouv/impress/releases/v3.1.0
[unreleased]: https://github.com/numerique-gouv/impress/compare/v3.0.0...main
[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,10 +48,6 @@ 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.
+6 -1
View File
@@ -48,7 +48,12 @@ Docs is a collaborative text editor designed to address common challenges in kno
### Test it
Test Docs on your browser by visiting this [demo document](https://impress-preprod.beta.numerique.gouv.fr/docs/6ee5aac4-4fb9-457d-95bf-bb56c2467713/)
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
```
### Run it locally
+1
View File
@@ -158,6 +158,7 @@ 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"
@@ -1,193 +0,0 @@
## 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
@@ -1,19 +0,0 @@
## 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,11 +47,6 @@ 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,6 +64,5 @@ 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,7 +1692,6 @@ 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,7 +19,6 @@ 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/",
@@ -42,7 +41,6 @@ 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,11 +410,6 @@ 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.1.0"
version = "3.0.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
+4 -1
View File
@@ -1,13 +1,16 @@
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 yarn install --frozen-lockfile
RUN AUTO_INSTALL_EXTRA_DEPS=${AUTO_INSTALL_EXTRA_DEPS} yarn install --frozen-lockfile
COPY .dockerignore ./.dockerignore
COPY ./src/frontend/.prettierrc.js ./.prettierrc.js
@@ -1,26 +1,5 @@
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,7 +2,27 @@ import path from 'path';
import { expect, test } from '@playwright/test';
import { CONFIG, createDoc } from './common';
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,
};
test.describe('Config', () => {
test('it checks the config api is called', async ({ page }) => {
@@ -16,7 +36,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 ({
@@ -27,7 +47,7 @@ test.describe('Config', () => {
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...CONFIG,
...config,
SENTRY_DSN: 'https://sentry.io/123',
},
});
@@ -100,7 +120,7 @@ test.describe('Config', () => {
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...CONFIG,
...config,
AI_FEATURE_ENABLED: false,
},
});
@@ -114,7 +134,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').selectText();
await page.getByText('Anything').dblclick();
expect(
await page.locator('button[data-test="convertMarkdown"]').count(),
).toBe(1);
@@ -131,7 +151,7 @@ test.describe('Config', () => {
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...CONFIG,
...config,
CRISP_WEBSITE_ID: '1234',
},
});
@@ -153,7 +173,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,11 +25,7 @@ test.describe('Doc Editor', () => {
await editor.click();
await editor.fill('test content');
await editor
.getByText('test content', {
exact: true,
})
.selectText();
await editor.getByText('test content').dblclick();
const toolbar = page.locator('.bn-formatting-toolbar');
await expect(toolbar.locator('button[data-test="bold"]')).toBeVisible();
@@ -130,7 +126,7 @@ test.describe('Doc Editor', () => {
await expect(editor.getByText('[test markdown]')).toBeVisible();
await editor.getByText('[test markdown]').selectText();
await editor.getByText('[test markdown]').dblclick();
await page.locator('button[data-test="convertMarkdown"]').click();
await expect(editor.getByText('[test markdown]')).toBeHidden();
@@ -223,8 +219,11 @@ test.describe('Doc Editor', () => {
await editor.fill('Hello World Doc persisted 2');
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
const urlDoc = page.url();
await page.goto(urlDoc);
await page.goto('/');
await goToGridDoc(page, {
title: doc,
});
await expect(editor.getByText('Hello World Doc persisted 2')).toBeVisible();
});
@@ -298,7 +297,7 @@ test.describe('Doc Editor', () => {
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
await editor.getByText('Hello').dblclick();
await page.getByRole('button', { name: 'AI' }).click();
@@ -381,7 +380,7 @@ test.describe('Doc Editor', () => {
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
await editor.getByText('Hello').dblclick();
/* 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').selectText();
await editor.getByText('Hello world').dblclick();
await page.keyboard.press('Control+k');
await expect(page.getByRole('textbox', { name: 'Edit URL' })).toBeVisible();
@@ -1,7 +1,5 @@
import { expect, test } from '@playwright/test';
import { CONFIG } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/docs/');
});
@@ -52,27 +50,4 @@ 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.1.0",
"version": "3.0.0",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "3.1.0",
"version": "3.0.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -19,8 +19,6 @@
"@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",
@@ -30,7 +28,6 @@
"@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",
@@ -80,5 +77,9 @@
"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.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 992 B

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

After

Width:  |  Height:  |  Size: 280 KiB

@@ -3,14 +3,7 @@ import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next';
import { APIError } from '@/api';
import {
Box,
BoxButton,
Icon,
InfiniteScroll,
Text,
TextErrors,
} from '@/components';
import { Box, BoxButton, InfiniteScroll, Text, TextErrors } from '@/components';
import { Doc } from '@/docs/doc-management';
import { useDate } from '@/hook';
@@ -75,7 +68,9 @@ const VersionListState = ({
causes={error.cause}
icon={
error.status === 502 ? (
<Icon iconName="wifi_off" $theme="danger" />
<Text $isMaterialIcon $theme="danger">
wifi_off
</Text>
) : undefined
}
/>
@@ -76,7 +76,11 @@ export default function HomeBanner() {
) : (
<Button
onClick={() => gotoLogin()}
icon={<Icon iconName="bolt" $color="white" />}
icon={
<Text $isMaterialIcon $color="white">
bolt
</Text>
}
>
{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, Icon, Text } from '@/components';
import { Box, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Footer } from '@/features/footer';
import { LeftPanel } from '@/features/left-panel';
@@ -155,7 +155,11 @@ export function HomeContent() {
$margin={{ top: 'small' }}
>
<Button
icon={<Icon iconName="chat" $color="white" />}
icon={
<Text $isMaterialIcon $color="white">
chat
</Text>
}
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, Icon, Text } from '@/components/';
import { DropdownMenu, Text } from '@/components/';
import { useConfig } from '@/core';
import { useLanguageSynchronizer } from './hooks/useLanguageSynchronizer';
@@ -72,7 +72,9 @@ export const LanguagePicker = () => {
$gap="0.5rem"
className="--docs--language-picker-text"
>
<Icon iconName="translate" $color="inherit" $size="xl" />
<Text $isMaterialIcon $color="inherit" $size="xl">
translate
</Text>
{currentLanguageLabel}
</Text>
</DropdownMenu>
+8 -2
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, Icon, StyledLink, Text } from '@/components';
import { Box, StyledLink, Text } from '@/components';
import { PageLayout } from '@/layouts';
import { NextPageWithLayout } from '@/types/next';
@@ -38,7 +38,13 @@ const Page: NextPageWithLayout = () => {
</Text>
<StyledLink href="/">
<StyledButton icon={<Icon iconName="house" $color="white" />}>
<StyledButton
icon={
<Text $isMaterialIcon $color="white">
house
</Text>
}
>
{t('Home')}
</StyledButton>
</StyledLink>
+8 -2
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, Icon, StyledLink, Text } from '@/components';
import { Box, StyledLink, Text } from '@/components';
import { PageLayout } from '@/layouts';
import { NextPageWithLayout } from '@/types/next';
@@ -33,7 +33,13 @@ const Page: NextPageWithLayout = () => {
<Box $margin={{ top: 'large' }}>
<StyledLink href="/">
<StyledButton icon={<Icon iconName="house" $color="white" />}>
<StyledButton
icon={
<Text $isMaterialIcon $color="white">
house
</Text>
}
>
{t('Home')}
</StyledButton>
</StyledLink>
+9 -13
View File
@@ -1,5 +1,6 @@
import type { AppProps } from 'next/app';
import Head from 'next/head';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { AppProvider } from '@/core/';
@@ -18,6 +19,14 @@ 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>
@@ -29,19 +38,6 @@ 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, Icon, TextErrors } from '@/components';
import { Box, Text, TextErrors } from '@/components';
import { DocEditor } from '@/docs/doc-editor';
import {
Doc,
@@ -126,7 +126,9 @@ const DocPage = ({ id }: DocProps) => {
causes={error.cause}
icon={
error.status === 502 ? (
<Icon iconName="wifi_off" $theme="danger" />
<Text $isMaterialIcon $theme="danger">
wifi_off
</Text>
) : undefined
}
/>
@@ -1,4 +1,6 @@
@import url('../cunningham/cunningham-style.css');
@import url('@gouvfr-lasuite/ui-kit/style');
@import url('../cunningham/cunningham-tokens.css');
@import url('../cunningham/cunningham-custom-tokens.css');
@import url('@fontsource/material-icons');
body {
@@ -1,8 +0,0 @@
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, Icon, StyledLink, Text } from '@/components';
import { Box, StyledLink, Text } from '@/components';
import { MainLayout } from '@/layouts';
import { NextPageWithLayout } from '@/types/next';
@@ -31,7 +31,13 @@ const Page: NextPageWithLayout = () => {
<Box $margin={{ top: 'large' }}>
<StyledLink href="/">
<StyledButton icon={<Icon iconName="house" $color="white" />}>
<StyledButton
icon={
<Text $isMaterialIcon $color="white">
house
</Text>
}
>
{t('Home')}
</StyledButton>
</StyledLink>
+139
View File
@@ -0,0 +1,139 @@
#!/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);
});
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "impress",
"version": "3.1.0",
"version": "3.0.0",
"private": true,
"workspaces": {
"packages": [
@@ -25,7 +25,8 @@
"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"
"server:test": "yarn COLLABORATION_SERVER run test",
"postinstall": "node ./bin/conditional-install.js"
},
"resolutions": {
"@types/node": "22.13.9",
@@ -1,6 +1,6 @@
{
"name": "eslint-config-impress",
"version": "3.1.0",
"version": "3.0.0",
"license": "MIT",
"scripts": {
"lint": "eslint --ext .js ."
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "3.1.0",
"version": "3.0.0",
"private": true,
"scripts": {
"extract-translation": "yarn extract-translation:impress",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "server-y-provider",
"version": "3.1.0",
"version": "3.0.0",
"description": "Y.js provider for docs",
"repository": "https://github.com/numerique-gouv/impress",
"license": "MIT",
+2598 -2661
View File
File diff suppressed because it is too large Load Diff
@@ -50,7 +50,6 @@ 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.1.0
- version: 3.0.0
---
repositories:
- name: bitnami
+1 -1
View File
@@ -1,5 +1,5 @@
apiVersion: v2
type: application
name: docs
version: 3.1.0
version: 3.0.0
appVersion: latest
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "3.1.0",
"version": "3.0.0",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {