Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4d968eff3 | |||
| 12f7c8b371 | |||
| 70e3e9e7d4 | |||
| 296dbb7957 | |||
| 3827f0f799 | |||
| d89e3dc6d4 | |||
| 91cf5f9367 | |||
| 5cc4b07cf6 | |||
| 0cfc242e09 | |||
| a6b3cfdb0c | |||
| 5ead18c94c | |||
| 5eeb8cae5c | |||
| e0426ca803 | |||
| 68bf024005 | |||
| fdd1068c90 | |||
| ba695bf647 | |||
| 27e7aec193 | |||
| 58b712a1de | |||
| 08f9036523 | |||
| a01790dc0b | |||
| ebe3efc8f7 | |||
| 66fbf27913 | |||
| 20e4a4e42a | |||
| 1aa4844eeb | |||
| 4bb9c092cb | |||
| c493eb8924 | |||
| 40fdf97520 | |||
| 91b10e75dd | |||
| 7a6da10e1c | |||
| 004e8ec645 | |||
| e14622ff66 | |||
| 75fd994a5b | |||
| 239933aef3 | |||
| 95c06d68cb | |||
| 0d12278474 | |||
| a1bca9c436 | |||
| 8ce13075c1 | |||
| 225f5a8abb | |||
| 71a8765770 | |||
| 9186a101ec | |||
| d02fa1ddd4 | |||
| 74e816c479 | |||
| 5d9eb2d694 | |||
| 33e168ba17 | |||
| f1c0f6bba0 | |||
| 1fd66d3081 | |||
| bae8c4c563 | |||
| 0dae35dab1 | |||
| 8f4fd15495 | |||
| cc6ce4a945 | |||
| 929a50b573 | |||
| 83dfd26d1c | |||
| addc6a331f | |||
| 044c8f0bbd | |||
| 7e62dcf1fc | |||
| e7742d914c | |||
| b1a5c17d75 | |||
| bd68396e52 | |||
| 9f1ae58ead | |||
| f597549e96 | |||
| 847e120d67 | |||
| 6682ddafff | |||
| 2f23404003 | |||
| 6d4210d34b | |||
| 85f7598be8 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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') }}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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.
|
||||
@@ -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
@@ -15,7 +15,7 @@ services:
|
||||
- "1081:1080"
|
||||
|
||||
minio:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
# user: ${DOCKER_USER:-1000}
|
||||
image: minio/minio
|
||||
environment:
|
||||
- MINIO_ROOT_USER=impress
|
||||
|
||||
Generated
+3853
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@blocknote/core": "^0.23.4",
|
||||
"next": "^15.1.7"
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 "
|
||||
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
|
||||
@@ -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 "
|
||||
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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": "*",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+3
-3
@@ -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';
|
||||
+16
-1
@@ -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>
|
||||
);
|
||||
|
||||
+46
-103
@@ -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],
|
||||
);
|
||||
|
||||
+35
@@ -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;
|
||||
|
||||
+15
-1
@@ -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
-1
@@ -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 = {
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+2
-2
@@ -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
Reference in New Issue
Block a user