Compare commits

...

65 Commits

Author SHA1 Message Date
Your name a4d968eff3 remise à niveau 2025-02-13 16:29:33 +01:00
Your name 12f7c8b371 after pull 2025-02-12 10:36:03 +01:00
MarineM1 70e3e9e7d4 Merge branch 'main' into accessibility 2025-02-12 10:02:25 +01:00
Anthony LC 296dbb7957 🔖(minor) release 2.2.0
Added:
- 📝(doc) Add security.md and codeofconduct.md
- (frontend) add home page
- (frontend) cursor display on activity
- (frontend) Add export page break

Changed:
- 🔧(backend) make AI feature reach configurable

Fixed:
- 🌐(CI) Fix email partially translated
- 🐛(frontend) fix cursor breakline
- 🐛(frontend) fix style pdf export
2025-02-11 14:16:58 +01:00
Anthony LC 3827f0f799 🩹(frontend) fine tuning v2.2.0
Fix minor issues:
- Add some height to the carret in the editor
- Improve css in mail templates
- Improve images resolution on homepage
2025-02-11 14:16:58 +01:00
Anthony LC d89e3dc6d4 🛂(frontend) display the AI buttons depend abilities
Anybody with edit right could use the AI.
We changed this behavior, now we have to be
authentified with edit right.
We update the UI to display the AI buttons
only if the user has the correct AI ability.
2025-02-11 10:54:55 +01:00
Samuel Paccoud - DINUM 91cf5f9367 🔧(backend) make AI feature reach configurable
We want to be able to define whether AI features are available to
anonymous users who gained editor access on a document, or if we
demand that they be authenticated or even if we demand that they
gained their editor access via a specific document access.

Being authenticated is now the default value. This will change the
default behavior on your existing instance (see UPGRADE.md)
2025-02-11 10:54:55 +01:00
Anthony LC 5cc4b07cf6 💄(frontend) improve caret style
Improve the caret, to looks more like the
google doc caret.
2025-02-10 15:53:03 +01:00
Anthony LC 0cfc242e09 🚚(frontend) move Blocknote styles
Move Blocknote styles to a separate file.
2025-02-10 15:53:03 +01:00
Anthony LC a6b3cfdb0c (frontend) add export page break
Blocknotejs introduced the ability to export a
document with page breaks.
This commit adds the page break feature to the
editor and so to our export feature.
2025-02-10 15:53:03 +01:00
Anthony LC 5ead18c94c 💬(frontend) add lacking buttons open source section
Some buttons were lacking in the open source
section of the home page. This commit adds them.
2025-02-10 15:40:23 +01:00
AntoLC 5eeb8cae5c 🌐(i18n) update translated strings
Update translated files with new translations
2025-02-10 15:40:23 +01:00
MarineM1 e0426ca803 Merge branch 'main' into accessibility 2025-02-10 14:17:20 +01:00
Jacques ROUSSEL 68bf024005 (helm) add pdbs to deployments
In order to avoid a service interruption during a Kubernetes (k8s)
upgrade, we add a Pod Disruption Budget (PDB) to deployments.
2025-02-10 13:05:54 +01:00
Anthony LC fdd1068c90 (frontend) fix test copy html
The recent upgrade of Blocknote has caused the
test to fail.
This commit updates the test to reflect
the new html structure.
2025-02-10 10:50:29 +01:00
Anthony LC ba695bf647 ⬇️(frontend) downgrade to @react-pdf/renderer to 4.1.6
The version 4.2.1 of @react-pdf/renderer
is not compatible @blocknote/xl-pdf-exporter
and @blocknote/xl-docx-exporter.
2025-02-10 10:50:29 +01:00
Anthony LC 27e7aec193 💄(frontend) improve styles export pdf
When exporting a document to PDF, the headings
spacings were too small, the break lines were
not displayed. This commit fixes these issues
by replacing the needed blocks.
2025-02-10 10:50:29 +01:00
Anthony LC 58b712a1de 🐛(frontend) fix cursor breakline
We had breakline issues with the initial
cursor because of some css properties.
We changed the cursor css to not take
any space in the lines,
avoiding the breakline issues.
We keep the new cursor visibility
feature (always, activity).
2025-02-10 10:50:29 +01:00
renovate[bot] 08f9036523 ⬆️(dependencies) update js dependencies 2025-02-10 10:50:29 +01:00
MarineM1 a01790dc0b Merge branch 'main' into accessibility 2025-02-10 08:35:03 +01:00
Anthony LC ebe3efc8f7 💄(frontend) update the favicon
Update the favicon with a better one.
2025-02-07 18:18:22 +01:00
Anthony LC 66fbf27913 🩹(frontend) fix PageLayout
The page layout was rendered behind the header,
which caused the top of the mention legales pages
to be hidden.
This commit fixes this issue.
2025-02-07 18:18:22 +01:00
Anthony LC 20e4a4e42a 🥚(frontend) easter egg in dev tools
Add a easter egg in the browser dev tools.
2025-02-07 18:18:22 +01:00
Anthony LC 1aa4844eeb 🎨(frontend) add dsfr proconnect homepage
If we are with the DSFR theme, we need to add the
proconnect button to the homepage.
We add an option in the cunningham theme to
display the proconnect section instead of the
opensource section.
2025-02-07 18:18:22 +01:00
Nathan Panchout 4bb9c092cb (frontend) add white label homepage
Add white label homepage with new assets and
components. When the user is not logged in,
the homepage will be displayed.
2025-02-07 18:18:22 +01:00
Anthony LC c493eb8924 ♻️(frontend) use a hook instead of a store for auth
We will use a hook instead of a store for the auth
feature. The hook will be powered by ReactQuery,
it will provide us fine-grained control over the
auth state and will be easier to use.
2025-02-07 18:18:22 +01:00
Anthony LC 40fdf97520 🚚(frontend) move auth to its own feature
We will move auth to its own feature to make it
easier to manage and to make it more modular.
2025-02-07 18:18:22 +01:00
Anthony LC 91b10e75dd 💄(email) fix line height email title
The line height of the email title was not
the correct size. We let the title managing
its own line height.
2025-02-07 17:05:49 +01:00
Anthony LC 7a6da10e1c 🐛(i18n) add back the missing email translations
We changed the way we upload the translations to
Crowdin, some translations were missing for the
email templates. We add them back and improve
the tests to make sure we don't forget them again.
2025-02-07 17:05:49 +01:00
Anthony LC 004e8ec645 🌐(CI) build mails when crowdin_upload workflow
When we were executing the crowdin_upload workflow,
we were not building the mail template to dispatch it
to the backend. It resulted in the mail not being
totally translated. This commit fixes that issue
by adding the build mail step to the crowdin_upload.
To do so, we added it to the dependencies workflow.
"dependencies" workflow is callable by other
workflows that need a specific job.
2025-02-07 17:05:49 +01:00
Your name e14622ff66 ️ (frontend) modal options modified
The modal for document options has been modified to allow navigation
using "tab" and to be closed.
2025-02-06 14:47:04 +01:00
Your name 75fd994a5b (frontend) Focus
The elements focused with "Tab" are visible and consistent with the
defined style.
2025-02-04 16:27:46 +01:00
Your name 239933aef3 Merge branch 'accessibility' of https://github.com/suitenumerique/docs into accessibility 2025-02-03 16:38:13 +01:00
Your name 95c06d68cb ️ (frontend) navigation with tab and other modifications
- In DocGrid, when navigating with tab, we can see where we are
- In Doc, don't read "doc title input" but "rename"
- In docSearch, don't read "search" icon and read "aucun document
  trouvé" with screen reader
2025-02-03 16:27:24 +01:00
MarineM1 0d12278474 Merge branch 'main' into accessibility 2025-02-03 10:58:02 +01:00
virgile-deville a1bca9c436 📝(doc) add security.md and conduct.md policies
We need a safe way for people to report vulnerabilities.
People now can go on SECURITY.md and follow our policy.

We want to have a policy for expected behaviour.
People can check out CODE_OF_CONDUCT.md.
2025-02-02 14:53:29 +01:00
Your name 8ce13075c1 (frontend) leftpanel and grid modified
- Read the Tooltip with the number of participants, not the icon
- In LeftPanel, when navigating with tab, we can see where we are
2025-01-31 16:47:37 +01:00
Your name 225f5a8abb erreur du commit précedent
je ne commite que les modifications annoncées dans le commit précédent
2025-01-31 13:23:05 +01:00
Your name 71a8765770 ️ (frontend) update after comments
-modifications after Sylvain's commentary
-modifications after Anto's commentaries
2025-01-31 11:42:32 +01:00
MarineM1 9186a101ec Merge branch 'main' into accessibility 2025-01-31 11:41:31 +01:00
Sylvain Zimmer d02fa1ddd4 ✏️(readme) fix a few typos
Found a few errors after proof-reading the README
2025-01-30 19:00:56 +01:00
MarineM1 74e816c479 Merge branch 'main' into accessibility 2025-01-30 16:14:09 +01:00
Your name 5d9eb2d694 mise en correspondance avec le main 2025-01-30 15:25:40 +01:00
Your name 33e168ba17 mise en correspondance avec le main 2025-01-30 14:49:13 +01:00
Your name f1c0f6bba0 ️ (frontend) pin icon translated and read
For good accessibility, the pinned icon will be read by the screen reader, but the simple icon will not
2025-01-30 14:26:29 +01:00
Anthony LC 1fd66d3081 🔖(minor) release 2.1.0
Added:
- (backend) add soft delete and restore API endpoints to documents
- (backend) allow organizing documents in a tree structure
- (backend) add "excerpt" field to document list serializer
- (backend) add github actions to manage Crowdin workflow
- 📈Integrate Posthog
- 🏷️(backend) add content-type to uploaded file
- (frontend) export pdf docx front side7

Changed:
- 💄(frontend) add abilities on doc row
- 💄(frontend) improve DocsGridItem responsive padding
- 🔧(backend) Bump maximum page size to 200
- 📝(doc) Improve Read me

Fixed:
- 🐛Fix invitations

Removed:
- 🔥(backend) remove "content" field from list serializer
2025-01-30 12:50:44 +01:00
AntoLC bae8c4c563 🌐(i18n) update translated strings
Update translated files with new translations
2025-01-30 11:37:05 +01:00
Anthony LC 0dae35dab1 ✏️(frontend) replace Word / Open Office by Docx
Replace the naming of the select export
options from Word / Open Office to Docx.
2025-01-30 11:37:05 +01:00
Your name 8f4fd15495 Merge branch 'accessibility' of https://github.com/suitenumerique/docs into accessibility 2025-01-30 09:36:00 +01:00
MarineM1 cc6ce4a945 Merge branch 'main' into accessibility 2025-01-30 09:02:51 +01:00
Anthony LC 929a50b573 ⬇️(frontend) downgrade cunningham to 2.9.4
The last version of Cunningham has problems.
Better to downgrade to the previous version.
We add cunningham library to renovate.json
to prevent future upgrade with Renovate.
An issue will be open to upgrade to the
last version manually.
2025-01-29 17:45:14 +01:00
Anthony LC 83dfd26d1c ⬇️(frontend) downgrade to react 18
We still have conflict with React 19, better to
downgrade to react 18 for the moment.
We add the react 18 libs to renovate.json
to prevent future upgrade with Renovate.
An issue will be open to upgrade to React 19
manually.
2025-01-29 17:45:14 +01:00
renovate[bot] addc6a331f ⬆️(dependencies) update js dependencies 2025-01-29 17:45:14 +01:00
MarineM1 044c8f0bbd Merge branch 'main' into accessibility 2025-01-29 13:18:04 +01:00
Your name 7e62dcf1fc Merge branch 'main' of https://github.com/suitenumerique/docs into accessibility 2025-01-29 13:10:44 +01:00
MarineM1 e7742d914c Merge branch 'main' into accessibility 2025-01-29 10:54:54 +01:00
Your name b1a5c17d75 ️(frontend) update some code html
To improve accessibility, certain parts of the code are modified
 with the help of a screen reader
2025-01-28 15:32:55 +01:00
Your name bd68396e52 commit suite pull du 28 01 2025 2025-01-28 10:32:11 +01:00
Your name 9f1ae58ead commit avant récup du code 2025-01-28 09:15:29 +01:00
Your name f597549e96 Accessibilité - modifications des boutons corporate, group et public pour le lecteur d'écran 2025-01-28 09:14:26 +01:00
Your name 847e120d67 commit après récupération des modif du main 2025-01-27 09:18:37 +01:00
Your name 6682ddafff accessibilité - icons apps,lock et group plus plues 2025-01-27 09:16:26 +01:00
Your name 2f23404003 Accessibilité - modif des boutons home et search 2025-01-22 11:18:24 +01:00
Your name 6d4210d34b essai de modificaation de la couleur des arrows + pull du 20/01/2025 2025-01-20 11:36:24 +01:00
Your name 85f7598be8 Accessibilité - Lecture du Logo supprimée car double 2025-01-16 15:21:38 +01:00
160 changed files with 11563 additions and 3375 deletions
+3 -2
View File
@@ -7,10 +7,11 @@ on:
- 'release/**'
jobs:
install-front:
uses: ./.github/workflows/front-dependencies-installation.yml
install-dependencies:
uses: ./.github/workflows/dependencies.yml
with:
node_version: '20.x'
with-front-dependencies-installation: true
synchronize-with-crowdin:
runs-on: ubuntu-latest
+12 -3
View File
@@ -7,13 +7,15 @@ on:
- main
jobs:
install-front:
uses: ./.github/workflows/front-dependencies-installation.yml
install-dependencies:
uses: ./.github/workflows/dependencies.yml
with:
node_version: '20.x'
with-front-dependencies-installation: true
with-build_mails: true
synchronize-with-crowdin:
needs: install-front
needs: install-dependencies
runs-on: ubuntu-latest
steps:
@@ -29,6 +31,13 @@ jobs:
- name: Install development dependencies
run: pip install --user .
working-directory: src/backend
- name: Restore the mail templates
uses: actions/cache@v4
id: mail-templates
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
fail-on-cache-miss: true
- name: Install gettext
run: |
sudo apt-get update
+85
View File
@@ -0,0 +1,85 @@
name: Dependency reusable workflow
on:
workflow_call:
inputs:
node_version:
required: false
default: '20.x'
type: string
with-front-dependencies-installation:
type: boolean
default: false
with-build_mails:
type: boolean
default: false
jobs:
front-dependencies-installation:
if: ${{ inputs.with-front-dependencies-installation == true }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Restore the frontend cache
uses: actions/cache@v4
id: front-node_modules
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
- name: Setup Node.js
if: steps.front-node_modules.outputs.cache-hit != 'true'
uses: actions/setup-node@v4
with:
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
- name: Cache install frontend
if: steps.front-node_modules.outputs.cache-hit != 'true'
uses: actions/cache@v4
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
build-mails:
if: ${{ inputs.with-build_mails == true }}
runs-on: ubuntu-latest
defaults:
run:
working-directory: src/mail
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Restore the mail templates
uses: actions/cache@v4
id: mail-templates
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
- name: Setup Node.js
if: steps.mail-templates.outputs.cache-hit != 'true'
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node_version }}
- name: Install yarn
if: steps.mail-templates.outputs.cache-hit != 'true'
run: npm install -g yarn
- name: Install node dependencies
if: steps.mail-templates.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
- name: Build mails
if: steps.mail-templates.outputs.cache-hit != 'true'
run: yarn build
- name: Cache mail templates
if: steps.mail-templates.outputs.cache-hit != 'true'
uses: actions/cache@v4
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
@@ -1,36 +0,0 @@
name: Install frontend installation reusable workflow
on:
workflow_call:
inputs:
node_version:
required: false
default: '20.x'
type: string
jobs:
front-dependencies-installation:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Restore the frontend cache
uses: actions/cache@v4
id: front-node_modules
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
- name: Setup Node.js
if: steps.front-node_modules.outputs.cache-hit != 'true'
uses: actions/setup-node@v4
with:
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
- name: Cache install frontend
if: steps.front-node_modules.outputs.cache-hit != 'true'
uses: actions/cache@v4
with:
path: "src/frontend/**/node_modules"
key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
+6 -5
View File
@@ -10,13 +10,14 @@ on:
jobs:
install-front:
uses: ./.github/workflows/front-dependencies-installation.yml
install-dependencies:
uses: ./.github/workflows/dependencies.yml
with:
node_version: '20.x'
with-front-dependencies-installation: true
test-front:
needs: install-front
needs: install-dependencies
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@@ -39,7 +40,7 @@ jobs:
lint-front:
runs-on: ubuntu-latest
needs: install-front
needs: install-dependencies
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -60,7 +61,7 @@ jobs:
test-e2e-chromium:
runs-on: ubuntu-latest
needs: install-front
needs: install-dependencies
timeout-minutes: 20
steps:
- name: Checkout repository
+7 -41
View File
@@ -9,6 +9,11 @@ on:
- "*"
jobs:
install-dependencies:
uses: ./.github/workflows/dependencies.yml
with:
with-build_mails: true
lint-git:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' # Makes sense only for pull requests
@@ -56,46 +61,6 @@ jobs:
exit 1
fi
build-mails:
runs-on: ubuntu-latest
defaults:
run:
working-directory: src/mail
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: "18"
- name: Restore the mail templates
uses: actions/cache@v4
id: mail-templates
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
- name: Install yarn
if: steps.mail-templates.outputs.cache-hit != 'true'
run: npm install -g yarn
- name: Install node dependencies
if: steps.mail-templates.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile
- name: Build mails
if: steps.mail-templates.outputs.cache-hit != 'true'
run: yarn build
- name: Cache mail templates
if: steps.mail-templates.outputs.cache-hit != 'true'
uses: actions/cache@v4
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
lint-back:
runs-on: ubuntu-latest
defaults:
@@ -121,7 +86,7 @@ jobs:
test-back:
runs-on: ubuntu-latest
needs: build-mails
needs: install-dependencies
defaults:
run:
@@ -169,6 +134,7 @@ jobs:
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
fail-on-cache-miss: true
- name: Start MinIO
run: |
+30 -3
View File
@@ -9,6 +9,28 @@ and this project adheres to
## [Unreleased]
## [2.2.0] - 2025-02-10
## Added
- 📝(doc) Add security.md and codeofconduct.md #604
- ✨(frontend) add home page #608
- ✨(frontend) cursor display on activity #609
- ✨(frontend) Add export page break #623
## Changed
- 🔧(backend) make AI feature reach configurable #628
## Fixed
- 🌐(CI) Fix email partially translated #616
- 🐛(frontend) fix cursor breakline #609
- 🐛(frontend) fix style pdf export #609
## [2.1.0] - 2025-01-29
## Added
- ✨(backend) add soft delete and restore API endpoints to documents #516
@@ -23,15 +45,18 @@ and this project adheres to
- 💄(frontend) add abilities on doc row #581
- 💄(frontend) improve DocsGridItem responsive padding #582
- 🔧(backend) Bump maximum page size to 200 #516
- 📝(doc) Improve Read me #558
## Changed
## Fixed
- 🔧(backend) Bump page size to 200 #516
- 🐛Fix invitations #575
## Removed
- 🔥(backend) remove "content" field from list serializer # 516
## [2.0.1] - 2025-01-17
## Fixed
@@ -386,7 +411,9 @@ and this project adheres to
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/numerique-gouv/impress/compare/v2.0.1...main
[unreleased]: https://github.com/numerique-gouv/impress/compare/v2.2.0...main
[v2.2.0]: https://github.com/numerique-gouv/impress/releases/v2.2.0
[v2.1.0]: https://github.com/numerique-gouv/impress/releases/v2.1.0
[v2.0.1]: https://github.com/numerique-gouv/impress/releases/v2.0.1
[v2.0.0]: https://github.com/numerique-gouv/impress/releases/v2.0.0
[v1.10.0]: https://github.com/numerique-gouv/impress/releases/v1.10.0
+75
View File
@@ -0,0 +1,75 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Enforcement Responsibilities
- Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
- Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
## Scope
- This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at docs@numerique.gouv.fr.
- All complaints will be reviewed and investigated promptly and fairly.
- All community leaders are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement Guidelines
- Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this
## Code of Conduct:
1. Correction
Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
2. Warning
Community Impact: A violation through a single incident or series of actions.
Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
3. Temporary Ban
Community Impact: A serious violation of community standards, including sustained inappropriate behavior.
Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
4. Permanent Ban
Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
Consequence: A permanent ban from any sort of public interaction within the community.
Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
Community Impact Guidelines were inspired by Mozilla's code of conduct enforcement ladder.
For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
+8 -7
View File
@@ -13,8 +13,10 @@ Welcome to Docs! The open source document editor where your notes can become kno
Chat on Matrix
</a> - <a href="/docs/">
Documentation
</a> - <a href="#getting-started">
</a> - <a href="#getting-started-">
Getting started
</a> - <a href="mailto:docs@numerique.gouv.fr">
Reach out
</a>
</p>
@@ -60,7 +62,7 @@ $ docker compose -v
docker compose version 1.27.4, build 40524192
```
> ⚠️ You may need to run the following commands with sudo but this can be avoided by assigning your user to the `docker` group.
> ⚠️ You may need to run the following commands with sudo but this can be avoided by adding your user to the `docker` group.
**Project bootstrap**
The easiest way to start working on the project is to use GNU Make:
@@ -71,7 +73,7 @@ $ make bootstrap FLUSH_ARGS='--no-input'
This command builds the `app` container, installs dependencies, performs database migrations and compile translations. It's a good idea to use this
command each time you are pulling code from the project repository to avoid dependency-releated or migration-releated issues.
command each time you are pulling code from the project repository to avoid dependency-related or migration-related issues.
Your Docker services should now be up and running 🎉
@@ -79,9 +81,8 @@ You can access to the project by going to <http://localhost:3000>.
You will be prompted to log in, the default credentials are:
```shellscript
```
username: impress
password: impress
```
@@ -172,11 +173,11 @@ docs
Docs is built on top of [Django Rest Framework](https://www.django-rest-framework.org/), [Next.js](https://nextjs.org/), [MinIO](https://min.io/), [BlockNote.js](https://www.blocknotejs.org/), [HocusPocus](https://tiptap.dev/docs/hocuspocus/introduction) and [Yjs](https://yjs.dev/)
### Gov ❤️ open source
Docs is the result of a joint effort lead by the French 🇫🇷🥖 ([DINUM](https://www.numerique.gouv.fr/dinum/)) and German 🇩🇪🥨 governments ([ZenDiS](https://zendis.de/)).
Docs is the result of a joint effort led by the French 🇫🇷🥖 ([DINUM](https://www.numerique.gouv.fr/dinum/)) and German 🇩🇪🥨 governments ([ZenDiS](https://zendis.de/)).
We are proud sponsors of [BlockNotejs](https://www.blocknotejs.org/) and [Yjs](https://yjs.dev/).
We are always looking for new public partners (we are currently onboarding the Netherlands 🇳🇱🧀), feel free to [reach out](https://matrix.to/#/#docs-official:matrix.org) if you are interested in using or contributing to Docs.
We are always looking for new public partners (we are currently onboarding the Netherlands 🇳🇱🧀), feel free to [reach out](mailto:docs@numerique.gouv.fr) if you are interested in using or contributing to Docs.
<p align="center">
<img src="/docs/assets/europe_opensource.png" width="50%"/>
+23
View File
@@ -0,0 +1,23 @@
# Security Policy
## Reporting a Vulnerability
Security is very important to us.
If you have any issue regarding security, please disclose the information responsibly submiting [this form](https://vdp.numerique.gouv.fr/p/Send-a-report?lang=en) and not by creating an issue on the repository. You can also email us at docs@numerique.gouv.fr
We appreciate your effort to make Docs more secure.
## Vulnerability disclosure policy
Working with security issues in an open source project can be challenging, as we are required to disclose potential problems that could be exploited by attackers. With this in mind, our security fix policy is as follows:
1. The Maintainers team will handle the fix as usual (Pull Request,
release).
2. In the release notes, we will include the identification numbers from the
GitHub Advisory Database (GHSA) and, if applicable, the Common Vulnerabilities
and Exposures (CVE) identifier for the vulnerability.
3. Once this grace period has passed, we will publish the vulnerability.
By adhering to this security policy, we aim to address security concerns
effectively and responsibly in our open source software project.
+5
View File
@@ -15,3 +15,8 @@ the following command inside your docker container:
(Note : in your development environment, you can `make migrate`.)
## [Unreleased]
- AI features are now limited to users who are authenticated. Before this release, even anonymous
users who gained editor access on a document with link reach used to get AI feature.
IF you want anonymous users to keep access on AI features, you must now define the
`AI_ALLOW_REACH_FROM` setting to "public".
+1 -1
View File
@@ -15,7 +15,7 @@ services:
- "1081:1080"
minio:
user: ${DOCKER_USER:-1000}
# user: ${DOCKER_USER:-1000}
image: minio/minio
environment:
- MINIO_ROOT_USER=impress
+3853
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
{
"dependencies": {
"@blocknote/core": "^0.23.4",
"next": "^15.1.7"
}
}
+6 -1
View File
@@ -14,10 +14,15 @@
"groupName": "ignored js dependencies",
"matchManagers": ["npm"],
"matchPackageNames": [
"@openfun/cunningham-react",
"@types/react",
"@types/react-dom",
"eslint",
"fetch-mock",
"node",
"node-fetch",
"eslint",
"react",
"react-dom",
"workbox-webpack-plugin"
]
}
+17 -2
View File
@@ -629,6 +629,9 @@ class Document(MP_Node, BaseModel):
# which date to allow them anyway)
# Anonymous users should also not see document accesses
has_access_role = bool(roles) and not is_deleted
can_update_from_access = (
is_owner_or_admin or RoleChoices.EDITOR in roles
) and not is_deleted
# Add roles provided by the document link, taking into account its ancestors
@@ -647,11 +650,23 @@ class Document(MP_Node, BaseModel):
is_owner_or_admin or RoleChoices.EDITOR in roles
) and not is_deleted
ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM
ai_access = any(
[
ai_allow_reach_from == LinkReachChoices.PUBLIC and can_update,
ai_allow_reach_from == LinkReachChoices.AUTHENTICATED
and user.is_authenticated
and can_update,
ai_allow_reach_from == LinkReachChoices.RESTRICTED
and can_update_from_access,
]
)
return {
"accesses_manage": is_owner_or_admin,
"accesses_view": has_access_role,
"ai_transform": can_update,
"ai_translate": can_update,
"ai_transform": ai_access,
"ai_translate": ai_access,
"attachment_upload": can_update,
"children_list": can_get,
"children_create": can_update and user.is_authenticated,
@@ -458,6 +458,10 @@ def test_api_document_invitations_create_email_from_content_language():
email_content = " ".join(email.body.split())
assert f"{user.full_name} a partagé un document avec vous!" in email_content
assert (
"Docs, votre nouvel outil incontournable pour organiser, partager et collaborer "
"sur vos documents en équipe." in email_content
)
def test_api_document_invitations_create_email_from_content_language_not_supported():
@@ -2,6 +2,7 @@
Test AI transform API endpoint for users in impress's core app.
"""
import random
from unittest.mock import MagicMock, patch
from django.core.cache import cache
@@ -31,6 +32,9 @@ def ai_settings():
yield
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"reach, role",
[
@@ -57,6 +61,7 @@ def test_api_documents_ai_transform_anonymous_forbidden(reach, role):
}
@override_settings(AI_ALLOW_REACH_FROM="public")
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_anonymous_success(mock_create):
@@ -93,6 +98,27 @@ def test_api_documents_ai_transform_anonymous_success(mock_create):
)
@override_settings(AI_ALLOW_REACH_FROM=random.choice(["authenticated", "restricted"]))
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_anonymous_limited_by_setting(mock_create):
"""
Anonymous users should be able to request AI transform to a document
if the link reach and role permit it.
"""
document = factories.DocumentFactory(link_reach="public", link_role="editor")
answer = '{"answer": "Salut"}'
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content=answer))]
)
url = f"/api/v1.0/documents/{document.id!s}/ai-transform/"
response = APIClient().post(url, {"text": "Hello", "action": "summarize"})
assert response.status_code == 401
@pytest.mark.parametrize(
"reach, role",
[
@@ -2,6 +2,7 @@
Test AI translate API endpoint for users in impress's core app.
"""
import random
from unittest.mock import MagicMock, patch
from django.core.cache import cache
@@ -51,6 +52,9 @@ def test_api_documents_ai_translate_viewset_options_metadata():
}
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"reach, role",
[
@@ -77,6 +81,7 @@ def test_api_documents_ai_translate_anonymous_forbidden(reach, role):
}
@override_settings(AI_ALLOW_REACH_FROM="public")
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_anonymous_success(mock_create):
@@ -113,6 +118,27 @@ def test_api_documents_ai_translate_anonymous_success(mock_create):
)
@override_settings(AI_ALLOW_REACH_FROM=random.choice(["authenticated", "restricted"]))
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_anonymous_limited_by_setting(mock_create):
"""
Anonymous users should be able to request AI translate to a document
if the link reach and role permit it.
"""
document = factories.DocumentFactory(link_reach="public", link_role="editor")
answer = '{"answer": "Salut"}'
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content=answer))]
)
url = f"/api/v1.0/documents/{document.id!s}/ai-translate/"
response = APIClient().post(url, {"text": "Hello", "language": "es"})
assert response.status_code == 401
@pytest.mark.parametrize(
"reach, role",
[
@@ -28,8 +28,8 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"abilities": {
"accesses_manage": False,
"accesses_view": False,
"ai_transform": document.link_role == "editor",
"ai_translate": document.link_role == "editor",
"ai_transform": False,
"ai_translate": False,
"attachment_upload": document.link_role == "editor",
"children_create": False,
"children_list": True,
@@ -84,8 +84,8 @@ def test_api_documents_retrieve_anonymous_public_parent():
"abilities": {
"accesses_manage": False,
"accesses_view": False,
"ai_transform": grand_parent.link_role == "editor",
"ai_translate": grand_parent.link_role == "editor",
"ai_transform": False,
"ai_translate": False,
"attachment_upload": grand_parent.link_role == "editor",
"children_create": False,
"children_list": True,
+77 -10
View File
@@ -12,6 +12,7 @@ from django.core import mail
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
from django.test.utils import override_settings
from django.utils import timezone
import pytest
@@ -124,6 +125,9 @@ def test_models_documents_soft_delete(depth):
# get_abilities
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"is_authenticated,reach,role",
[
@@ -175,6 +179,9 @@ def test_models_documents_get_abilities_forbidden(
assert document.get_abilities(user) == expected_abilities
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"is_authenticated,reach",
[
@@ -243,8 +250,8 @@ def test_models_documents_get_abilities_editor(
expected_abilities = {
"accesses_manage": False,
"accesses_view": False,
"ai_transform": True,
"ai_translate": True,
"ai_transform": is_authenticated,
"ai_translate": is_authenticated,
"attachment_upload": True,
"children_create": is_authenticated,
"children_list": True,
@@ -271,6 +278,9 @@ def test_models_documents_get_abilities_editor(
assert all(value is False for value in document.get_abilities(user).values())
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
def test_models_documents_get_abilities_owner(django_assert_num_queries):
"""Check abilities returned for the owner of a document."""
user = factories.UserFactory()
@@ -300,12 +310,16 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
}
with django_assert_num_queries(1):
assert document.get_abilities(user) == expected_abilities
document.soft_delete()
document.refresh_from_db()
expected_abilities["move"] = False
assert document.get_abilities(user) == expected_abilities
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
def test_models_documents_get_abilities_administrator(django_assert_num_queries):
"""Check abilities returned for the administrator of a document."""
user = factories.UserFactory()
@@ -335,11 +349,15 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
}
with django_assert_num_queries(1):
assert document.get_abilities(user) == expected_abilities
document.soft_delete()
document.refresh_from_db()
assert all(value is False for value in document.get_abilities(user).values())
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
"""Check abilities returned for the editor of a document."""
user = factories.UserFactory()
@@ -369,23 +387,31 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
}
with django_assert_num_queries(1):
assert document.get_abilities(user) == expected_abilities
document.soft_delete()
document.refresh_from_db()
assert all(value is False for value in document.get_abilities(user).values())
def test_models_documents_get_abilities_reader_user(django_assert_num_queries):
@pytest.mark.parametrize("ai_access_setting", ["public", "authenticated", "restricted"])
def test_models_documents_get_abilities_reader_user(
ai_access_setting, django_assert_num_queries
):
"""Check abilities returned for the reader of a document."""
user = factories.UserFactory()
document = factories.DocumentFactory(users=[(user, "reader")])
access_from_link = (
document.link_reach != "restricted" and document.link_role == "editor"
)
expected_abilities = {
"accesses_manage": False,
"accesses_view": True,
"ai_transform": access_from_link,
"ai_translate": access_from_link,
# If you get your editor rights from the link role and not your access role
# You should not access AI if it's restricted to users with specific access
"ai_transform": access_from_link and ai_access_setting != "restricted",
"ai_translate": access_from_link and ai_access_setting != "restricted",
"attachment_upload": access_from_link,
"children_create": access_from_link,
"children_list": True,
@@ -404,11 +430,14 @@ def test_models_documents_get_abilities_reader_user(django_assert_num_queries):
"versions_list": True,
"versions_retrieve": True,
}
with django_assert_num_queries(1):
assert document.get_abilities(user) == expected_abilities
document.soft_delete()
document.refresh_from_db()
assert all(value is False for value in document.get_abilities(user).values())
with override_settings(AI_ALLOW_REACH_FROM=ai_access_setting):
with django_assert_num_queries(1):
assert document.get_abilities(user) == expected_abilities
document.soft_delete()
document.refresh_from_db()
assert all(value is False for value in document.get_abilities(user).values())
def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
@@ -446,6 +475,44 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
}
@override_settings(AI_ALLOW_REACH_FROM="public")
@pytest.mark.parametrize(
"is_authenticated,reach",
[
(True, "public"),
(False, "public"),
(True, "authenticated"),
],
)
def test_models_document_get_abilities_ai_access_authenticated(is_authenticated, reach):
"""Validate AI abilities when AI is available to any anonymous user with editor rights."""
user = factories.UserFactory() if is_authenticated else AnonymousUser()
document = factories.DocumentFactory(link_reach=reach, link_role="editor")
abilities = document.get_abilities(user)
assert abilities["ai_transform"] is True
assert abilities["ai_translate"] is True
@override_settings(AI_ALLOW_REACH_FROM="authenticated")
@pytest.mark.parametrize(
"is_authenticated,reach",
[
(True, "public"),
(False, "public"),
(True, "authenticated"),
],
)
def test_models_document_get_abilities_ai_access_public(is_authenticated, reach):
"""Validate AI abilities when AI is available only to authenticated users with editor rights."""
user = factories.UserFactory() if is_authenticated else AnonymousUser()
document = factories.DocumentFactory(link_reach=reach, link_role="editor")
abilities = document.get_abilities(user)
assert abilities["ai_transform"] == is_authenticated
assert abilities["ai_translate"] == is_authenticated
def test_models_documents_get_versions_slice_pagination(settings):
"""
The "get_versions_slice" method should allow navigating all versions of
+6 -1
View File
@@ -516,7 +516,12 @@ class Base(Configuration):
AI_API_KEY = values.Value(None, environ_name="AI_API_KEY", environ_prefix=None)
AI_BASE_URL = values.Value(None, environ_name="AI_BASE_URL", environ_prefix=None)
AI_MODEL = values.Value(None, environ_name="AI_MODEL", environ_prefix=None)
AI_ALLOW_REACH_FROM = values.Value(
choices=("public", "authenticated", "restricted"),
default="authenticated",
environ_name="AI_ALLOW_REACH_FROM",
environ_prefix=None,
)
AI_DOCUMENT_RATE_THROTTLE_RATES = {
"minute": 5,
"hour": 100,
+141 -79
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-15 21:00+0000\n"
"PO-Revision-Date: 2025-01-27 09:27\n"
"POT-Creation-Date: 2025-02-06 15:30+0000\n"
"PO-Revision-Date: 2025-02-10 14:14\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -17,18 +17,23 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr "Persönliche Daten"
#: build/lib/core/admin.py:46 core/admin.py:46
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Berechtigungen"
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Wichtige Daten"
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr ""
#: build/lib/core/api/filters.py:16 core/api/filters.py:16
msgid "Creator is me"
msgstr "Ersteller bin ich"
@@ -41,23 +46,23 @@ msgstr "Favorit"
msgid "Title"
msgstr "Titel"
#: build/lib/core/api/serializers.py:317 core/api/serializers.py:317
#: build/lib/core/api/serializers.py:346 core/api/serializers.py:346
msgid "A new document was created on your behalf!"
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
#: build/lib/core/api/serializers.py:321 core/api/serializers.py:321
#: build/lib/core/api/serializers.py:350 core/api/serializers.py:350
msgid "You have been granted ownership of a new document:"
msgstr "Sie sind Besitzer eines neuen Dokuments:"
#: build/lib/core/api/serializers.py:422 core/api/serializers.py:422
#: build/lib/core/api/serializers.py:453 core/api/serializers.py:453
msgid "Body"
msgstr "Inhalt"
#: build/lib/core/api/serializers.py:425 core/api/serializers.py:425
#: build/lib/core/api/serializers.py:456 core/api/serializers.py:456
msgid "Body type"
msgstr "Typ"
#: build/lib/core/api/serializers.py:431 core/api/serializers.py:431
#: build/lib/core/api/serializers.py:462 core/api/serializers.py:462
msgid "Format"
msgstr ""
@@ -71,270 +76,306 @@ msgstr "Ungültiges Antwortformat oder Token-Verifizierung fehlgeschlagen"
msgid "User account is disabled"
msgstr "Benutzerkonto ist deaktiviert"
#: build/lib/core/models.py:63 build/lib/core/models.py:70 core/models.py:63
#: core/models.py:70
#: build/lib/core/enums.py:19 core/enums.py:19
msgid "First child"
msgstr ""
#: build/lib/core/enums.py:20 core/enums.py:20
msgid "Last child"
msgstr ""
#: build/lib/core/enums.py:21 core/enums.py:21
msgid "First sibling"
msgstr ""
#: build/lib/core/enums.py:22 core/enums.py:22
msgid "Last sibling"
msgstr ""
#: build/lib/core/enums.py:23 core/enums.py:23
msgid "Left"
msgstr ""
#: build/lib/core/enums.py:24 core/enums.py:24
msgid "Right"
msgstr ""
#: build/lib/core/models.py:54 build/lib/core/models.py:61 core/models.py:54
#: core/models.py:61
msgid "Reader"
msgstr "Lesen"
#: build/lib/core/models.py:64 build/lib/core/models.py:71 core/models.py:64
#: core/models.py:71
#: build/lib/core/models.py:55 build/lib/core/models.py:62 core/models.py:55
#: core/models.py:62
msgid "Editor"
msgstr "Bearbeiten"
#: build/lib/core/models.py:72 core/models.py:72
#: build/lib/core/models.py:63 core/models.py:63
msgid "Administrator"
msgstr ""
#: build/lib/core/models.py:73 core/models.py:73
#: build/lib/core/models.py:64 core/models.py:64
msgid "Owner"
msgstr "Besitzer"
#: build/lib/core/models.py:84 core/models.py:84
#: build/lib/core/models.py:75 core/models.py:75
msgid "Restricted"
msgstr "Beschränkt"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:79 core/models.py:79
msgid "Authenticated"
msgstr "Authentifiziert"
#: build/lib/core/models.py:90 core/models.py:90
#: build/lib/core/models.py:81 core/models.py:81
msgid "Public"
msgstr "Öffentlich"
#: build/lib/core/models.py:112 core/models.py:112
#: build/lib/core/models.py:103 core/models.py:103
msgid "id"
msgstr ""
#: build/lib/core/models.py:113 core/models.py:113
#: build/lib/core/models.py:104 core/models.py:104
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:119 core/models.py:119
#: build/lib/core/models.py:110 core/models.py:110
msgid "created on"
msgstr "Erstellt"
#: build/lib/core/models.py:120 core/models.py:120
#: build/lib/core/models.py:111 core/models.py:111
msgid "date and time at which a record was created"
msgstr "Datum und Uhrzeit, an dem ein Datensatz erstellt wurde"
#: build/lib/core/models.py:125 core/models.py:125
#: build/lib/core/models.py:116 core/models.py:116
msgid "updated on"
msgstr "Aktualisiert"
#: build/lib/core/models.py:126 core/models.py:126
#: build/lib/core/models.py:117 core/models.py:117
msgid "date and time at which a record was last updated"
msgstr "Datum und Uhrzeit, an dem zuletzt aktualisiert wurde"
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:153 core/models.py:153
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:175 core/models.py:175
#: build/lib/core/models.py:166 core/models.py:166
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr "Geben Sie eine gültige Unterseite ein. Dieser Wert darf nur Buchstaben, Zahlen und die @/./+/-/_/: Zeichen enthalten."
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:172 core/models.py:172
msgid "sub"
msgstr "unter"
#: build/lib/core/models.py:183 core/models.py:183
#: build/lib/core/models.py:174 core/models.py:174
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr "Erforderlich. 255 Zeichen oder weniger. Buchstaben, Zahlen und die Zeichen @/./+/-/_/:"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:183 core/models.py:183
msgid "full name"
msgstr "Name"
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:184 core/models.py:184
msgid "short name"
msgstr "Kurzbezeichnung"
#: build/lib/core/models.py:195 core/models.py:195
#: build/lib/core/models.py:186 core/models.py:186
msgid "identity email address"
msgstr "Identitäts-E-Mail-Adresse"
#: build/lib/core/models.py:200 core/models.py:200
#: build/lib/core/models.py:191 core/models.py:191
msgid "admin email address"
msgstr "Admin E-Mail-Adresse"
#: build/lib/core/models.py:207 core/models.py:207
#: build/lib/core/models.py:198 core/models.py:198
msgid "language"
msgstr "Sprache"
#: build/lib/core/models.py:208 core/models.py:208
#: build/lib/core/models.py:199 core/models.py:199
msgid "The language in which the user wants to see the interface."
msgstr "Die Sprache, in der der Benutzer die Benutzeroberfläche sehen möchte."
#: build/lib/core/models.py:214 core/models.py:214
#: build/lib/core/models.py:205 core/models.py:205
msgid "The timezone in which the user wants to see times."
msgstr "Die Zeitzone, in der der Nutzer Zeiten sehen möchte."
#: build/lib/core/models.py:217 core/models.py:217
#: build/lib/core/models.py:208 core/models.py:208
msgid "device"
msgstr "Gerät"
#: build/lib/core/models.py:219 core/models.py:219
#: build/lib/core/models.py:210 core/models.py:210
msgid "Whether the user is a device or a real user."
msgstr "Ob der Benutzer ein Gerät oder ein echter Benutzer ist."
#: build/lib/core/models.py:222 core/models.py:222
#: build/lib/core/models.py:213 core/models.py:213
msgid "staff status"
msgstr "Status des Teammitgliedes"
#: build/lib/core/models.py:224 core/models.py:224
#: build/lib/core/models.py:215 core/models.py:215
msgid "Whether the user can log into this admin site."
msgstr "Gibt an, ob der Benutzer sich in diese Admin-Seite einloggen kann."
#: build/lib/core/models.py:227 core/models.py:227
#: build/lib/core/models.py:218 core/models.py:218
msgid "active"
msgstr "aktiviert"
#: build/lib/core/models.py:230 core/models.py:230
#: build/lib/core/models.py:221 core/models.py:221
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Ob dieser Benutzer als aktiviert behandelt werden soll. Deaktivieren Sie diese Option, anstatt Konten zu löschen."
#: build/lib/core/models.py:242 core/models.py:242
#: build/lib/core/models.py:233 core/models.py:233
msgid "user"
msgstr "Benutzer"
#: build/lib/core/models.py:243 core/models.py:243
#: build/lib/core/models.py:234 core/models.py:234
msgid "users"
msgstr "Benutzer"
#: build/lib/core/models.py:382 build/lib/core/models.py:758 core/models.py:382
#: core/models.py:758
#: build/lib/core/models.py:373 build/lib/core/models.py:925 core/models.py:373
#: core/models.py:925
msgid "title"
msgstr "Titel"
#: build/lib/core/models.py:404 core/models.py:404
#: build/lib/core/models.py:374 core/models.py:374
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:405 core/models.py:405
msgid "Document"
msgstr "Dokument"
#: build/lib/core/models.py:405 core/models.py:405
#: build/lib/core/models.py:406 core/models.py:406
msgid "Documents"
msgstr "Dokumente"
#: build/lib/core/models.py:408 core/models.py:408
#: build/lib/core/models.py:418 core/models.py:418
msgid "Untitled Document"
msgstr "Unbenanntes Dokument"
#: build/lib/core/models.py:633 core/models.py:633
#: build/lib/core/models.py:719 core/models.py:719
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
#: build/lib/core/models.py:637 core/models.py:637
#: build/lib/core/models.py:723 core/models.py:723
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} hat Sie mit der Rolle \"{role}\" zu folgendem Dokument eingeladen:"
#: build/lib/core/models.py:640 core/models.py:640
#: build/lib/core/models.py:726 core/models.py:726
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} hat ein Dokument mit Ihnen geteilt: {title}"
#: build/lib/core/models.py:663 core/models.py:663
#: build/lib/core/models.py:762 core/models.py:762
msgid "This document is not deleted."
msgstr ""
#: build/lib/core/models.py:769 core/models.py:769
msgid "This document was permanently deleted and cannot be restored."
msgstr ""
#: build/lib/core/models.py:820 core/models.py:820
msgid "Document/user link trace"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:664 core/models.py:664
#: build/lib/core/models.py:821 core/models.py:821
msgid "Document/user link traces"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:670 core/models.py:670
#: build/lib/core/models.py:827 core/models.py:827
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:693 core/models.py:693
#: build/lib/core/models.py:850 core/models.py:850
msgid "Document favorite"
msgstr "Dokumentenfavorit"
#: build/lib/core/models.py:694 core/models.py:694
#: build/lib/core/models.py:851 core/models.py:851
msgid "Document favorites"
msgstr "Dokumentfavoriten"
#: build/lib/core/models.py:700 core/models.py:700
#: build/lib/core/models.py:857 core/models.py:857
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Dieses Dokument ist bereits durch den gleichen Benutzer favorisiert worden."
#: build/lib/core/models.py:722 core/models.py:722
#: build/lib/core/models.py:879 core/models.py:879
msgid "Document/user relation"
msgstr "Dokument/Benutzerbeziehung"
#: build/lib/core/models.py:723 core/models.py:723
#: build/lib/core/models.py:880 core/models.py:880
msgid "Document/user relations"
msgstr "Dokument/Benutzerbeziehungen"
#: build/lib/core/models.py:729 core/models.py:729
#: build/lib/core/models.py:886 core/models.py:886
msgid "This user is already in this document."
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:735 core/models.py:735
#: build/lib/core/models.py:892 core/models.py:892
msgid "This team is already in this document."
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:741 build/lib/core/models.py:930 core/models.py:741
#: core/models.py:930
#: build/lib/core/models.py:898 build/lib/core/models.py:1012
#: core/models.py:898 core/models.py:1012
msgid "Either user or team must be set, not both."
msgstr "Benutzer oder Team müssen gesetzt werden, nicht beides."
#: build/lib/core/models.py:759 core/models.py:759
#: build/lib/core/models.py:926 core/models.py:926
msgid "description"
msgstr "Beschreibung"
#: build/lib/core/models.py:760 core/models.py:760
#: build/lib/core/models.py:927 core/models.py:927
msgid "code"
msgstr "Code"
#: build/lib/core/models.py:761 core/models.py:761
#: build/lib/core/models.py:928 core/models.py:928
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:763 core/models.py:763
#: build/lib/core/models.py:930 core/models.py:930
msgid "public"
msgstr "öffentlich"
#: build/lib/core/models.py:765 core/models.py:765
#: build/lib/core/models.py:932 core/models.py:932
msgid "Whether this template is public for anyone to use."
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
#: build/lib/core/models.py:771 core/models.py:771
#: build/lib/core/models.py:938 core/models.py:938
msgid "Template"
msgstr "Vorlage"
#: build/lib/core/models.py:772 core/models.py:772
#: build/lib/core/models.py:939 core/models.py:939
msgid "Templates"
msgstr "Vorlagen"
#: build/lib/core/models.py:911 core/models.py:911
#: build/lib/core/models.py:993 core/models.py:993
msgid "Template/user relation"
msgstr "Vorlage/Benutzer-Beziehung"
#: build/lib/core/models.py:912 core/models.py:912
#: build/lib/core/models.py:994 core/models.py:994
msgid "Template/user relations"
msgstr "Vorlage/Benutzerbeziehungen"
#: build/lib/core/models.py:918 core/models.py:918
#: build/lib/core/models.py:1000 core/models.py:1000
msgid "This user is already in this template."
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
#: build/lib/core/models.py:924 core/models.py:924
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "This team is already in this template."
msgstr "Dieses Team ist bereits in diesem Template."
#: build/lib/core/models.py:947 core/models.py:947
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:1048 core/models.py:1048
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:1049 core/models.py:1049
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:987 core/models.py:987
#: build/lib/core/models.py:1069 core/models.py:1069
msgid "This email is already associated to a registered user."
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
@@ -350,3 +391,24 @@ msgstr "Französisch"
msgid "German"
msgstr "Deutsch"
#: core/templates/mail/html/invitation.html:162
#: core/templates/mail/text/invitation.txt:3
msgid "Logo email"
msgstr "Logo-E-Mail"
#: core/templates/mail/html/invitation.html:209
#: core/templates/mail/text/invitation.txt:10
msgid "Open"
msgstr "Öffnen"
#: core/templates/mail/html/invitation.html:226
#: core/templates/mail/text/invitation.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs, Ihr neues unentbehrliches Werkzeug für die Organisation, den Austausch und die Zusammenarbeit in Ihren Dokumenten als Team. "
#: core/templates/mail/html/invitation.html:233
#: core/templates/mail/text/invitation.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr " Erstellt von %(brandname)s "
+141 -79
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-15 21:00+0000\n"
"PO-Revision-Date: 2025-01-27 09:27\n"
"POT-Creation-Date: 2025-02-06 15:30+0000\n"
"PO-Revision-Date: 2025-02-10 14:14\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -17,18 +17,23 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr ""
#: build/lib/core/admin.py:46 core/admin.py:46
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr ""
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr ""
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr ""
#: build/lib/core/api/filters.py:16 core/api/filters.py:16
msgid "Creator is me"
msgstr ""
@@ -41,23 +46,23 @@ msgstr ""
msgid "Title"
msgstr ""
#: build/lib/core/api/serializers.py:317 core/api/serializers.py:317
#: build/lib/core/api/serializers.py:346 core/api/serializers.py:346
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:321 core/api/serializers.py:321
#: build/lib/core/api/serializers.py:350 core/api/serializers.py:350
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:422 core/api/serializers.py:422
#: build/lib/core/api/serializers.py:453 core/api/serializers.py:453
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:425 core/api/serializers.py:425
#: build/lib/core/api/serializers.py:456 core/api/serializers.py:456
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:431 core/api/serializers.py:431
#: build/lib/core/api/serializers.py:462 core/api/serializers.py:462
msgid "Format"
msgstr ""
@@ -71,270 +76,306 @@ msgstr ""
msgid "User account is disabled"
msgstr ""
#: build/lib/core/models.py:63 build/lib/core/models.py:70 core/models.py:63
#: core/models.py:70
#: build/lib/core/enums.py:19 core/enums.py:19
msgid "First child"
msgstr ""
#: build/lib/core/enums.py:20 core/enums.py:20
msgid "Last child"
msgstr ""
#: build/lib/core/enums.py:21 core/enums.py:21
msgid "First sibling"
msgstr ""
#: build/lib/core/enums.py:22 core/enums.py:22
msgid "Last sibling"
msgstr ""
#: build/lib/core/enums.py:23 core/enums.py:23
msgid "Left"
msgstr ""
#: build/lib/core/enums.py:24 core/enums.py:24
msgid "Right"
msgstr ""
#: build/lib/core/models.py:54 build/lib/core/models.py:61 core/models.py:54
#: core/models.py:61
msgid "Reader"
msgstr ""
#: build/lib/core/models.py:64 build/lib/core/models.py:71 core/models.py:64
#: core/models.py:71
#: build/lib/core/models.py:55 build/lib/core/models.py:62 core/models.py:55
#: core/models.py:62
msgid "Editor"
msgstr ""
#: build/lib/core/models.py:72 core/models.py:72
#: build/lib/core/models.py:63 core/models.py:63
msgid "Administrator"
msgstr ""
#: build/lib/core/models.py:73 core/models.py:73
#: build/lib/core/models.py:64 core/models.py:64
msgid "Owner"
msgstr ""
#: build/lib/core/models.py:84 core/models.py:84
#: build/lib/core/models.py:75 core/models.py:75
msgid "Restricted"
msgstr ""
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:79 core/models.py:79
msgid "Authenticated"
msgstr ""
#: build/lib/core/models.py:90 core/models.py:90
#: build/lib/core/models.py:81 core/models.py:81
msgid "Public"
msgstr ""
#: build/lib/core/models.py:112 core/models.py:112
#: build/lib/core/models.py:103 core/models.py:103
msgid "id"
msgstr ""
#: build/lib/core/models.py:113 core/models.py:113
#: build/lib/core/models.py:104 core/models.py:104
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:119 core/models.py:119
#: build/lib/core/models.py:110 core/models.py:110
msgid "created on"
msgstr ""
#: build/lib/core/models.py:120 core/models.py:120
#: build/lib/core/models.py:111 core/models.py:111
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:125 core/models.py:125
#: build/lib/core/models.py:116 core/models.py:116
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:126 core/models.py:126
#: build/lib/core/models.py:117 core/models.py:117
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:153 core/models.py:153
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:175 core/models.py:175
#: build/lib/core/models.py:166 core/models.py:166
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:172 core/models.py:172
msgid "sub"
msgstr ""
#: build/lib/core/models.py:183 core/models.py:183
#: build/lib/core/models.py:174 core/models.py:174
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:183 core/models.py:183
msgid "full name"
msgstr ""
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:184 core/models.py:184
msgid "short name"
msgstr ""
#: build/lib/core/models.py:195 core/models.py:195
#: build/lib/core/models.py:186 core/models.py:186
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:200 core/models.py:200
#: build/lib/core/models.py:191 core/models.py:191
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:207 core/models.py:207
#: build/lib/core/models.py:198 core/models.py:198
msgid "language"
msgstr ""
#: build/lib/core/models.py:208 core/models.py:208
#: build/lib/core/models.py:199 core/models.py:199
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:214 core/models.py:214
#: build/lib/core/models.py:205 core/models.py:205
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:217 core/models.py:217
#: build/lib/core/models.py:208 core/models.py:208
msgid "device"
msgstr ""
#: build/lib/core/models.py:219 core/models.py:219
#: build/lib/core/models.py:210 core/models.py:210
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:222 core/models.py:222
#: build/lib/core/models.py:213 core/models.py:213
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:224 core/models.py:224
#: build/lib/core/models.py:215 core/models.py:215
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:227 core/models.py:227
#: build/lib/core/models.py:218 core/models.py:218
msgid "active"
msgstr ""
#: build/lib/core/models.py:230 core/models.py:230
#: build/lib/core/models.py:221 core/models.py:221
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:242 core/models.py:242
#: build/lib/core/models.py:233 core/models.py:233
msgid "user"
msgstr ""
#: build/lib/core/models.py:243 core/models.py:243
#: build/lib/core/models.py:234 core/models.py:234
msgid "users"
msgstr ""
#: build/lib/core/models.py:382 build/lib/core/models.py:758 core/models.py:382
#: core/models.py:758
#: build/lib/core/models.py:373 build/lib/core/models.py:925 core/models.py:373
#: core/models.py:925
msgid "title"
msgstr ""
#: build/lib/core/models.py:404 core/models.py:404
msgid "Document"
#: build/lib/core/models.py:374 core/models.py:374
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:405 core/models.py:405
msgid "Document"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:408 core/models.py:408
#: build/lib/core/models.py:418 core/models.py:418
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:633 core/models.py:633
#: build/lib/core/models.py:719 core/models.py:719
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:637 core/models.py:637
#: build/lib/core/models.py:723 core/models.py:723
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:640 core/models.py:640
#: build/lib/core/models.py:726 core/models.py:726
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:663 core/models.py:663
#: build/lib/core/models.py:762 core/models.py:762
msgid "This document is not deleted."
msgstr ""
#: build/lib/core/models.py:769 core/models.py:769
msgid "This document was permanently deleted and cannot be restored."
msgstr ""
#: build/lib/core/models.py:820 core/models.py:820
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:664 core/models.py:664
#: build/lib/core/models.py:821 core/models.py:821
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:670 core/models.py:670
#: build/lib/core/models.py:827 core/models.py:827
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:693 core/models.py:693
#: build/lib/core/models.py:850 core/models.py:850
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:694 core/models.py:694
#: build/lib/core/models.py:851 core/models.py:851
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:700 core/models.py:700
#: build/lib/core/models.py:857 core/models.py:857
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
#: build/lib/core/models.py:879 core/models.py:879
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:723 core/models.py:723
#: build/lib/core/models.py:880 core/models.py:880
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:729 core/models.py:729
#: build/lib/core/models.py:886 core/models.py:886
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:735 core/models.py:735
#: build/lib/core/models.py:892 core/models.py:892
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:741 build/lib/core/models.py:930 core/models.py:741
#: core/models.py:930
#: build/lib/core/models.py:898 build/lib/core/models.py:1012
#: core/models.py:898 core/models.py:1012
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:759 core/models.py:759
#: build/lib/core/models.py:926 core/models.py:926
msgid "description"
msgstr ""
#: build/lib/core/models.py:760 core/models.py:760
#: build/lib/core/models.py:927 core/models.py:927
msgid "code"
msgstr ""
#: build/lib/core/models.py:761 core/models.py:761
#: build/lib/core/models.py:928 core/models.py:928
msgid "css"
msgstr ""
#: build/lib/core/models.py:763 core/models.py:763
#: build/lib/core/models.py:930 core/models.py:930
msgid "public"
msgstr ""
#: build/lib/core/models.py:765 core/models.py:765
#: build/lib/core/models.py:932 core/models.py:932
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:771 core/models.py:771
#: build/lib/core/models.py:938 core/models.py:938
msgid "Template"
msgstr ""
#: build/lib/core/models.py:772 core/models.py:772
#: build/lib/core/models.py:939 core/models.py:939
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:911 core/models.py:911
#: build/lib/core/models.py:993 core/models.py:993
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:912 core/models.py:912
#: build/lib/core/models.py:994 core/models.py:994
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:918 core/models.py:918
#: build/lib/core/models.py:1000 core/models.py:1000
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:924 core/models.py:924
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:947 core/models.py:947
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "email address"
msgstr ""
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:1048 core/models.py:1048
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:1049 core/models.py:1049
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:987 core/models.py:987
#: build/lib/core/models.py:1069 core/models.py:1069
msgid "This email is already associated to a registered user."
msgstr ""
@@ -350,3 +391,24 @@ msgstr ""
msgid "German"
msgstr ""
#: core/templates/mail/html/invitation.html:162
#: core/templates/mail/text/invitation.txt:3
msgid "Logo email"
msgstr ""
#: core/templates/mail/html/invitation.html:209
#: core/templates/mail/text/invitation.txt:10
msgid "Open"
msgstr ""
#: core/templates/mail/html/invitation.html:226
#: core/templates/mail/text/invitation.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/invitation.html:233
#: core/templates/mail/text/invitation.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr ""
+141 -79
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-15 21:00+0000\n"
"PO-Revision-Date: 2025-01-27 09:27\n"
"POT-Creation-Date: 2025-02-06 15:30+0000\n"
"PO-Revision-Date: 2025-02-10 14:14\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -17,18 +17,23 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr "Infos Personnelles"
#: build/lib/core/admin.py:46 core/admin.py:46
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr ""
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Dates importantes"
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr ""
#: build/lib/core/api/filters.py:16 core/api/filters.py:16
msgid "Creator is me"
msgstr ""
@@ -41,23 +46,23 @@ msgstr ""
msgid "Title"
msgstr ""
#: build/lib/core/api/serializers.py:317 core/api/serializers.py:317
#: build/lib/core/api/serializers.py:346 core/api/serializers.py:346
msgid "A new document was created on your behalf!"
msgstr "Un nouveau document a été créé pour vous !"
#: build/lib/core/api/serializers.py:321 core/api/serializers.py:321
#: build/lib/core/api/serializers.py:350 core/api/serializers.py:350
msgid "You have been granted ownership of a new document:"
msgstr "Vous avez été déclaré propriétaire d'un nouveau document :"
#: build/lib/core/api/serializers.py:422 core/api/serializers.py:422
#: build/lib/core/api/serializers.py:453 core/api/serializers.py:453
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:425 core/api/serializers.py:425
#: build/lib/core/api/serializers.py:456 core/api/serializers.py:456
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:431 core/api/serializers.py:431
#: build/lib/core/api/serializers.py:462 core/api/serializers.py:462
msgid "Format"
msgstr ""
@@ -71,270 +76,306 @@ msgstr ""
msgid "User account is disabled"
msgstr ""
#: build/lib/core/models.py:63 build/lib/core/models.py:70 core/models.py:63
#: core/models.py:70
#: build/lib/core/enums.py:19 core/enums.py:19
msgid "First child"
msgstr ""
#: build/lib/core/enums.py:20 core/enums.py:20
msgid "Last child"
msgstr ""
#: build/lib/core/enums.py:21 core/enums.py:21
msgid "First sibling"
msgstr ""
#: build/lib/core/enums.py:22 core/enums.py:22
msgid "Last sibling"
msgstr ""
#: build/lib/core/enums.py:23 core/enums.py:23
msgid "Left"
msgstr ""
#: build/lib/core/enums.py:24 core/enums.py:24
msgid "Right"
msgstr ""
#: build/lib/core/models.py:54 build/lib/core/models.py:61 core/models.py:54
#: core/models.py:61
msgid "Reader"
msgstr "Lecteur"
#: build/lib/core/models.py:64 build/lib/core/models.py:71 core/models.py:64
#: core/models.py:71
#: build/lib/core/models.py:55 build/lib/core/models.py:62 core/models.py:55
#: core/models.py:62
msgid "Editor"
msgstr "Éditeur"
#: build/lib/core/models.py:72 core/models.py:72
#: build/lib/core/models.py:63 core/models.py:63
msgid "Administrator"
msgstr "Administrateur"
#: build/lib/core/models.py:73 core/models.py:73
#: build/lib/core/models.py:64 core/models.py:64
msgid "Owner"
msgstr "Propriétaire"
#: build/lib/core/models.py:84 core/models.py:84
#: build/lib/core/models.py:75 core/models.py:75
msgid "Restricted"
msgstr "Restreint"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:79 core/models.py:79
msgid "Authenticated"
msgstr "Authentifié"
#: build/lib/core/models.py:90 core/models.py:90
#: build/lib/core/models.py:81 core/models.py:81
msgid "Public"
msgstr ""
#: build/lib/core/models.py:112 core/models.py:112
#: build/lib/core/models.py:103 core/models.py:103
msgid "id"
msgstr ""
#: build/lib/core/models.py:113 core/models.py:113
#: build/lib/core/models.py:104 core/models.py:104
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:119 core/models.py:119
#: build/lib/core/models.py:110 core/models.py:110
msgid "created on"
msgstr ""
#: build/lib/core/models.py:120 core/models.py:120
#: build/lib/core/models.py:111 core/models.py:111
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:125 core/models.py:125
#: build/lib/core/models.py:116 core/models.py:116
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:126 core/models.py:126
#: build/lib/core/models.py:117 core/models.py:117
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:153 core/models.py:153
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:175 core/models.py:175
#: build/lib/core/models.py:166 core/models.py:166
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:172 core/models.py:172
msgid "sub"
msgstr ""
#: build/lib/core/models.py:183 core/models.py:183
#: build/lib/core/models.py:174 core/models.py:174
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:183 core/models.py:183
msgid "full name"
msgstr ""
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:184 core/models.py:184
msgid "short name"
msgstr ""
#: build/lib/core/models.py:195 core/models.py:195
#: build/lib/core/models.py:186 core/models.py:186
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:200 core/models.py:200
#: build/lib/core/models.py:191 core/models.py:191
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:207 core/models.py:207
#: build/lib/core/models.py:198 core/models.py:198
msgid "language"
msgstr ""
#: build/lib/core/models.py:208 core/models.py:208
#: build/lib/core/models.py:199 core/models.py:199
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:214 core/models.py:214
#: build/lib/core/models.py:205 core/models.py:205
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:217 core/models.py:217
#: build/lib/core/models.py:208 core/models.py:208
msgid "device"
msgstr ""
#: build/lib/core/models.py:219 core/models.py:219
#: build/lib/core/models.py:210 core/models.py:210
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:222 core/models.py:222
#: build/lib/core/models.py:213 core/models.py:213
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:224 core/models.py:224
#: build/lib/core/models.py:215 core/models.py:215
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:227 core/models.py:227
#: build/lib/core/models.py:218 core/models.py:218
msgid "active"
msgstr ""
#: build/lib/core/models.py:230 core/models.py:230
#: build/lib/core/models.py:221 core/models.py:221
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:242 core/models.py:242
#: build/lib/core/models.py:233 core/models.py:233
msgid "user"
msgstr ""
#: build/lib/core/models.py:243 core/models.py:243
#: build/lib/core/models.py:234 core/models.py:234
msgid "users"
msgstr ""
#: build/lib/core/models.py:382 build/lib/core/models.py:758 core/models.py:382
#: core/models.py:758
#: build/lib/core/models.py:373 build/lib/core/models.py:925 core/models.py:373
#: core/models.py:925
msgid "title"
msgstr ""
#: build/lib/core/models.py:404 core/models.py:404
msgid "Document"
#: build/lib/core/models.py:374 core/models.py:374
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:405 core/models.py:405
msgid "Document"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:408 core/models.py:408
#: build/lib/core/models.py:418 core/models.py:418
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:633 core/models.py:633
#: build/lib/core/models.py:719 core/models.py:719
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} a partagé un document avec vous!"
#: build/lib/core/models.py:637 core/models.py:637
#: build/lib/core/models.py:723 core/models.py:723
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} vous a invité avec le rôle \"{role}\" sur le document suivant:"
#: build/lib/core/models.py:640 core/models.py:640
#: build/lib/core/models.py:726 core/models.py:726
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} a partagé un document avec vous: {title}"
#: build/lib/core/models.py:663 core/models.py:663
#: build/lib/core/models.py:762 core/models.py:762
msgid "This document is not deleted."
msgstr ""
#: build/lib/core/models.py:769 core/models.py:769
msgid "This document was permanently deleted and cannot be restored."
msgstr ""
#: build/lib/core/models.py:820 core/models.py:820
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:664 core/models.py:664
#: build/lib/core/models.py:821 core/models.py:821
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:670 core/models.py:670
#: build/lib/core/models.py:827 core/models.py:827
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:693 core/models.py:693
#: build/lib/core/models.py:850 core/models.py:850
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:694 core/models.py:694
#: build/lib/core/models.py:851 core/models.py:851
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:700 core/models.py:700
#: build/lib/core/models.py:857 core/models.py:857
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
#: build/lib/core/models.py:879 core/models.py:879
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:723 core/models.py:723
#: build/lib/core/models.py:880 core/models.py:880
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:729 core/models.py:729
#: build/lib/core/models.py:886 core/models.py:886
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:735 core/models.py:735
#: build/lib/core/models.py:892 core/models.py:892
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:741 build/lib/core/models.py:930 core/models.py:741
#: core/models.py:930
#: build/lib/core/models.py:898 build/lib/core/models.py:1012
#: core/models.py:898 core/models.py:1012
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:759 core/models.py:759
#: build/lib/core/models.py:926 core/models.py:926
msgid "description"
msgstr ""
#: build/lib/core/models.py:760 core/models.py:760
#: build/lib/core/models.py:927 core/models.py:927
msgid "code"
msgstr ""
#: build/lib/core/models.py:761 core/models.py:761
#: build/lib/core/models.py:928 core/models.py:928
msgid "css"
msgstr ""
#: build/lib/core/models.py:763 core/models.py:763
#: build/lib/core/models.py:930 core/models.py:930
msgid "public"
msgstr ""
#: build/lib/core/models.py:765 core/models.py:765
#: build/lib/core/models.py:932 core/models.py:932
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:771 core/models.py:771
#: build/lib/core/models.py:938 core/models.py:938
msgid "Template"
msgstr ""
#: build/lib/core/models.py:772 core/models.py:772
#: build/lib/core/models.py:939 core/models.py:939
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:911 core/models.py:911
#: build/lib/core/models.py:993 core/models.py:993
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:912 core/models.py:912
#: build/lib/core/models.py:994 core/models.py:994
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:918 core/models.py:918
#: build/lib/core/models.py:1000 core/models.py:1000
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:924 core/models.py:924
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:947 core/models.py:947
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "email address"
msgstr ""
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:1048 core/models.py:1048
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:1049 core/models.py:1049
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:987 core/models.py:987
#: build/lib/core/models.py:1069 core/models.py:1069
msgid "This email is already associated to a registered user."
msgstr ""
@@ -350,3 +391,24 @@ msgstr ""
msgid "German"
msgstr ""
#: core/templates/mail/html/invitation.html:162
#: core/templates/mail/text/invitation.txt:3
msgid "Logo email"
msgstr "Logo de l'e-mail"
#: core/templates/mail/html/invitation.html:209
#: core/templates/mail/text/invitation.txt:10
msgid "Open"
msgstr "Ouvrir"
#: core/templates/mail/html/invitation.html:226
#: core/templates/mail/text/invitation.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs, votre nouvel outil incontournable pour organiser, partager et collaborer sur vos documents en équipe. "
#: core/templates/mail/html/invitation.html:233
#: core/templates/mail/text/invitation.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr " Proposé par %(brandname)s "
+141 -79
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-15 21:00+0000\n"
"PO-Revision-Date: 2025-01-27 09:27\n"
"POT-Creation-Date: 2025-02-06 15:30+0000\n"
"PO-Revision-Date: 2025-02-10 14:14\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -17,18 +17,23 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:33 core/admin.py:33
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr ""
#: build/lib/core/admin.py:46 core/admin.py:46
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr ""
#: build/lib/core/admin.py:58 core/admin.py:58
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr ""
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr ""
#: build/lib/core/api/filters.py:16 core/api/filters.py:16
msgid "Creator is me"
msgstr ""
@@ -41,23 +46,23 @@ msgstr ""
msgid "Title"
msgstr ""
#: build/lib/core/api/serializers.py:317 core/api/serializers.py:317
#: build/lib/core/api/serializers.py:346 core/api/serializers.py:346
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:321 core/api/serializers.py:321
#: build/lib/core/api/serializers.py:350 core/api/serializers.py:350
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:422 core/api/serializers.py:422
#: build/lib/core/api/serializers.py:453 core/api/serializers.py:453
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:425 core/api/serializers.py:425
#: build/lib/core/api/serializers.py:456 core/api/serializers.py:456
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:431 core/api/serializers.py:431
#: build/lib/core/api/serializers.py:462 core/api/serializers.py:462
msgid "Format"
msgstr ""
@@ -71,270 +76,306 @@ msgstr ""
msgid "User account is disabled"
msgstr ""
#: build/lib/core/models.py:63 build/lib/core/models.py:70 core/models.py:63
#: core/models.py:70
#: build/lib/core/enums.py:19 core/enums.py:19
msgid "First child"
msgstr ""
#: build/lib/core/enums.py:20 core/enums.py:20
msgid "Last child"
msgstr ""
#: build/lib/core/enums.py:21 core/enums.py:21
msgid "First sibling"
msgstr ""
#: build/lib/core/enums.py:22 core/enums.py:22
msgid "Last sibling"
msgstr ""
#: build/lib/core/enums.py:23 core/enums.py:23
msgid "Left"
msgstr ""
#: build/lib/core/enums.py:24 core/enums.py:24
msgid "Right"
msgstr ""
#: build/lib/core/models.py:54 build/lib/core/models.py:61 core/models.py:54
#: core/models.py:61
msgid "Reader"
msgstr ""
#: build/lib/core/models.py:64 build/lib/core/models.py:71 core/models.py:64
#: core/models.py:71
#: build/lib/core/models.py:55 build/lib/core/models.py:62 core/models.py:55
#: core/models.py:62
msgid "Editor"
msgstr ""
#: build/lib/core/models.py:72 core/models.py:72
#: build/lib/core/models.py:63 core/models.py:63
msgid "Administrator"
msgstr ""
#: build/lib/core/models.py:73 core/models.py:73
#: build/lib/core/models.py:64 core/models.py:64
msgid "Owner"
msgstr ""
#: build/lib/core/models.py:84 core/models.py:84
#: build/lib/core/models.py:75 core/models.py:75
msgid "Restricted"
msgstr ""
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:79 core/models.py:79
msgid "Authenticated"
msgstr ""
#: build/lib/core/models.py:90 core/models.py:90
#: build/lib/core/models.py:81 core/models.py:81
msgid "Public"
msgstr ""
#: build/lib/core/models.py:112 core/models.py:112
#: build/lib/core/models.py:103 core/models.py:103
msgid "id"
msgstr ""
#: build/lib/core/models.py:113 core/models.py:113
#: build/lib/core/models.py:104 core/models.py:104
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:119 core/models.py:119
#: build/lib/core/models.py:110 core/models.py:110
msgid "created on"
msgstr ""
#: build/lib/core/models.py:120 core/models.py:120
#: build/lib/core/models.py:111 core/models.py:111
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:125 core/models.py:125
#: build/lib/core/models.py:116 core/models.py:116
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:126 core/models.py:126
#: build/lib/core/models.py:117 core/models.py:117
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:162 core/models.py:162
#: build/lib/core/models.py:153 core/models.py:153
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:175 core/models.py:175
#: build/lib/core/models.py:166 core/models.py:166
msgid "Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_/: characters."
msgstr ""
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:172 core/models.py:172
msgid "sub"
msgstr ""
#: build/lib/core/models.py:183 core/models.py:183
#: build/lib/core/models.py:174 core/models.py:174
msgid "Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_/: characters only."
msgstr ""
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:183 core/models.py:183
msgid "full name"
msgstr ""
#: build/lib/core/models.py:193 core/models.py:193
#: build/lib/core/models.py:184 core/models.py:184
msgid "short name"
msgstr ""
#: build/lib/core/models.py:195 core/models.py:195
#: build/lib/core/models.py:186 core/models.py:186
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:200 core/models.py:200
#: build/lib/core/models.py:191 core/models.py:191
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:207 core/models.py:207
#: build/lib/core/models.py:198 core/models.py:198
msgid "language"
msgstr ""
#: build/lib/core/models.py:208 core/models.py:208
#: build/lib/core/models.py:199 core/models.py:199
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:214 core/models.py:214
#: build/lib/core/models.py:205 core/models.py:205
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:217 core/models.py:217
#: build/lib/core/models.py:208 core/models.py:208
msgid "device"
msgstr ""
#: build/lib/core/models.py:219 core/models.py:219
#: build/lib/core/models.py:210 core/models.py:210
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:222 core/models.py:222
#: build/lib/core/models.py:213 core/models.py:213
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:224 core/models.py:224
#: build/lib/core/models.py:215 core/models.py:215
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:227 core/models.py:227
#: build/lib/core/models.py:218 core/models.py:218
msgid "active"
msgstr ""
#: build/lib/core/models.py:230 core/models.py:230
#: build/lib/core/models.py:221 core/models.py:221
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:242 core/models.py:242
#: build/lib/core/models.py:233 core/models.py:233
msgid "user"
msgstr ""
#: build/lib/core/models.py:243 core/models.py:243
#: build/lib/core/models.py:234 core/models.py:234
msgid "users"
msgstr ""
#: build/lib/core/models.py:382 build/lib/core/models.py:758 core/models.py:382
#: core/models.py:758
#: build/lib/core/models.py:373 build/lib/core/models.py:925 core/models.py:373
#: core/models.py:925
msgid "title"
msgstr ""
#: build/lib/core/models.py:404 core/models.py:404
msgid "Document"
#: build/lib/core/models.py:374 core/models.py:374
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:405 core/models.py:405
msgid "Document"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:408 core/models.py:408
#: build/lib/core/models.py:418 core/models.py:418
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:633 core/models.py:633
#: build/lib/core/models.py:719 core/models.py:719
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:637 core/models.py:637
#: build/lib/core/models.py:723 core/models.py:723
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:640 core/models.py:640
#: build/lib/core/models.py:726 core/models.py:726
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:663 core/models.py:663
#: build/lib/core/models.py:762 core/models.py:762
msgid "This document is not deleted."
msgstr ""
#: build/lib/core/models.py:769 core/models.py:769
msgid "This document was permanently deleted and cannot be restored."
msgstr ""
#: build/lib/core/models.py:820 core/models.py:820
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:664 core/models.py:664
#: build/lib/core/models.py:821 core/models.py:821
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:670 core/models.py:670
#: build/lib/core/models.py:827 core/models.py:827
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:693 core/models.py:693
#: build/lib/core/models.py:850 core/models.py:850
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:694 core/models.py:694
#: build/lib/core/models.py:851 core/models.py:851
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:700 core/models.py:700
#: build/lib/core/models.py:857 core/models.py:857
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
#: build/lib/core/models.py:879 core/models.py:879
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:723 core/models.py:723
#: build/lib/core/models.py:880 core/models.py:880
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:729 core/models.py:729
#: build/lib/core/models.py:886 core/models.py:886
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:735 core/models.py:735
#: build/lib/core/models.py:892 core/models.py:892
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:741 build/lib/core/models.py:930 core/models.py:741
#: core/models.py:930
#: build/lib/core/models.py:898 build/lib/core/models.py:1012
#: core/models.py:898 core/models.py:1012
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:759 core/models.py:759
#: build/lib/core/models.py:926 core/models.py:926
msgid "description"
msgstr ""
#: build/lib/core/models.py:760 core/models.py:760
#: build/lib/core/models.py:927 core/models.py:927
msgid "code"
msgstr ""
#: build/lib/core/models.py:761 core/models.py:761
#: build/lib/core/models.py:928 core/models.py:928
msgid "css"
msgstr ""
#: build/lib/core/models.py:763 core/models.py:763
#: build/lib/core/models.py:930 core/models.py:930
msgid "public"
msgstr ""
#: build/lib/core/models.py:765 core/models.py:765
#: build/lib/core/models.py:932 core/models.py:932
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:771 core/models.py:771
#: build/lib/core/models.py:938 core/models.py:938
msgid "Template"
msgstr ""
#: build/lib/core/models.py:772 core/models.py:772
#: build/lib/core/models.py:939 core/models.py:939
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:911 core/models.py:911
#: build/lib/core/models.py:993 core/models.py:993
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:912 core/models.py:912
#: build/lib/core/models.py:994 core/models.py:994
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:918 core/models.py:918
#: build/lib/core/models.py:1000 core/models.py:1000
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:924 core/models.py:924
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:947 core/models.py:947
#: build/lib/core/models.py:1029 core/models.py:1029
msgid "email address"
msgstr ""
#: build/lib/core/models.py:966 core/models.py:966
#: build/lib/core/models.py:1048 core/models.py:1048
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:1049 core/models.py:1049
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:987 core/models.py:987
#: build/lib/core/models.py:1069 core/models.py:1069
msgid "This email is already associated to a registered user."
msgstr ""
@@ -350,3 +391,24 @@ msgstr ""
msgid "German"
msgstr ""
#: core/templates/mail/html/invitation.html:162
#: core/templates/mail/text/invitation.txt:3
msgid "Logo email"
msgstr ""
#: core/templates/mail/html/invitation.html:209
#: core/templates/mail/text/invitation.txt:10
msgid "Open"
msgstr ""
#: core/templates/mail/html/invitation.html:226
#: core/templates/mail/text/invitation.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/invitation.html:233
#: core/templates/mail/text/invitation.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr ""
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "2.0.1"
version = "2.2.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -1,6 +1,17 @@
import { Page, expect } from '@playwright/test';
export const keyCloakSignIn = async (page: Page, browserName: string) => {
export const keyCloakSignIn = async (
page: Page,
browserName: string,
fromHome: boolean = true,
) => {
if (fromHome) {
await page
.getByRole('button', { name: 'Proconnect Login' })
.first()
.click();
}
const login = `user-e2e-${browserName}`;
const password = `password-e2e-${browserName}`;
@@ -258,3 +269,8 @@ export const mockedAccesses = async (page: Page, json?: object) => {
}
});
};
export const expectLoginPage = async (page: Page) =>
await expect(
page.getByRole('heading', { name: 'Collaborative writing' }),
).toBeVisible();
@@ -63,27 +63,6 @@ test.describe('Config', () => {
expect((await consoleMessage).text()).toContain(invalidMsg);
});
test('it checks that theme is configured from config endpoint', async ({
page,
}) => {
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes('/config/') && response.status() === 200,
);
await page.goto('/');
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
const jsonResponse = await response.json();
expect(jsonResponse.FRONTEND_THEME).toStrictEqual('dsfr');
const footer = page.locator('footer').first();
// alt 'Gouvernement Logo' comes from the theme
await expect(footer.getByAltText('Gouvernement Logo')).toBeVisible();
});
test('it checks that media server is configured from config endpoint', async ({
page,
browserName,
@@ -161,3 +140,28 @@ test.describe('Config', () => {
).toBeVisible();
});
});
test.describe('Config: Not loggued', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('it checks that theme is configured from config endpoint', async ({
page,
}) => {
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes('/config/') && response.status() === 200,
);
await page.goto('/');
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
const jsonResponse = await response.json();
expect(jsonResponse.FRONTEND_THEME).toStrictEqual('dsfr');
const footer = page.locator('footer').first();
// alt 'Gouvernement Logo' comes from the theme
await expect(footer.getByAltText('Gouvernement Logo')).toBeVisible();
});
});
@@ -24,8 +24,6 @@ test.describe('Doc Create', () => {
const header = page.locator('header').first();
await header.locator('h2').getByText('Docs').click();
await expect(page.getByTestId('grid-loader')).toBeVisible();
const docsGrid = page.getByTestId('docs-grid');
await expect(docsGrid).toBeVisible();
await expect(page.getByTestId('grid-loader')).toBeHidden();
@@ -1,3 +1,5 @@
/* eslint-disable playwright/no-conditional-expect */
/* eslint-disable playwright/no-conditional-in-test */
import path from 'path';
import { expect, test } from '@playwright/test';
@@ -368,4 +370,77 @@ test.describe('Doc Editor', () => {
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
});
[
{ ai_transform: false, ai_translate: false },
{ ai_transform: true, ai_translate: false },
{ ai_transform: false, ai_translate: true },
].forEach(({ ai_transform, ai_translate }) => {
test(`it checks AI buttons when can transform is at "${ai_transform}" and can translate is at "${ai_translate}"`, async ({
page,
}) => {
await mockedDocument(page, {
accesses: [
{
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
role: 'owner',
user: {
email: 'super@owner.com',
full_name: 'Super Owner',
},
},
],
abilities: {
destroy: true, // Means owner
link_configuration: true,
ai_transform,
ai_translate,
accesses_manage: true,
accesses_view: true,
update: true,
partial_update: true,
retrieve: true,
},
link_reach: 'public',
link_role: 'editor',
created_at: '2021-09-01T09:00:00Z',
});
await goToGridDoc(page);
await verifyDocName(page, 'Mocked document');
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').dblclick();
if (!ai_transform && !ai_translate) {
await expect(page.getByRole('button', { name: 'AI' })).toBeHidden();
return;
}
await page.getByRole('button', { name: 'AI' }).click();
if (ai_transform) {
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeVisible();
} else {
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeHidden();
}
if (ai_translate) {
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeVisible();
} else {
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeHidden();
}
});
});
});
@@ -29,9 +29,7 @@ test.describe('Doc Export', () => {
.first(),
).toBeVisible();
await expect(
page.getByText(
'Upload your docs to a Microsoft Word, Open Office or PDF document',
),
page.getByText('Download your document in a .docx or .pdf format.'),
).toBeVisible();
await expect(
page.getByRole('combobox', { name: 'Template' }),
@@ -43,8 +41,16 @@ test.describe('Doc Export', () => {
await expect(page.getByRole('button', { name: 'Download' })).toBeVisible();
});
test('it exports the doc to pdf', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
test('it exports the doc with pdf line break', async ({
page,
browserName,
}) => {
const [randomDoc] = await createDoc(
page,
'doc-editor-line-break',
browserName,
1,
);
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
@@ -52,8 +58,20 @@ test.describe('Doc Export', () => {
await verifyDocName(page, randomDoc);
await page.locator('.ProseMirror.bn-editor').click();
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
const editor = page.locator('.ProseMirror.bn-editor');
await editor.click();
await editor.locator('.bn-block-outer').last().fill('Hello');
await page.keyboard.press('Enter');
await editor.locator('.bn-block-outer').last().fill('/');
await page.getByText('Page Break').click();
await expect(editor.locator('.bn-page-break')).toBeVisible();
await page.keyboard.press('Enter');
await editor.locator('.bn-block-outer').last().fill('World');
await page
.getByRole('button', {
@@ -71,9 +89,10 @@ test.describe('Doc Export', () => {
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfText = (await pdf(pdfBuffer)).text;
const pdfData = await pdf(pdfBuffer);
expect(pdfText).toContain('Hello World'); // This is the doc text
expect(pdfData.numpages).toBe(2);
expect(pdfData.text).toContain('\n\nHello\n\nWorld'); // This is the doc text
});
test('it exports the doc to docx', async ({ page, browserName }) => {
@@ -95,7 +114,7 @@ test.describe('Doc Export', () => {
.click();
await page.getByRole('combobox', { name: 'Format' }).click();
await page.getByRole('option', { name: 'Word / Open Office' }).click();
await page.getByRole('option', { name: 'Docx' }).click();
await page
.getByRole('button', {
@@ -213,7 +213,6 @@ test.describe('Document grid item options', () => {
test.describe('Documents filters', () => {
test('it checks the prebuild left panel filters', async ({ page }) => {
// All Docs
await expect(page.getByTestId('grid-loader')).toBeVisible();
const response = await page.waitForResponse(
(response) =>
response.url().endsWith('documents/?page=1') &&
@@ -254,7 +253,6 @@ test.describe('Documents filters', () => {
url = new URL(page.url());
target = url.searchParams.get('target');
expect(target).toBe('my_docs');
await expect(page.getByTestId('grid-loader')).toBeVisible();
const responseMyDocs = await page.waitForResponse(
(response) =>
response.url().endsWith('documents/?page=1&is_creator_me=true') &&
@@ -270,7 +268,6 @@ test.describe('Documents filters', () => {
url = new URL(page.url());
target = url.searchParams.get('target');
expect(target).toBe('shared_with_me');
await expect(page.getByTestId('grid-loader')).toBeVisible();
const responseSharedWithMe = await page.waitForResponse(
(response) =>
response.url().includes('documents/?page=1&is_creator_me=false') &&
@@ -291,8 +288,6 @@ test.describe('Documents Grid', () => {
test('checks all the elements are visible', async ({ page }) => {
let docs: SmallDoc[] = [];
await expect(page.getByTestId('grid-loader')).toBeVisible();
const response = await page.waitForResponse(
(response) =>
response.url().endsWith('documents/?page=1') &&
@@ -395,9 +395,7 @@ test.describe('Doc Header', () => {
navigator.clipboard.readText(),
);
const clipboardContent = await handle.jsonValue();
expect(clipboardContent.trim()).toBe(
`<h1 data-level=\"1\">Hello World</h1><p></p>`,
);
expect(clipboardContent.trim()).toBe(`<h1>Hello World</h1><p></p>`);
});
test('it checks the copy link button', async ({ page }) => {
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { keyCloakSignIn, mockedDocument } from './common';
import { expectLoginPage, keyCloakSignIn, mockedDocument } from './common';
test.describe('Doc Routing', () => {
test.beforeEach(async ({ page }) => {
@@ -63,16 +63,13 @@ test.describe('Doc Routing: Not loggued', () => {
await page.goto('/docs/mocked-document-id/');
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
await page.getByRole('button', { name: 'Login' }).click();
await keyCloakSignIn(page, browserName);
await keyCloakSignIn(page, browserName, false);
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
});
// eslint-disable-next-line playwright/expect-expect
test('The homepage redirects to login.', async ({ page }) => {
await page.goto('/');
await expect(
page.getByRole('button', {
name: 'Sign In',
}),
).toBeVisible();
await expectLoginPage(page);
});
});
@@ -1,6 +1,11 @@
import { expect, test } from '@playwright/test';
import { createDoc, keyCloakSignIn, verifyDocName } from './common';
import {
createDoc,
expectLoginPage,
keyCloakSignIn,
verifyDocName,
} from './common';
const browsersName = ['chromium', 'webkit', 'firefox'];
@@ -91,7 +96,7 @@ test.describe('Doc Visibility: Restricted', () => {
})
.click();
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
await expectLoginPage(page);
await page.goto(urlDoc);
@@ -121,6 +126,10 @@ test.describe('Doc Visibility: Restricted', () => {
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
).toBeVisible();
await page.goto(urlDoc);
await expect(
@@ -169,10 +178,11 @@ test.describe('Doc Visibility: Restricted', () => {
await keyCloakSignIn(page, otherBrowser!);
await page.goto(urlDoc);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
).toBeVisible();
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1000);
await page.goto(urlDoc);
await verifyDocName(page, docTitle);
await expect(page.getByLabel('Share button')).toBeVisible();
@@ -247,7 +257,7 @@ test.describe('Doc Visibility: Public', () => {
})
.click();
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
await expectLoginPage(page);
await page.goto(urlDoc);
@@ -313,7 +323,7 @@ test.describe('Doc Visibility: Public', () => {
})
.click();
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
await expectLoginPage(page);
await page.goto(urlDoc);
@@ -364,7 +374,7 @@ test.describe('Doc Visibility: Authenticated', () => {
})
.click();
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
await expectLoginPage(page);
await page.goto(urlDoc);
@@ -414,6 +424,10 @@ test.describe('Doc Visibility: Authenticated', () => {
const otherBrowser = browsersName.find((b) => b !== browserName);
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
).toBeVisible();
await page.goto(urlDoc);
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
@@ -470,6 +484,10 @@ test.describe('Doc Visibility: Authenticated', () => {
const otherBrowser = browsersName.find((b) => b !== browserName);
await keyCloakSignIn(page, otherBrowser!);
await expect(
page.getByRole('link', { name: 'Docs Logo Docs BETA' }),
).toBeVisible();
await page.goto(urlDoc);
await verifyDocName(page, docTitle);
@@ -1,13 +1,10 @@
import { expect, test } from '@playwright/test';
import { goToGridDoc } from './common';
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test.describe('Footer', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('checks all the elements are visible', async ({ page }) => {
await page.goto('/');
const footer = page.locator('footer').first();
await expect(footer.getByAltText('Gouvernement Logo')).toBeVisible();
@@ -47,12 +44,6 @@ test.describe('Footer', () => {
).toBeVisible();
});
test('checks footer is not visible on doc editor', async ({ page }) => {
await expect(page.locator('footer')).toBeVisible();
await goToGridDoc(page);
await expect(page.locator('footer')).toBeHidden();
});
const legalPages = [
{ name: 'Legal Notice', url: '/legal-notice/' },
{ name: 'Personal data and cookies', url: '/personal-data-cookies/' },
@@ -60,6 +51,8 @@ test.describe('Footer', () => {
];
for (const { name, url } of legalPages) {
test(`checks ${name} page`, async ({ page }) => {
await page.goto('/');
const footer = page.locator('footer').first();
await footer.getByRole('link', { name }).click();
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { keyCloakSignIn } from './common';
import { expectLoginPage, keyCloakSignIn } from './common';
test.describe('Header', () => {
test.beforeEach(async ({ page }) => {
@@ -10,7 +10,7 @@ test.describe('Header', () => {
test('checks all the elements are visible', async ({ page }) => {
const header = page.locator('header').first();
await expect(header.getByAltText('Docs Logo')).toBeVisible();
await expect(header.getByLabel('Docs Logo')).toBeVisible();
await expect(header.locator('h2').getByText('Docs')).toHaveCSS(
'color',
'rgb(0, 0, 145)',
@@ -98,6 +98,6 @@ test.describe('Header: Log out', () => {
})
.click();
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
await expectLoginPage(page);
});
});
@@ -0,0 +1,52 @@
import { expect, test } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('/docs/');
});
test.describe('Home page', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('checks all the elements are visible', async ({ page }) => {
// Check header content
const header = page.locator('header').first();
const footer = page.locator('footer').first();
await expect(header).toBeVisible();
await expect(
header.getByRole('combobox', { name: 'Language' }),
).toBeVisible();
await expect(
header.getByRole('button', { name: 'Les services de La Suite numé' }),
).toBeVisible();
await expect(
header.getByRole('img', { name: 'Gouvernement Logo' }),
).toBeVisible();
await expect(header.getByRole('img', { name: 'Docs logo' })).toBeVisible();
await expect(header.getByRole('heading', { name: 'Docs' })).toBeVisible();
await expect(header.getByText('BETA')).toBeVisible();
// Check the titles
const h2 = page.locator('h2');
await expect(
h2.getByText('Collaborative writing, Simplified.'),
).toBeVisible();
await expect(
h2.getByText('An uncompromising writing experience.'),
).toBeVisible();
await expect(
h2.getByText('Simple and secure collaboration.'),
).toBeVisible();
await expect(h2.getByText('Flexible export.')).toBeVisible();
await expect(
h2.getByText('A new way to organize knowledge.'),
).toBeVisible();
await expect(
page.getByText('Docs is already available, log in to use it now.'),
).toBeVisible();
await expect(
page.getByRole('button', { name: 'Proconnect Login' }),
).toHaveCount(2);
await expect(footer).toBeVisible();
});
});
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "2.0.1",
"version": "2.2.0",
"private": true,
"scripts": {
"lint": "eslint . --ext .ts",
@@ -12,7 +12,7 @@
"test:ui::chromium": "yarn test:ui --project=chromium"
},
"devDependencies": {
"@playwright/test": "1.49.1",
"@playwright/test": "1.50.1",
"@types/node": "*",
"@types/pdf-parse": "1.1.4",
"eslint-config-impress": "*",
+15 -3
View File
@@ -5,6 +5,7 @@ const config = {
colors: {
'card-border': '#ededed',
'primary-bg': '#FAFAFA',
'primary-action': '#1212FF',
'primary-050': '#F5F5FE',
'primary-100': '#EDF5FA',
'primary-150': '#E5EEFA',
@@ -59,6 +60,11 @@ const config = {
h4: '1.375rem',
h5: '1.25rem',
h6: '1.125rem',
'xl-alt': '5rem',
'lg-alt': '4.5rem',
'md-alt': '4rem',
'sm-alt': '3.5rem',
'xs-alt': '3rem',
},
weights: {
thin: 100,
@@ -224,7 +230,7 @@ const config = {
'color-hover': 'var(--c--theme--colors--primary-700)',
},
border: {
color: 'var(--c--theme--colors--primary-200)',
color: 'var(--c--theme--colors--greyscale-300)',
},
},
tertiary: {
@@ -247,6 +253,9 @@ const config = {
'la-gauffre': {
activated: false,
},
'home-proconnect': {
activated: false,
},
},
},
dsfr: {
@@ -379,8 +388,8 @@ const config = {
'color-active': '#EDEDED',
},
border: {
color: 'var(--c--theme--colors--primary-600)',
'color-hover': 'var(--c--theme--colors--primary-600)',
color: 'var(--c--theme--colors--greyscale-300)',
'color-hover': 'var(--c--theme--colors--greyscale-300)',
},
color: 'var(--c--theme--colors--primary-text)',
},
@@ -462,6 +471,9 @@ const config = {
'la-gauffre': {
activated: true,
},
'home-proconnect': {
activated: true,
},
},
},
},
+27 -27
View File
@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "2.0.1",
"version": "2.2.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -15,48 +15,48 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@blocknote/core": "0.21.0",
"@blocknote/mantine": "0.21.0",
"@blocknote/react": "0.21.0",
"@blocknote/xl-docx-exporter": "0.21.0",
"@blocknote/xl-pdf-exporter": "0.21.0",
"@blocknote/core": "0.23.2",
"@blocknote/mantine": "0.23.2",
"@blocknote/react": "0.23.2",
"@blocknote/xl-docx-exporter": "0.23.2",
"@blocknote/xl-pdf-exporter": "0.23.2",
"@gouvfr-lasuite/integration": "1.0.2",
"@hocuspocus/provider": "2.15.0",
"@hocuspocus/provider": "2.15.2",
"@openfun/cunningham-react": "2.9.4",
"@react-pdf/renderer": "4.1.6",
"@sentry/nextjs": "8.47.0",
"@tanstack/react-query": "5.62.11",
"@sentry/nextjs": "8.54.0",
"@tanstack/react-query": "5.66.0",
"cmdk": "1.0.4",
"crisp-sdk-web": "1.0.25",
"docx": "9.1.0",
"i18next": "24.2.0",
"docx": "9.1.1",
"i18next": "24.2.2",
"i18next-browser-languagedetector": "8.0.2",
"idb": "8.0.1",
"idb": "8.0.2",
"lodash": "4.17.21",
"luxon": "3.5.0",
"next": "15.1.3",
"posthog-js": "1.204.0",
"next": "15.1.6",
"posthog-js": "1.215.6",
"react": "*",
"react-aria-components": "1.5.0",
"react-aria-components": "1.6.0",
"react-dom": "*",
"react-i18next": "15.4.0",
"react-intersection-observer": "9.13.1",
"react-select": "5.9.0",
"styled-components": "6.1.13",
"react-intersection-observer": "9.15.1",
"react-select": "5.10.0",
"styled-components": "6.1.15",
"use-debounce": "10.0.4",
"y-protocols": "1.0.6",
"yjs": "13.6.21",
"zustand": "5.0.2"
"yjs": "13.6.23",
"zustand": "5.0.3"
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
"@tanstack/react-query-devtools": "5.62.11",
"@tanstack/react-query-devtools": "5.66.0",
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.1.0",
"@testing-library/user-event": "14.5.2",
"@testing-library/react": "16.2.0",
"@testing-library/user-event": "14.6.1",
"@types/jest": "29.5.14",
"@types/lodash": "4.17.13",
"@types/lodash": "4.17.15",
"@types/luxon": "3.4.2",
"@types/node": "*",
"@types/react": "18.3.12",
@@ -69,9 +69,9 @@
"jest-environment-jsdom": "29.7.0",
"node-fetch": "2.7.0",
"prettier": "3.4.2",
"stylelint": "16.12.0",
"stylelint-config-standard": "36.0.1",
"stylelint-prettier": "5.0.2",
"stylelint": "16.14.1",
"stylelint-config-standard": "37.0.0",
"stylelint-prettier": "5.0.3",
"typescript": "*",
"webpack": "5.97.1",
"workbox-webpack-plugin": "7.1.0"
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

@@ -0,0 +1,18 @@
<svg
width="32"
height="33"
viewBox="0 0 32 33"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M21.6305 29.5812C22.7983 29.2538 23.9166 28.6562 24.6505 27.6003C25.3749 26.5663 25.5789 25.2547 25.5789 23.9925V5.50099C25.5789 5.17358 25.5611 4.84557 25.5216 4.52148C26.1016 4.74961 26.5486 5.12658 26.8626 5.65239C27.2331 6.25024 27.4184 7.03757 27.4184 8.01435V26.7964C27.4184 28.1184 27.0942 29.1078 26.4458 29.7646C25.7974 30.4214 24.8207 30.7498 23.5155 30.7498H16.4209C16.5889 30.7204 16.7574 30.6901 16.9262 30.659C18.4067 30.3944 19.9713 30.0354 21.6185 29.5846L21.6305 29.5812Z"
fill="#C9191E"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.58203 26.405V7.5977C4.58203 6.45251 4.88938 5.58519 5.50408 4.99575C6.1272 4.40631 6.95242 4.08212 7.97972 4.02318C9.49542 3.93055 10.9311 3.80425 12.2868 3.64425C13.6425 3.47584 14.9393 3.28217 16.1771 3.06324C17.4234 2.8443 18.6359 2.60011 19.8148 2.33065C21.0274 2.04435 21.9578 2.1875 22.6062 2.7601C23.2546 3.33269 23.5788 4.24632 23.5788 5.50099V23.9925C23.5788 25.0956 23.3893 25.9166 23.0104 26.4555C22.6315 27.0029 21.9915 27.4028 21.0905 27.6554C19.4906 28.0933 17.9833 28.4386 16.5687 28.6912C15.154 28.9522 13.7731 29.1501 12.4258 29.2848C11.0785 29.4196 9.69751 29.5248 8.28286 29.6006C7.11241 29.668 6.20299 29.4238 5.5546 28.868C4.90622 28.3207 4.58203 27.4997 4.58203 26.405ZM9.20865 11.0124C11.0635 10.8944 12.7632 10.7131 14.3075 10.4683C14.6822 10.4072 15.0564 10.3436 15.4291 10.2776C15.8192 10.2085 16.1013 9.86859 16.1013 9.47337C16.1013 8.96154 15.638 8.57609 15.135 8.66189C14.846 8.71118 14.5555 8.75909 14.2635 8.80562C12.7346 9.04923 11.0452 9.22998 9.19523 9.3477C8.91819 9.36558 8.69776 9.45188 8.55608 9.62391C8.42209 9.78661 8.35645 9.98229 8.35645 10.2053C8.35645 10.4321 8.43296 10.6295 8.58568 10.7918L8.58783 10.7939C8.75336 10.9595 8.96369 11.0311 9.20865 11.0124ZM9.20801 15.206C11.0631 15.088 12.763 14.9066 14.3075 14.6619C15.8588 14.4089 17.3936 14.1138 18.9112 13.7766C19.2191 13.7081 19.4498 13.6003 19.5652 13.433C19.6786 13.2721 19.7347 13.0876 19.7347 12.8832C19.7347 12.6526 19.6469 12.454 19.476 12.2926C19.2921 12.1189 19.0348 12.0784 18.7304 12.1411L18.7285 12.1415C17.2823 12.4694 15.794 12.7553 14.2635 12.9992C12.7346 13.2428 11.0452 13.4235 9.19523 13.5413C8.91819 13.5591 8.69776 13.6454 8.55608 13.8175C8.42276 13.9794 8.35645 14.1705 8.35645 14.3863C8.35645 14.6203 8.43209 14.8223 8.58558 14.9854L8.59 14.9896C8.75499 15.1449 8.96316 15.2155 9.20551 15.2062L9.20801 15.206ZM9.20847 19.3994C11.0634 19.2729 12.7631 19.0874 14.3075 18.8427C15.8589 18.5982 17.3934 18.3073 18.9112 17.97C19.2199 17.9014 19.4508 17.7891 19.566 17.6127C19.6783 17.4529 19.7347 17.2733 19.7347 17.0766C19.7347 16.8461 19.6469 16.6474 19.476 16.4861C19.2921 16.3123 19.0348 16.2718 18.7304 16.3345L18.729 16.3348C17.2827 16.6543 15.7942 16.9361 14.2635 17.18C12.7345 17.4236 11.045 17.6086 9.19495 17.7347C8.91804 17.7526 8.69771 17.8389 8.55608 18.0109C8.42276 18.1728 8.35645 18.3639 8.35645 18.5797C8.35645 18.8137 8.43209 19.0158 8.58558 19.1789L8.59 19.183C8.75499 19.3383 8.96316 19.4089 9.20551 19.3996L9.20847 19.3994ZM14.3075 23.007C12.7632 23.2518 11.0635 23.4331 9.20867 23.5512C8.9637 23.5698 8.75337 23.4982 8.58783 23.3326L8.58572 23.3305C8.433 23.1682 8.35645 22.9708 8.35645 22.7441C8.35645 22.521 8.42209 22.3253 8.55608 22.1626C8.69776 21.9906 8.91827 21.9043 9.19531 21.8864C11.0453 21.7687 12.7346 21.588 14.2635 21.3443C14.5555 21.2978 14.846 21.2499 15.135 21.2006C15.638 21.1148 16.1013 21.5003 16.1013 22.0121C16.1013 22.4073 15.8192 22.7472 15.4291 22.8163C15.0564 22.8823 14.6822 22.9459 14.3075 23.007Z"
fill="#000091"
/>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

@@ -1,3 +1,4 @@
import { FocusScope } from '@react-aria/focus';
import {
PropsWithChildren,
ReactNode,
@@ -6,16 +7,16 @@ import {
useState,
} from 'react';
import { Button, Popover } from 'react-aria-components';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
const StyledPopover = styled(Popover)`
background-color: white;
border-radius: 4px;
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
border: 1px solid #dddddd;
transition: opacity 0.2s ease-in-out;
padding: 1rem;
`;
const StyledButton = styled(Button)`
@@ -29,6 +30,10 @@ const StyledButton = styled(Button)`
font-size: 0.938rem;
padding: 0;
text-wrap: nowrap;
&:focus-within {
outline: 2px solid #007bff;
}
`;
export interface DropButtonProps {
@@ -43,15 +48,17 @@ export const DropButton = ({
isOpen = false,
onOpenChange,
children,
label,
}: PropsWithChildren<DropButtonProps>) => {
const { t } = useTranslation();
const [isLocalOpen, setIsLocalOpen] = useState(isOpen);
const triggerRef = useRef(null);
const triggerRef = useRef<HTMLButtonElement>(null);
const firstFocusableRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
setIsLocalOpen(isOpen);
}, [isOpen]);
if (isLocalOpen && firstFocusableRef.current) {
firstFocusableRef.current.focus();
}
}, [isLocalOpen]);
const onOpenChangeHandler = (isOpen: boolean) => {
setIsLocalOpen(isOpen);
@@ -63,18 +70,30 @@ export const DropButton = ({
<StyledButton
ref={triggerRef}
onPress={() => onOpenChangeHandler(true)}
aria-label={label}
aria-haspopup="true"
aria-expanded={isLocalOpen}
aria-label={t('Open the document options')}
>
{button}
<span aria-hidden="true">{button}</span>
</StyledButton>
<StyledPopover
triggerRef={triggerRef}
isOpen={isLocalOpen}
onOpenChange={onOpenChangeHandler}
>
{children}
</StyledPopover>
{isLocalOpen && (
<StyledPopover
triggerRef={triggerRef}
isOpen={isLocalOpen}
onOpenChange={onOpenChangeHandler}
>
<FocusScope contain restoreFocus>
{children}
<button
ref={firstFocusableRef}
onClick={() => setIsLocalOpen(false)}
>
{t('Close the modal')}
</button>
</FocusScope>
</StyledPopover>
)}
</>
);
};
@@ -1,3 +1,4 @@
//import { t } from 'i18next';
import { PropsWithChildren, useState } from 'react';
import { css } from 'styled-components';
@@ -47,7 +47,11 @@ export const QuickSearchInput = ({
$gap={spacing['2xs']}
$padding={{ all: 'base' }}
>
{!loading && <Icon iconName="search" $variation="600" />}
{!loading && (
<span aria-hidden="true">
<Icon iconName="search" $variation="600" />
</span>
)}
{loading && (
<div>
<Loader size="small" />
@@ -3,10 +3,10 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useEffect } from 'react';
import { useCunninghamTheme } from '@/cunningham';
import { Auth } from '@/features/auth';
import '@/i18n/initI18n';
import { useResponsiveStore } from '@/stores/';
import { Auth } from './auth/';
import { ConfigProvider } from './config/';
/**
@@ -1,66 +0,0 @@
import { Loader } from '@openfun/cunningham-react';
import { useRouter } from 'next/router';
import { PropsWithChildren, useEffect, useState } from 'react';
import { Box } from '@/components';
import { useAuthStore } from './useAuthStore';
/**
* TODO: Remove this restriction when we will have a homepage design for non-authenticated users.
*
* We define the paths that are not allowed without authentication.
* Actually, only the home page and the docs page are not allowed without authentication.
* When we will have a homepage design for non-authenticated users, we will remove this restriction to have
* the full website accessible without authentication.
*/
const regexpUrlsAuth = [/\/docs\/$/g, /^\/$/g];
export const Auth = ({ children }: PropsWithChildren) => {
const { initAuth, initiated, authenticated, login, getAuthUrl } =
useAuthStore();
const { asPath, replace } = useRouter();
const [pathAllowed, setPathAllowed] = useState<boolean>(
!regexpUrlsAuth.some((regexp) => !!asPath.match(regexp)),
);
useEffect(() => {
initAuth();
}, [initAuth]);
useEffect(() => {
setPathAllowed(!regexpUrlsAuth.some((regexp) => !!asPath.match(regexp)));
}, [asPath]);
// We force to login except on allowed paths
useEffect(() => {
if (!initiated || authenticated || pathAllowed) {
return;
}
login();
}, [authenticated, pathAllowed, login, initiated]);
// Redirect to the path before login
useEffect(() => {
if (!authenticated) {
return;
}
const authUrl = getAuthUrl();
if (authUrl) {
void replace(authUrl);
}
}, [authenticated, getAuthUrl, replace]);
if ((!initiated && pathAllowed) || (!authenticated && !pathAllowed)) {
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
<Loader />
</Box>
);
}
return children;
};
@@ -1,23 +0,0 @@
import { Button } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '@/core/auth';
export const ButtonLogin = () => {
const { t } = useTranslation();
const { logout, authenticated, login } = useAuthStore();
if (!authenticated) {
return (
<Button onClick={login} color="primary-text" aria-label={t('Login')}>
{t('Login')}
</Button>
);
}
return (
<Button onClick={logout} color="primary-text" aria-label={t('Logout')}>
{t('Logout')}
</Button>
);
};
@@ -1,2 +0,0 @@
export * from './getMe';
export * from './types';
@@ -1 +0,0 @@
export const PATH_AUTH_LOCAL_STORAGE = 'docs-path-auth';
@@ -1,4 +0,0 @@
export * from './api/types';
export * from './Auth';
export * from './ButtonLogin';
export * from './useAuthStore';
@@ -1,64 +0,0 @@
import { create } from 'zustand';
import { baseApiUrl } from '@/api';
import { terminateCrispSession } from '@/services';
import { User, getMe } from './api';
import { PATH_AUTH_LOCAL_STORAGE } from './conf';
interface AuthStore {
initiated: boolean;
authenticated: boolean;
initAuth: () => void;
logout: () => void;
login: () => void;
setAuthUrl: (url: string) => void;
getAuthUrl: () => string | undefined;
userData?: User;
}
const initialState = {
initiated: false,
authenticated: false,
userData: undefined,
};
export const useAuthStore = create<AuthStore>((set, get) => ({
initiated: initialState.initiated,
authenticated: initialState.authenticated,
userData: initialState.userData,
initAuth: () => {
getMe()
.then((data: User) => {
set({ authenticated: true, userData: data });
})
.catch(() => {})
.finally(() => {
set({ initiated: true });
});
},
login: () => {
get().setAuthUrl(window.location.pathname);
window.location.replace(`${baseApiUrl()}authenticate/`);
},
logout: () => {
terminateCrispSession();
window.location.replace(`${baseApiUrl()}logout/`);
},
// If we try to access a specific page and we are not authenticated
// we store the path in the local storage to redirect to it after login
setAuthUrl() {
if (window.location.pathname !== '/') {
localStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.pathname);
}
},
// If a path is stored in the local storage, we return it then remove it
getAuthUrl() {
const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE);
if (path_auth) {
localStorage.removeItem(PATH_AUTH_LOCAL_STORAGE);
return path_auth;
}
},
}));
@@ -1,3 +1,2 @@
export * from './AppProvider';
export * from './auth';
export * from './config';
@@ -203,6 +203,7 @@ input:-webkit-autofill:focus {
.c__select__wrapper .c__select__inner__actions__open:focus {
outline: none;
}
.c__select__wrapper .labelled-box__label.c__offscreen {
@@ -605,3 +606,31 @@ input:-webkit-autofill:focus {
.c__tooltip {
padding: 4px 6px;
}
/**
* lecture ou non des icons
*/
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
[data-icon]:before {
font-family: 'Material Icons';
content: attr(data-icon);
}
button:focus {
background-color: var(
--c--components--button--primary-text--background--color-hover
);
border-radius: var(--c--components--button--border-radius--focus);
box-shadow: 0 0 0 2px var(--c--theme--colors--primary-400)
}
@@ -71,6 +71,7 @@
--c--theme--colors--danger-text: var(--c--theme--colors--greyscale-000);
--c--theme--colors--card-border: #ededed;
--c--theme--colors--primary-bg: #fafafa;
--c--theme--colors--primary-action: #1212ff;
--c--theme--colors--primary-050: #f5f5fe;
--c--theme--colors--primary-150: #e5eefa;
--c--theme--colors--primary-950: #1b1b35;
@@ -122,6 +123,11 @@
--c--theme--font--sizes--ml: 0.938rem;
--c--theme--font--sizes--xl: 1.25rem;
--c--theme--font--sizes--t: 0.6875rem;
--c--theme--font--sizes--xl-alt: 5rem;
--c--theme--font--sizes--lg-alt: 4.5rem;
--c--theme--font--sizes--md-alt: 4rem;
--c--theme--font--sizes--sm-alt: 3.5rem;
--c--theme--font--sizes--xs-alt: 3rem;
--c--theme--font--weights--thin: 100;
--c--theme--font--weights--light: 300;
--c--theme--font--weights--regular: 400;
@@ -316,7 +322,7 @@
--c--theme--colors--primary-700
);
--c--components--button--secondary--border--color: var(
--c--theme--colors--primary-200
--c--theme--colors--greyscale-300
);
--c--components--button--tertiary--color: var(
--c--theme--colors--primary-text
@@ -339,6 +345,7 @@
--c--components--button--disabled--color: white;
--c--components--button--disabled--background--color: #b3cef0;
--c--components--la-gauffre--activated: false;
--c--components--home-proconnect--activated: false;
}
.cunningham-theme--dark {
@@ -501,10 +508,10 @@
--c--components--button--secondary--background--color-hover: #f6f6f6;
--c--components--button--secondary--background--color-active: #ededed;
--c--components--button--secondary--border--color: var(
--c--theme--colors--primary-600
--c--theme--colors--greyscale-300
);
--c--components--button--secondary--border--color-hover: var(
--c--theme--colors--primary-600
--c--theme--colors--greyscale-300
);
--c--components--button--secondary--color: var(
--c--theme--colors--primary-text
@@ -584,6 +591,7 @@
);
--c--components--forms-textarea--border-radius: 0;
--c--components--la-gauffre--activated: true;
--c--components--home-proconnect--activated: true;
}
.clr-secondary-text {
@@ -874,6 +882,10 @@
color: var(--c--theme--colors--primary-bg);
}
.clr-primary-action {
color: var(--c--theme--colors--primary-action);
}
.clr-primary-050 {
color: var(--c--theme--colors--primary-050);
}
@@ -1302,6 +1314,10 @@
background-color: var(--c--theme--colors--primary-bg);
}
.bg-primary-action {
background-color: var(--c--theme--colors--primary-action);
}
.bg-primary-050 {
background-color: var(--c--theme--colors--primary-050);
}
@@ -1550,6 +1566,31 @@
letter-spacing: var(--c--theme--font--letterspacings--t);
}
.fs-xl-alt {
font-size: var(--c--theme--font--sizes--xl-alt);
letter-spacing: var(--c--theme--font--letterspacings--xl-alt);
}
.fs-lg-alt {
font-size: var(--c--theme--font--sizes--lg-alt);
letter-spacing: var(--c--theme--font--letterspacings--lg-alt);
}
.fs-md-alt {
font-size: var(--c--theme--font--sizes--md-alt);
letter-spacing: var(--c--theme--font--letterspacings--md-alt);
}
.fs-sm-alt {
font-size: var(--c--theme--font--sizes--sm-alt);
letter-spacing: var(--c--theme--font--letterspacings--sm-alt);
}
.fs-xs-alt {
font-size: var(--c--theme--font--sizes--xs-alt);
letter-spacing: var(--c--theme--font--letterspacings--xs-alt);
}
.f-base {
font-family: var(--c--theme--font--families--base);
}
@@ -75,6 +75,7 @@ export const tokens = {
'danger-text': '#fff',
'card-border': '#ededed',
'primary-bg': '#FAFAFA',
'primary-action': '#1212FF',
'primary-050': '#F5F5FE',
'primary-150': '#E5EEFA',
'primary-950': '#1B1B35',
@@ -129,6 +130,11 @@ export const tokens = {
ml: '0.938rem',
xl: '1.25rem',
t: '0.6875rem',
'xl-alt': '5rem',
'lg-alt': '4.5rem',
'md-alt': '4rem',
'sm-alt': '3.5rem',
'xs-alt': '3rem',
},
weights: {
thin: 100,
@@ -315,7 +321,7 @@ export const tokens = {
color: 'white',
'color-hover': 'var(--c--theme--colors--primary-700)',
},
border: { color: 'var(--c--theme--colors--primary-200)' },
border: { color: 'var(--c--theme--colors--greyscale-300)' },
},
tertiary: {
color: 'var(--c--theme--colors--primary-text)',
@@ -330,6 +336,7 @@ export const tokens = {
disabled: { color: 'white', background: { color: '#b3cef0' } },
},
'la-gauffre': { activated: false },
'home-proconnect': { activated: false },
},
},
dark: {
@@ -502,8 +509,8 @@ export const tokens = {
secondary: {
background: { 'color-hover': '#F6F6F6', 'color-active': '#EDEDED' },
border: {
color: 'var(--c--theme--colors--primary-600)',
'color-hover': 'var(--c--theme--colors--primary-600)',
color: 'var(--c--theme--colors--greyscale-300)',
'color-hover': 'var(--c--theme--colors--greyscale-300)',
},
color: 'var(--c--theme--colors--primary-text)',
},
@@ -575,6 +582,7 @@ export const tokens = {
},
'forms-textarea': { 'border-radius': '0' },
'la-gauffre': { activated: true },
'home-proconnect': { activated: true },
},
},
},
@@ -1,7 +1,7 @@
import { Crisp } from 'crisp-sdk-web';
import fetchMock from 'fetch-mock';
import { useAuthStore } from '../useAuthStore';
import { gotoLogout } from '../utils';
jest.mock('crisp-sdk-web', () => ({
...jest.requireActual('crisp-sdk-web'),
@@ -17,7 +17,7 @@ jest.mock('crisp-sdk-web', () => ({
},
}));
describe('useAuthStore', () => {
describe('utils', () => {
afterEach(() => {
jest.clearAllMocks();
fetchMock.restore();
@@ -33,7 +33,7 @@ describe('useAuthStore', () => {
writable: true,
});
useAuthStore.getState().logout();
gotoLogout();
expect(Crisp.session.reset).toHaveBeenCalled();
});
@@ -0,0 +1,2 @@
export * from './useAuthQuery';
export * from './types';
@@ -1,4 +1,6 @@
import { fetchAPI } from '@/api';
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
import { APIError, fetchAPI } from '@/api';
import { User } from './types';
@@ -19,3 +21,16 @@ export const getMe = async (): Promise<User> => {
}
return response.json() as Promise<User>;
};
export const KEY_AUTH = 'auth';
export function useAuthQuery(
queryConfig?: UseQueryOptions<User, APIError, User>,
) {
return useQuery<User, APIError, User>({
queryKey: [KEY_AUTH],
queryFn: getMe,
staleTime: 1000 * 60 * 15, // 15 minutes
...queryConfig,
});
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

@@ -0,0 +1,47 @@
import { Loader } from '@openfun/cunningham-react';
import { useRouter } from 'next/router';
import { PropsWithChildren } from 'react';
import { Box } from '@/components';
import { useAuth } from '../hooks';
export const Auth = ({ children }: PropsWithChildren) => {
const { isLoading, pathAllowed, isFetchedAfterMount, authenticated } =
useAuth();
const { replace, pathname } = useRouter();
if (isLoading && !isFetchedAfterMount) {
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
<Loader />
</Box>
);
}
/**
* If the user is not authenticated and the path is not allowed, we redirect to the login page.
*/
if (!authenticated && !pathAllowed) {
void replace('/login');
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
<Loader />
</Box>
);
}
/**
* If the user is authenticated and the path is the login page, we redirect to the home page.
*/
if (pathname === '/login' && authenticated) {
void replace('/');
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
<Loader />
</Box>
);
}
return children;
};
@@ -0,0 +1,48 @@
import { Button } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { BoxButton } from '@/components';
import ProConnectImg from '../assets/button-proconnect.svg';
import { useAuth } from '../hooks';
import { gotoLogin, gotoLogout } from '../utils';
export const ButtonLogin = () => {
const { t } = useTranslation();
const { authenticated } = useAuth();
if (!authenticated) {
return (
<Button onClick={gotoLogin} color="primary-text" aria-label={t('Login')}>
{t('Login')}
</Button>
);
}
return (
<Button onClick={gotoLogout} color="primary-text" aria-label={t('Logout')}>
{t('Logout')}
</Button>
);
};
export const ProConnectButton = () => {
const { t } = useTranslation();
return (
<BoxButton
onClick={gotoLogin}
aria-label={t('Proconnect Login')}
$css={css`
background-color: var(--c--theme--colors--primary-text);
&:hover {
background-color: var(--c--theme--colors--primary-action);
}
`}
$radius="4px"
>
<ProConnectImg />
</BoxButton>
);
};
@@ -0,0 +1,2 @@
export * from './Auth';
export * from './ButtonLogin';
@@ -0,0 +1,5 @@
import { baseApiUrl } from '@/api';
export const PATH_AUTH_LOCAL_STORAGE = 'docs-path-auth';
export const LOGIN_URL = `${baseApiUrl()}authenticate/`;
export const LOGOUT_URL = `${baseApiUrl()}logout/`;
@@ -0,0 +1 @@
export * from './useAuth';
@@ -0,0 +1,34 @@
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useAuthQuery } from '../api';
import { getAuthUrl } from '../utils';
const regexpUrlsAuth = [/\/docs\/$/g, /\/docs$/g, /^\/$/g];
export const useAuth = () => {
const { data: user, ...authStates } = useAuthQuery();
const { pathname, replace } = useRouter();
const [pathAllowed, setPathAllowed] = useState<boolean>(
!regexpUrlsAuth.some((regexp) => !!pathname.match(regexp)),
);
useEffect(() => {
setPathAllowed(!regexpUrlsAuth.some((regexp) => !!pathname.match(regexp)));
}, [pathname]);
// Redirect to the path before login
useEffect(() => {
if (!user) {
return;
}
const authUrl = getAuthUrl();
if (authUrl) {
void replace(authUrl);
}
}, [user, replace]);
return { user, authenticated: !!user, pathAllowed, ...authStates };
};
@@ -0,0 +1,4 @@
export * from './api/types';
export * from './components';
export * from './hooks';
export * from './utils';
@@ -0,0 +1,27 @@
import { terminateCrispSession } from '@/services/Crisp';
import { LOGIN_URL, LOGOUT_URL, PATH_AUTH_LOCAL_STORAGE } from './conf';
export const getAuthUrl = () => {
const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE);
if (path_auth) {
localStorage.removeItem(PATH_AUTH_LOCAL_STORAGE);
return path_auth;
}
};
export const setAuthUrl = () => {
if (window.location.pathname !== '/') {
localStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.pathname);
}
};
export const gotoLogin = () => {
setAuthUrl();
window.location.replace(LOGIN_URL);
};
export const gotoLogout = () => {
terminateCrispSession();
window.location.replace(LOGOUT_URL);
};
@@ -92,6 +92,13 @@ export function AIGroupButton() {
return null;
}
const canAITransform = currentDoc.abilities.ai_transform;
const canAITranslate = currentDoc.abilities.ai_translate;
if (!canAITransform && !canAITranslate) {
return null;
}
return (
<Components.Generic.Menu.Root>
<Components.Generic.Menu.Trigger>
@@ -111,79 +118,85 @@ export function AIGroupButton() {
className="bn-menu-dropdown bn-drag-handle-menu"
sub={true}
>
<AIMenuItemTransform
action="prompt"
docId={currentDoc.id}
icon={
<Text $isMaterialIcon $size="s">
text_fields
</Text>
}
>
{t('Use as prompt')}
</AIMenuItemTransform>
<AIMenuItemTransform
action="rephrase"
docId={currentDoc.id}
icon={
<Text $isMaterialIcon $size="s">
refresh
</Text>
}
>
{t('Rephrase')}
</AIMenuItemTransform>
<AIMenuItemTransform
action="summarize"
docId={currentDoc.id}
icon={
<Text $isMaterialIcon $size="s">
summarize
</Text>
}
>
{t('Summarize')}
</AIMenuItemTransform>
<AIMenuItemTransform
action="correct"
docId={currentDoc.id}
icon={
<Text $isMaterialIcon $size="s">
check
</Text>
}
>
{t('Correct')}
</AIMenuItemTransform>
<Components.Generic.Menu.Root position="right" sub={true}>
<Components.Generic.Menu.Trigger sub={false}>
<Components.Generic.Menu.Item
className="bn-menu-item"
subTrigger={true}
>
<Box $direction="row" $gap="0.6rem">
{canAITransform && (
<>
<AIMenuItemTransform
action="prompt"
docId={currentDoc.id}
icon={
<Text $isMaterialIcon $size="s">
translate
text_fields
</Text>
{t('Language')}
</Box>
</Components.Generic.Menu.Item>
</Components.Generic.Menu.Trigger>
<Components.Generic.Menu.Dropdown
sub={true}
className="bn-menu-dropdown"
>
{languages.map((language) => (
<AIMenuItemTranslate
key={language.value}
language={language.value}
docId={currentDoc.id}
}
>
{t('Use as prompt')}
</AIMenuItemTransform>
<AIMenuItemTransform
action="rephrase"
docId={currentDoc.id}
icon={
<Text $isMaterialIcon $size="s">
refresh
</Text>
}
>
{t('Rephrase')}
</AIMenuItemTransform>
<AIMenuItemTransform
action="summarize"
docId={currentDoc.id}
icon={
<Text $isMaterialIcon $size="s">
summarize
</Text>
}
>
{t('Summarize')}
</AIMenuItemTransform>
<AIMenuItemTransform
action="correct"
docId={currentDoc.id}
icon={
<Text $isMaterialIcon $size="s">
check
</Text>
}
>
{t('Correct')}
</AIMenuItemTransform>
</>
)}
{canAITranslate && (
<Components.Generic.Menu.Root position="right" sub={true}>
<Components.Generic.Menu.Trigger sub={false}>
<Components.Generic.Menu.Item
className="bn-menu-item"
subTrigger={true}
>
{language.display_name}
</AIMenuItemTranslate>
))}
</Components.Generic.Menu.Dropdown>
</Components.Generic.Menu.Root>
<Box $direction="row" $gap="0.6rem">
<Text $isMaterialIcon $size="s">
translate
</Text>
{t('Language')}
</Box>
</Components.Generic.Menu.Item>
</Components.Generic.Menu.Trigger>
<Components.Generic.Menu.Dropdown
sub={true}
className="bn-menu-dropdown"
>
{languages.map((language) => (
<AIMenuItemTranslate
key={language.value}
language={language.value}
docId={currentDoc.id}
>
{language.display_name}
</AIMenuItemTranslate>
))}
</Components.Generic.Menu.Dropdown>
</Components.Generic.Menu.Root>
)}
</Components.Generic.Menu.Dropdown>
</Components.Generic.Menu.Root>
);
@@ -1,4 +1,9 @@
import { Dictionary, locales } from '@blocknote/core';
import {
BlockNoteSchema,
Dictionary,
locales,
withPageBreak,
} from '@blocknote/core';
import '@blocknote/core/fonts/inter.css';
import { BlockNoteView } from '@blocknote/mantine';
import '@blocknote/mantine/style.css';
@@ -6,106 +11,23 @@ import { useCreateBlockNote } from '@blocknote/react';
import { HocuspocusProvider } from '@hocuspocus/provider';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import * as Y from 'yjs';
import { Box, TextErrors } from '@/components';
import { useAuthStore } from '@/core/auth';
import { useAuth } from '@/features/auth';
import { Doc } from '@/features/docs/doc-management';
import { useUploadFile } from '../hook';
import { useHeadings } from '../hook/useHeadings';
import useSaveDoc from '../hook/useSaveDoc';
import { useEditorStore } from '../stores';
import { cssEditor } from '../styles';
import { randomColor } from '../utils';
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
import { BlockNoteToolbar } from './BlockNoteToolbar';
const cssEditor = (readonly: boolean) => css`
&,
& > .bn-container,
& .ProseMirror {
height: 100%;
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 50px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 43px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 35px;
}
h1 {
font-size: 1.875rem;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.25rem;
}
a {
color: var(--c--theme--colors--greyscale-500);
cursor: pointer;
}
.bn-block-group
.bn-block-group
.bn-block-outer:not([data-prev-depth-changed]):before {
border-left: none;
}
}
.bn-editor {
color: var(--c--theme--colors--greyscale-700);
}
.bn-block-outer:not(:first-child) {
&:has(h1) {
padding-top: 32px;
}
&:has(h2) {
padding-top: 24px;
}
&:has(h3) {
padding-top: 16px;
}
}
& .bn-inline-content code {
background-color: gainsboro;
padding: 2px;
border-radius: 4px;
}
@media screen and (width <= 560px) {
& .bn-editor {
${readonly && `padding-left: 10px;`}
}
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 46px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 40px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 40px;
}
& .bn-editor h1 {
font-size: 1.6rem;
}
& .bn-editor h2 {
font-size: 1.35rem;
}
& .bn-editor h3 {
font-size: 1.2rem;
}
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
font-size: 14px;
}
}
`;
export const blockNoteSchema = withPageBreak(BlockNoteSchema.create());
interface BlockNoteEditorProps {
doc: Doc;
@@ -113,7 +35,7 @@ interface BlockNoteEditorProps {
}
export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
const { userData } = useAuthStore();
const { user } = useAuth();
const { setEditor } = useEditorStore();
const { t } = useTranslation();
@@ -126,7 +48,8 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
const collabName = readOnly
? 'Reader'
: userData?.full_name || userData?.email || t('Anonymous');
: user?.full_name || user?.email || t('Anonymous');
const showCursorLabels: 'always' | 'activity' | (string & {}) = 'activity';
const editor = useCreateBlockNote(
{
@@ -138,33 +61,50 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
color: randomColor(),
},
/**
* We re-use the blocknote code to render the cursor but we:
* - fix rendering issue with Firefox
* - We don't want to show the cursor when anonymous users
* We render the cursor with a custom element to:
* - fix rendering issue with the default cursor
* - hide the cursor when anonymous users
*/
renderCursor: (user: { color: string; name: string }) => {
const cursor = document.createElement('span');
const cursorElement = document.createElement('span');
if (user.name === 'Reader') {
return cursor;
return cursorElement;
}
cursor.classList.add('collaboration-cursor__caret');
cursor.setAttribute('style', `border-color: ${user.color}`);
cursorElement.classList.add('collaboration-cursor-custom__base');
const caretElement = document.createElement('span');
caretElement.classList.add('collaboration-cursor-custom__caret');
caretElement.setAttribute('spellcheck', `false`);
caretElement.setAttribute('style', `background-color: ${user.color}`);
const label = document.createElement('span');
if (showCursorLabels === 'always') {
cursorElement.setAttribute('data-active', '');
}
label.classList.add('collaboration-cursor__label');
label.setAttribute('style', `background-color: ${user.color}`);
label.insertBefore(document.createTextNode(user.name), null);
const labelElement = document.createElement('span');
cursor.insertBefore(label, null);
labelElement.classList.add('collaboration-cursor-custom__label');
labelElement.setAttribute('spellcheck', `false`);
labelElement.setAttribute(
'style',
`background-color: ${user.color};border: 1px solid ${user.color};`,
);
labelElement.insertBefore(document.createTextNode(user.name), null);
return cursor;
caretElement.insertBefore(labelElement, null);
cursorElement.insertBefore(document.createTextNode('\u2060'), null); // Non-breaking space
cursorElement.insertBefore(caretElement, null);
cursorElement.insertBefore(document.createTextNode('\u2060'), null); // Non-breaking space
return cursorElement;
},
showCursorLabels: showCursorLabels as 'always' | 'activity',
},
dictionary: locales[lang as keyof typeof locales] as Dictionary,
uploadFile,
schema: blockNoteSchema,
},
[collabName, lang, provider, uploadFile],
);
@@ -197,10 +137,12 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
<BlockNoteView
editor={editor}
formattingToolbar={false}
slashMenu={false}
editable={!readOnly}
theme="light"
>
<BlockNoteToolbar />
<BlockNoteSuggestionMenu />
</BlockNoteView>
</Box>
);
@@ -225,6 +167,7 @@ export const BlockNoteEditorVersion = ({
},
provider: undefined,
},
schema: blockNoteSchema,
},
[initialContent],
);
@@ -0,0 +1,35 @@
import { combineByGroup, filterSuggestionItems } from '@blocknote/core';
import '@blocknote/mantine/style.css';
import {
SuggestionMenuController,
getDefaultReactSlashMenuItems,
getPageBreakReactSlashMenuItems,
useBlockNoteEditor,
} from '@blocknote/react';
import React, { useMemo } from 'react';
import { DocsBlockNoteEditor } from '../types';
export const BlockNoteSuggestionMenu = () => {
const editor = useBlockNoteEditor() as DocsBlockNoteEditor;
const getSlashMenuItems = useMemo(() => {
return async (query: string) =>
Promise.resolve(
filterSuggestionItems(
combineByGroup(
getDefaultReactSlashMenuItems(editor),
getPageBreakReactSlashMenuItems(editor),
),
query,
),
);
}, [editor]);
return (
<SuggestionMenuController
triggerCharacter="/"
getItems={getSlashMenuItems}
/>
);
};
@@ -65,7 +65,7 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
$css="overflow-x: clip; flex: 1;"
$position="relative"
>
<Box $css="flex:1;" $overflow="auto" $position="relative">
<Box $css="flex:1;" $position="relative" $width="100%">
{isVersion ? (
<DocVersionEditor docId={doc.id} versionId={versionId} />
) : (
@@ -1,9 +1,9 @@
import { BlockNoteEditor } from '@blocknote/core';
import { useEffect } from 'react';
import { useHeadingStore } from '../stores';
import { DocsBlockNoteEditor } from '../types';
export const useHeadings = (editor: BlockNoteEditor) => {
export const useHeadings = (editor: DocsBlockNoteEditor) => {
const { setHeadings, resetHeadings } = useHeadingStore();
useEffect(() => {
@@ -1,9 +1,10 @@
import { BlockNoteEditor } from '@blocknote/core';
import { create } from 'zustand';
import { DocsBlockNoteEditor } from '../types';
export interface UseEditorstore {
editor?: BlockNoteEditor;
setEditor: (editor: BlockNoteEditor | undefined) => void;
editor?: DocsBlockNoteEditor;
setEditor: (editor: DocsBlockNoteEditor | undefined) => void;
}
export const useEditorStore = create<UseEditorstore>((set) => ({
@@ -1,7 +1,6 @@
import { BlockNoteEditor } from '@blocknote/core';
import { create } from 'zustand';
import { HeadingBlock } from '../types';
import { DocsBlockNoteEditor, HeadingBlock } from '../types';
const recursiveTextContent = (content: HeadingBlock['content']): string => {
if (!content) {
@@ -21,7 +20,7 @@ const recursiveTextContent = (content: HeadingBlock['content']): string => {
export interface UseHeadingStore {
headings: HeadingBlock[];
setHeadings: (editor: BlockNoteEditor) => void;
setHeadings: (editor: DocsBlockNoteEditor) => void;
resetHeadings: () => void;
}
@@ -0,0 +1,121 @@
import { css } from 'styled-components';
export const cssEditor = (readonly: boolean) => css`
&,
& > .bn-container,
& .ProseMirror {
height: 100%;
.collaboration-cursor-custom__base {
position: relative;
}
.collaboration-cursor-custom__caret {
position: absolute;
height: 100%;
width: 2px;
bottom: 4%;
left: -1px;
}
.collaboration-cursor-custom__label {
color: #0d0d0d;
font-size: 12px;
font-weight: 600;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
position: absolute;
top: -17px;
padding: 0px 6px;
border-radius: 0px;
white-space: nowrap;
transition: clip-path 0.3s ease-in-out;
border-radius: 4px 4px 4px 0;
box-shadow: inset -2px 2px 6px #ffffff00;
clip-path: polygon(0 85%, 4% 85%, 4% 100%, 0% 100%);
}
.collaboration-cursor-custom__base[data-active]
.collaboration-cursor-custom__label {
pointer-events: none;
box-shadow: inset -2px 2px 6px #ffffff88;
clip-path: polygon(0 0, 100% 0%, 100% 100%, 0% 100%);
}
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 50px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 43px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 35px;
}
h1 {
font-size: 1.875rem;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.25rem;
}
a {
color: var(--c--theme--colors--greyscale-500);
cursor: pointer;
}
.bn-block-group
.bn-block-group
.bn-block-outer:not([data-prev-depth-changed]):before {
border-left: none;
}
}
.bn-editor {
color: var(--c--theme--colors--greyscale-700);
}
.bn-block-outer:not(:first-child) {
&:has(h1) {
padding-top: 32px;
}
&:has(h2) {
padding-top: 24px;
}
&:has(h3) {
padding-top: 16px;
}
}
& .bn-inline-content code {
background-color: gainsboro;
padding: 2px;
border-radius: 4px;
}
@media screen and (width <= 560px) {
& .bn-editor {
${readonly && `padding-left: 10px;`}
}
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 46px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 40px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 40px;
}
& .bn-editor h1 {
font-size: 1.6rem;
}
& .bn-editor h2 {
font-size: 1.35rem;
}
& .bn-editor h3 {
font-size: 1.2rem;
}
.bn-block-content[data-is-empty-and-focused][data-content-type='paragraph']
.bn-inline-content:has(> .ProseMirror-trailingBreak:only-child)::before {
font-size: 14px;
}
}
`;
@@ -1,3 +1,7 @@
import { BlockNoteEditor } from '@blocknote/core';
import { blockNoteSchema } from './components/BlockNoteEditor';
export interface DocAttachment {
file: string;
}
@@ -12,3 +16,9 @@ export type HeadingBlock = {
level: number;
};
};
export type DocsBlockNoteEditor = BlockNoteEditor<
typeof blockNoteSchema.blockSchema,
typeof blockNoteSchema.inlineContentSchema,
typeof blockNoteSchema.styleSchema
>;
@@ -1,5 +1,3 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
import {
Tooltip,
VariantType,
@@ -114,7 +112,6 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
defaultValue={isUntitled ? undefined : titleDisplay}
onKeyDownCapture={handleKeyDown}
suppressContentEditableWarning={true}
aria-label="doc title input"
onBlurCapture={(event) =>
handleTitleSubmit(event.target.textContent || '')
}
@@ -98,7 +98,61 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
const exporter = new PDFExporter(
editor.schema,
pdfDefaultSchemaMappings,
{
...pdfDefaultSchemaMappings,
blockMapping: {
...pdfDefaultSchemaMappings.blockMapping,
heading: (block, exporter) => {
const PIXELS_PER_POINT = 0.75;
const MERGE_RATIO = 7.5;
const FONT_SIZE = 16;
const fontSizeEM =
block.props.level === 1
? 2
: block.props.level === 2
? 1.5
: 1.17;
return (
<Text
style={{
fontSize: fontSizeEM * FONT_SIZE * PIXELS_PER_POINT,
fontWeight: 700,
marginTop: `${fontSizeEM * MERGE_RATIO}px`,
marginBottom: `${fontSizeEM * MERGE_RATIO}px`,
}}
>
{exporter.transformInlineContent(block.content)}
</Text>
);
},
paragraph: (block, exporter) => {
/**
* Breakline in the editor are not rendered in the PDF
* By adding a space if the block is empty we ensure that the block is rendered
*/
if (Array.isArray(block.content)) {
block.content.forEach((content) => {
if (content.type === 'text' && !content.text) {
content.text = ' ';
}
});
if (!block.content.length) {
block.content.push({
styles: {},
text: ' ',
type: 'text',
});
}
}
return (
<Text key={block.id}>
{exporter.transformInlineContent(block.content)}
</Text>
);
},
},
},
{
resolveFileUrl: async (url) =>
exportResolveFileUrl(url, defaultExporter.options.resolveFileUrl),
@@ -179,9 +233,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
$gap="1rem"
>
<Text $variation="600" $size="sm">
{t(
'Upload your docs to a Microsoft Word, Open Office or PDF document.',
)}
{t('Download your document in a .docx or .pdf format.')}
</Text>
<Select
clearable={false}
@@ -197,7 +249,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
fullWidth
label={t('Format')}
options={[
{ label: t('Word / Open Office'), value: DocDownloadFormat.DOCX },
{ label: t('Docx'), value: DocDownloadFormat.DOCX },
{ label: t('PDF'), value: DocDownloadFormat.PDF },
]}
value={format}
@@ -1,4 +1,4 @@
import { User } from '@/core';
import { User } from '@/features/auth';
export interface Access {
id: string;
@@ -48,10 +48,20 @@ export interface Doc {
abilities: {
accesses_manage: boolean;
accesses_view: boolean;
attachment_upload: true;
ai_transform: boolean;
ai_translate: boolean;
attachment_upload: boolean;
children_create: boolean;
children_list: boolean;
collaboration_auth: boolean;
destroy: boolean;
favorite: boolean;
invite_owner: boolean;
link_configuration: boolean;
media_auth: boolean;
move: boolean;
partial_update: boolean;
restore: boolean;
retrieve: boolean;
update: boolean;
versions_destroy: boolean;
@@ -51,7 +51,7 @@ export const DocSearchModal = ({ ...modalProps }: DocSearchModalProps) => {
return {
groupName: docs.length > 0 ? t('Select a document') : '',
elements: search ? docs : [],
emptyString: t('No document found'),
//emptyString: t('No document found'),
endActions: hasNextPage
? [{ content: <InView onChange={() => void fetchNextPage()} /> }]
: [],
@@ -96,6 +96,20 @@ export const DocSearchModal = ({ ...modalProps }: DocSearchModalProps) => {
renderElement={(doc) => <DocSearchItem doc={doc} />}
/>
)}
{/* Message accessible pour les résultats vides */}
{search && docsData.elements.length === 0 && (
<p
role="alert"
aria-live="polite"
style={{
textAlign: 'center',
marginTop: '1rem',
color: '#666',
}}
>
{t('No document found')}
</p>
)}
</Box>
</QuickSearch>
</Box>
@@ -1,7 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { User } from '@/core/auth';
import { User } from '@/features/auth';
import {
Access,
Doc,
@@ -1,7 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { User } from '@/core/auth';
import { User } from '@/features/auth';
import { Doc, Role } from '@/features/docs/doc-management';
import { Invitation, OptionType } from '@/features/docs/doc-share/types';
import { ContentLanguage } from '@/i18n/types';
@@ -1,7 +1,7 @@
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
import { User } from '@/core/auth';
import { User } from '@/features/auth';
import { Doc } from '@/features/docs/doc-management';
export type UsersParams = {
@@ -9,8 +9,8 @@ import { css } from 'styled-components';
import { APIError } from '@/api';
import { Box } from '@/components';
import { User } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { User } from '@/features/auth';
import { Doc, Role } from '@/features/docs';
import { useLanguage } from '@/i18n/hooks/useLanguage';
@@ -2,8 +2,8 @@ import { Button } from '@openfun/cunningham-react';
import { css } from 'styled-components';
import { Box, Icon, Text } from '@/components';
import { User } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { User } from '@/features/auth';
type Props = {
user: User;
@@ -7,8 +7,8 @@ import {
DropdownMenuOption,
IconOptions,
} from '@/components';
import { User } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { User } from '@/features/auth';
import { Doc, Role } from '@/features/docs/doc-management';
import { useDeleteDocInvitation, useUpdateDocInvitation } from '../api';
@@ -10,7 +10,7 @@ import {
QuickSearchData,
QuickSearchGroup,
} from '@/components/quick-search/';
import { User } from '@/core';
import { User } from '@/features/auth';
import { Access, Doc } from '@/features/docs';
import { useResponsiveStore } from '@/stores';
import { isValidEmail } from '@/utils';
@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, Icon, Text } from '@/components';
import { User } from '@/core';
import { User } from '@/features/auth';
import { SearchUserRow } from './SearchUserRow';
@@ -3,8 +3,8 @@ import {
QuickSearchItemContent,
QuickSearchItemContentProps,
} from '@/components/quick-search';
import { User } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { User } from '@/features/auth';
import { UserAvatar } from './UserAvatar';
@@ -1,8 +1,8 @@
import { css } from 'styled-components';
import { Box } from '@/components';
import { User } from '@/core';
import { tokens } from '@/cunningham';
import { User } from '@/features/auth';
const colors = tokens.themes.default.theme.colors;
@@ -1,16 +1,16 @@
import { useAuthStore } from '@/core/auth';
import { useAuth } from '@/features/auth';
import { Access, Role } from '@/features/docs/doc-management';
export const useWhoAmI = (access: Access) => {
const { userData } = useAuthStore();
const { user } = useAuth();
const isMyself = userData?.id === access.user.id;
const isMyself = user?.id === access.user.id;
const rolesAllowed = access.abilities.set_role_to;
const isLastOwner =
!rolesAllowed.length && access.role === Role.OWNER && isMyself;
const isOtherOwner = access.role === Role.OWNER && userData?.id && !isMyself;
const isOtherOwner = access.role === Role.OWNER && user?.id && !isMyself;
return {
isLastOwner,
@@ -1,4 +1,4 @@
import { User } from '@/core/auth';
import { User } from '@/features/auth';
import { Role } from '@/features/docs';
export interface Invitation {
@@ -1,8 +1,8 @@
import { BlockNoteEditor } from '@blocknote/core';
import { useState } from 'react';
import { BoxButton, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { DocsBlockNoteEditor } from '@/features/docs/doc-editor';
import { useResponsiveStore } from '@/stores';
const leftPaddingMap: { [key: number]: string } = {
@@ -17,7 +17,7 @@ export type HeadingsHighlight = {
}[];
interface HeadingProps {
editor: BlockNoteEditor;
editor: DocsBlockNoteEditor;
level: number;
text: string;
headingId: string;

Some files were not shown because too many files have changed in this diff Show More