Compare commits

..

9 Commits

Author SHA1 Message Date
rvveber ab74aeff5c fix: Ensure federated types are downloaded before type compiling/ type checking
- Adds a function to the webpack.config and documentation to make sure host types are downloaded to the plugin before the first type compile/ type check is happening

Avoids an error where on cold start the types were not yet downloaded but types were already checked.
2025-11-12 17:16:11 +01:00
rvveber 482975c2b5 refine documentation 2025-11-04 18:07:27 +01:00
rvveber b4afee1b8c refine documentation 2025-11-04 17:43:42 +01:00
rvveber a1cb4cee95 refine documentation 2025-11-04 17:34:11 +01:00
rvveber 17b668485d refine documentation 2025-11-04 13:57:06 +01:00
rvveber 5ce23661e6 update documentation 2025-11-04 13:46:44 +01:00
rvveber bc360f31fc Improved documentation 2025-11-04 13:32:36 +01:00
rvveber a174a00af8 improve documentation accuracy 2025-11-04 11:43:27 +01:00
rvveber f0f2ca5edd Integrates plugin system POC to test
... more fine grained commits later
2025-11-04 10:22:58 +01:00
243 changed files with 19451 additions and 10428 deletions
-27
View File
@@ -1,27 +0,0 @@
name: Label Preview
on:
pull_request:
types: [labeled, opened]
permissions:
pull-requests: write
jobs:
comment:
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'preview')
steps:
- uses: thollander/actions-comment-pull-request@v3
with:
message: |
:rocket: Preview will be available at [https://${{ github.event.pull_request.number }}-docs.ppr-docs.beta.numerique.gouv.fr/](https://${{ github.event.pull_request.number }}-docs.ppr-docs.beta.numerique.gouv.fr/)
You can use the existing account with these credentials:
- username: `docs`
- password: `docs`
You can also create a new account if you want to.
Once this Pull Request is merged, the preview will be destroyed.
comment-tag: preview-url
-3
View File
@@ -75,6 +75,3 @@ db.sqlite3
.vscode/
*.iml
.devcontainer
# Cursor rules
.cursorrules
+4 -70
View File
@@ -6,62 +6,12 @@ and this project adheres to
## [Unreleased]
## [3.9.0] - 2025-11-10
### Added
- ✨(frontend) create skeleton component for DocEditor #1491
- ✨(frontend) add an EmojiPicker in the document tree and title #1381
- ✨(frontend) ajustable left panel #1456
### Changed
- ♻️(frontend) adapt custom blocks to new implementation #1375
- ♻️(backend) increase user short_name field length #1510
- 🚸(frontend) separate viewers from editors #1509
### Fixed
- 🐛(frontend) fix duplicate document entries in grid #1479
- 🐛(backend) fix trashbin list #1520
- ♿(frontend) improve accessibility:
- ♿(frontend) remove empty alt on logo due to Axe a11y error #1516
- 🐛(backend) fix s3 version_id validation #1543
- 🐛(frontend) retry check media status after page reload #1555
- 🐛(frontend) fix Interlinking memory leak #1560
- 🐛(frontend) button new doc UI fix #1557
- 🐛(frontend) interlinking UI fix #1557
## [3.8.2] - 2025-10-17
### Fixed
- 🐛(service-worker) fix sw registration and page reload logic #1500
## [3.8.1] - 2025-10-17
### Fixed
- ⚡️(backend) improve trashbin endpoint performance #1495
- 🐛(backend) manage invitation partial update without email #1494
- ♿(frontend) improve accessibility:
- ♿ add missing aria-label to add sub-doc button for accessibility #1480
- ♿ add missing aria-label to more options button on sub-docs #1481
### Removed
- 🔥(backend) remove treebeard form for the document admin #1470
## [3.8.0] - 2025-10-14
### Added
- ✨(frontend) add pdf block to the editor #1293
- ✨List and restore deleted docs #1450
### Changed
- ♻️(frontend) Refactor Auth component for improved redirection logic #1461
- ♻️(frontend) replace Arial font-family with token font #1411
- ♿(frontend) improve accessibility:
- ♿(frontend) enable enter key to open documentss #1354
@@ -80,11 +30,10 @@ and this project adheres to
- ♿ add h1 for SR on 40X pages and remove alt texts #1438
- ♿ update labels and shared document icon accessibility #1442
- 🍱(frontend) Fonts GDPR compliants #1453
- ♻️(service-worker) improve SW registration and update handling #1473
### Fixed
- 🐛(backend) duplicate sub docs as root for reader users #1385
- 🐛(backend) duplicate sub docs as root for reader users
- ⚗️(service-worker) remove index from cache first strategy #1395
- 🐛(frontend) fix 404 page when reload 403 page #1402
- 🐛(frontend) fix legacy role computation #1376
@@ -95,24 +44,14 @@ and this project adheres to
- 🐛(frontend) exclude h4-h6 headings from table of contents #1441
- 🔒(frontend) prevent readers from changing callout emoji #1449
- 🐛(frontend) fix overlapping placeholders in multi-column layout #1455
- 🐛(backend) filter invitation with case insensitive email #1457
- 🐛(backend) filter invitation with case insensitive email
- 🐛(frontend) reduce no access image size from 450 to 300 #1463
- 🐛(frontend) preserve interlink style on drag-and-drop in editor #1460
- ✨(frontend) load docs logo from public folder via url #1462
- 🔧(keycloak) Fix https required issue in dev mode #1286
## Removed
- 🔥(frontend) remove custom DividerBlock ##1375
## [3.7.0] - 2025-09-12
### Added
- ✨(api) add API route to fetch document content #1206
- ✨(frontend) doc emojis improvements #1381
- add an EmojiPicker in the document tree and document title
- remove emoji buttons in menus
### Changed
@@ -126,8 +65,6 @@ and this project adheres to
- ✨unify tab focus style for better visual consistency #1341
- ♿hide decorative icons, label menus, avoid accessible name… #1362
- ♻️(tilt) use helm dev-backend chart
- 🩹(frontend) on main pages do not display leading emoji as page icon #1381
- 🩹(frontend) handle properly emojis in interlinking #1381
### Removed
@@ -172,6 +109,7 @@ and this project adheres to
- 🐛(frontend) fix dnd conflict with tree and Blocknote #1328
- 🐛(frontend) fix display bug on homepage #1332
- 🐛link role update #1287
- 🔧(keycloak) Fix https required issue in dev mode #1286
## [3.5.0] - 2025-07-31
@@ -843,11 +781,7 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.9.0...main
[v3.9.0]: https://github.com/suitenumerique/docs/releases/v3.9.0
[v3.8.2]: https://github.com/suitenumerique/docs/releases/v3.8.2
[v3.8.1]: https://github.com/suitenumerique/docs/releases/v3.8.1
[v3.8.0]: https://github.com/suitenumerique/docs/releases/v3.8.0
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.7.0...main
[v3.7.0]: https://github.com/suitenumerique/docs/releases/v3.7.0
[v3.6.0]: https://github.com/suitenumerique/docs/releases/v3.6.0
[v3.5.0]: https://github.com/suitenumerique/docs/releases/v3.5.0
+2
View File
@@ -99,6 +99,8 @@ These are the environment variables you can set for the `impress-backend` contai
| STORAGES_STATICFILES_BACKEND | | whitenoise.storage.CompressedManifestStaticFilesStorage |
| THEME_CUSTOMIZATION_CACHE_TIMEOUT | Cache duration for the customization settings | 86400 |
| THEME_CUSTOMIZATION_FILE_PATH | Full path to the file customizing the theme. An example is provided in src/backend/impress/configuration/theme/default.json | BASE_DIR/impress/configuration/theme/default.json |
| PLUGINS_CONFIG_FILE_PATH | Full path to the JSON file containing the plugins configuration loaded by the backend. Example: src/backend/impress/configuration/plugins/default.json | BASE_DIR/impress/configuration/plugins/default.json |
| PLUGINS_CONFIG_CACHE_TIMEOUT | Time in seconds the plugins configuration file is cached by the backend before being reloaded. Default is 2 hours (7200 seconds). | 7200 |
| TRASHBIN_CUTOFF_DAYS | Trashbin cutoff | 30 |
| USER_OIDC_ESSENTIAL_CLAIMS | Essential claims in OIDC token | [] |
| Y_PROVIDER_API_BASE_URL | Y Provider url | |
+589
View File
@@ -0,0 +1,589 @@
# Frontend Plugin System
## Table of Contents
- [Overview](#overview "Go to Overview section")
- [Getting Started: Building Your First Plugin](#getting-started-building-your-first-plugin "Go to the Getting Started guide")
- [1. Prepare the Host Environment](#1-prepare-the-host-environment "Step 1: Prepare the host")
- [2. Scaffolding a New Plugin Project](#2-scaffolding-a-new-plugin-project "Step 2: Scaffold the plugin")
- [3. Creating a Plugin Component](#3-creating-a-plugin-component "Step 3: Create the React component")
- [4. Federation Configuration](#4-federation-configuration "Step 4: Configure module federation")
- [5. Enabling Type-Sharing for Intellisense](#5-enabling-type-sharing-for-intellisense "Step 5: Enable type-sharing")
- [6. Running and Configuring Your Plugin](#6-running-and-configuring-your-plugin "Step 6: Run and configure")
- [Host-Plugin Interaction](#host-plugin-interaction "How the host and plugin interact")
- [Host Exports](#host-exports "What the host exports")
- [Choosing Shared Dependencies](#choosing-shared-dependencies "Learn about shared dependencies")
- [Development Workflow](#development-workflow "Go to Development Workflow section")
- [Test and Debug](#test-and-debug "How to test and debug")
- [Best Practices](#best-practices "View best practices")
- [Plugin Configuration File Reference](#plugin-configuration-file-reference "Go to the Configuration File reference")
- [Configuration Structure](#configuration-structure "See the config file structure")
- [Injection Position Examples](#injection-position-examples "See examples of injection positions")
- [Releasing a Plugin](#releasing-a-plugin "How to release a plugin")
- [Deploying Docs with Plugins](#deploying-docs-with-plugins "How to deploy plugins in production")
## Overview
The plugin system allows developers to extend the application's functionality and appearance without modifying the core.
It's ideal for teams or third parties to add custom features.
<br>
### Glossary
- **Remote**: An application exposing components via module federation.
- **Host**: The main entry point application. This is Docs itself ("impress").
- **Plugin**: A remote module integrated into the host to provide UI components.
- **Module Federation**: The technology that enables runtime module sharing between separate applications.
<br>
### Features and Limitations
**Features:**
- Add new UI components.
- Reuse host UI components.
- Dynamically inject components via CSS selectors and a [configuration file](#plugin-configuration-file-reference "See the configuration file reference").
- Integrate without rebuilding or redeploying the host application.
- Build and version plugins independently.
<br>
**Limitations:**
- Focused on DOM/UI customisations; you cannot add Next.js routes or other server-side features.
- Runs client-side without direct host state access. <br>
Shared caches (e.g., React Query) only work if the dependency is also [shared as a singleton](#choosing-shared-dependencies "Learn about shared dependencies").
- Host upgrades may require tweaking CSS selectors <br>
and matching versions for shared libraries.
<br>
## Getting Started: Building Your First Plugin
A plugin is a standalone React application bundled with Webpack <br>
that exposes one or more components via [Module Federation](#4-federation-configuration "See the federation configuration").
This guide walks you through creating your first plugin.
<br>
### 1. Prepare the Host Environment
Developing a plugin requires running the host application (Docs) in parallel.
This live integration is essential for rendering your plugin, enabling hot-reloading, sharing types for Intellisense, <br>
and discovering the exact versions of [shared dependencies](#choosing-shared-dependencies "Learn about shared dependencies").
<br>
1. **Clone the repository locally**:<br>
If you haven't already, clone the Docs repository to your local machine and follow the initial setup instructions.
2. **Set the development flag**:<br>
In the host application's `.env.development` file, set `NEXT_PUBLIC_DEVELOP_PLUGINS=true`.
3. **Stop conflicting services**:<br>
If you are using the project's Docker setup, make sure <br>
the frontend service is stopped (`docker compose stop frontend-development`), as we will run the Docs frontend locally.
4. **Run the host**:<br>
Navigate to `src/frontend/apps/impress`, run `yarn install`, and then `yarn dev`.
5. **Check the logs**:<br>
On startup, the Next.js dev server will print the versions of all shared singleton libraries (e.g., React, styled-components). <br>
You will need these exact versions for your plugin's `package.json`.
<br>
### 2. Scaffolding a New Plugin Project
Create a new, simple React project. <br>
Your project should have a [`webpack.config.js`](#4-federation-configuration "See the federation configuration") and include dependencies for React, Webpack, and TypeScript.
<br>
A minimal `package.json` would look like this:<br>
```json
{
"name": "my-plugin",
"version": "1.0.0",
"scripts": {
"dev": "webpack serve --mode=development",
"build": "webpack --mode=production"
},
"dependencies": {
"react": "<same as host>",
"react-dom": "<same as host>",
"styled-components": "<same as host>",
"@openfun/cunningham-react": "<same as host>",
"@tanstack/react-query": "<same as host>"
},
"devDependencies": {
"webpack": "^5.0.0",
"webpack-cli": "^5.0.0",
"webpack-dev-server": "^4.0.0",
"ts-loader": "^9.0.0",
"typescript": "^5.0.0",
"@types/react": "^18.0.0",
"@module-federation/native-federation-typescript": "^0.2.1"
}
}
```
> Replace `<same as host>` with the versions found in the [host's dev startup log](#1-prepare-the-host-environment "See how to prepare the host").
<br>
### 3\. Creating a Plugin Component
This is a React component that your `webpack.config.js` file exposes. <br>
This minimal example shows how to accept `props`, which can be passed from the [plugin configuration file](#plugin-configuration-file-reference "See the configuration file reference").
<br>
```typescript
// src/MyCustomComponent.tsx
import React from 'react';
// A simple component with inline prop types
const MyCustomComponent = ({ message }: { message?: string }) => {
return (
<div>
This is the plugin component.
{message && <p>Message from props: {message}</p>}
</div>
);
};
export default MyCustomComponent;
```
<br>
### 4\. Federation Configuration
The core of the plugin is its Webpack configuration. <br>
All plugins should use this `webpack.config.js` as a base.
Disclaimer:
We try to not change this file.<br>
But in the future, it may evolve as the plugin system matures.
<br>
```javascript
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const {
NativeFederationTypeScriptHost,
} = require('@module-federation/native-federation-typescript/webpack');
const {
NativeFederationTypeScriptHost: NativeFederationTypeScriptHostCore,
} = require('@module-federation/native-federation-typescript');
module.exports = (env, argv) => {
const dev = argv.mode !== 'production';
const moduleFederationConfig = {
name: 'my_plugin', // A unique name for your plugin
filename: 'remoteEntry.js',
exposes: {
// Maps a public name to a component file
'./MyCustomComponent': './src/MyCustomComponent.tsx',
},
remotes: {
// Allows importing from the host application. The URL is switched automatically.
impress: dev
? 'impress@http://localhost:3000/_next/static/chunks/remoteEntry.js' // Development
: 'impress@/_next/static/chunks/remoteEntry.js', // Production
},
shared: {
// Defines shared libraries to avoid duplication
react: { singleton: true },
'react-dom': { singleton: true },
'styled-components': { singleton: true },
'@openfun/cunningham-react': { singleton: true },
'@tanstack/react-query': { singleton: true },
},
};
let mfTypesReady;
const ensureFederatedTypesPlugin = {
apply(compiler) {
compiler.hooks.beforeCompile.tapPromise(
'EnsureFederatedTypes',
async () => {
if (!mfTypesReady) {
const downloader = NativeFederationTypeScriptHostCore.raw({
moduleFederationConfig,
});
mfTypesReady = downloader.writeBundle();
}
await mfTypesReady;
},
);
},
};
return {
devServer: {
// The port should match the one in your plugin's configuration file
port: 8080,
},
entry: './src/index.tsx', // Your plugin's entry point; can be an empty file as modules are exposed directly.
plugins: [
new ModuleFederationPlugin(moduleFederationConfig),
// This plugin enables type-sharing for intellisense
...(dev
? [
ensureFederatedTypesPlugin, // ensures the zip is ready before the first compile
NativeFederationTypeScriptHost({ moduleFederationConfig }),
]
: []),
],
// ... other webpack config (output, module rules, etc.)
};
};
```
> Don't change `remotes.impress` if you want your [released plugin](#releasing-a-plugin "Learn how to release a plugin") to be [deployable by others](#deploying-docs-with-plugins "Learn about deployment").
<br>
### 5\. Enabling Type-Sharing for Intellisense
To get autocompletion for components and hooks exposed by the host, <br>
configure your plugin's `tsconfig.json` to find the host's types.
<br>
In your plugin's `tsconfig.json`:
```json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": ["./@mf-types/*"]
}
}
}
```
<br>
When you run the host application with `NEXT_PUBLIC_DEVELOP_PLUGINS=true`, it generates a `@mf-types.zip` file. <br>
The `NativeFederationTypeScriptHost` plugin in your webpack config will automatically download and unpack it ahead of the first compile, <br>
making the host's types available to your plugin and IDE. In development with this flag enabled, route changes may be slower because type generation and automatic exposure run during rebuilds; this does not affect production where navigations are instant.
<br>
### 6\. Running and Configuring Your Plugin
With the host application already running (from step 1), <br>
you can now start your plugin's development server and configure the host to load it.
<br>
1. **Start the plugin**:<br>
In your plugin's project directory, run `yarn dev`.
2. **Configure the host**:<br>
Tell the host to load your plugin by editing its configuration file. <br>
When running Docs locally, this file is located at `src/backend/impress/configuration/plugins/default.json`. <br>
Update it to point to your local plugin's `remoteEntry.js`.
<br>
```json
{
"id": "my-custom-component",
"remote": {
"url": "http://localhost:8080/remoteEntry.js",
"name": "my_plugin",
"module": "./MyCustomComponent"
},
"injection": {
"target": "#some-element-in-the-host"
},
"props": {
"message": "Hello from the configuration!"
}
}
```
<br>
After changing the `target` to a valid CSS selector in the host's DOM, save the file. <br>
The host application will automatically detect the change and inject your component, passing the `props` object along.
Your component should appear in the running host application after a reload.
<br>
## Host-Plugin Interaction
### Host Exports
The host automatically exposes many of its components and hooks. <br>
You can import them in the plugin as if they were local modules, thanks to the [`remotes` configuration](#4-federation-configuration "See the remotes config in Webpack") in the `webpack.config.js`.
<br>
```typescript
// In the plugin's code
import { Icon } from 'impress/components';
import { useAuthQuery } from 'impress/features/auth/api';
```
<br>
### Choosing Shared Dependencies
Sharing dependencies is critical for performance and stability.
<br>
- **Minimal Shared Libraries**:<br>
Always share **`react`**, **`react-dom`**, **`styled-components`**, and **`@openfun/cunningham-react`** to use the same instances as the host.
- **Sharing State**:<br>
Libraries that rely on a global context (like `@tanstack/react-query`) **must** be shared to access the host's state and cache.
- **Discovering More Shared Libraries**: With `NEXT_PUBLIC_DEVELOP_PLUGINS=true`, <br>
[the host prints its shared dependency map to the Next.js dev server logs on startup](#1-prepare-the-host-environment "See how to prepare the host"). <br>
You can use this to align versions and add more shared libraries to your plugin.
<br>
> **Important**: Both the host and the plugin must declare a dependency in [`moduleFederationConfig.shared`](#4-federation-configuration "See the federation configuration") for it to become a true singleton. <br>
> If a shared dependency is omitted from the plugin's config, Webpack will bundle a separate copy, breaking the singleton pattern.
<br>
## Development Workflow
### Test and Debug
- Use the `[PluginSystem]` logs in the browser console to see if the plugin is loading correctly.
- Errors in the plugin are caught by an `ErrorBoundary` and will not crash the host.
<br>
Common Errors:
| Issue | Cause/Fix |
| :--- | :--- |
| Unreachable `remoteEntry.js` | Check the `url` in the [plugin configuration](#6-running-and-configuring-your-plugin "See how to configure the plugin"). |
| Library version conflicts | Ensure `shared` library versions in `package.json` match the [host's versions](#1-prepare-the-host-environment "See how to check host versions"). |
| Invalid CSS selectors | Validate the `target` selector against the host's DOM. |
<br>
### Best Practices
- Build modular components with well-typed props.
- Prefer using the host's exposed types and components over implementing new ones.
- Keep shared dependency versions aligned with the host <br>
and re-test after host upgrades.
- Treat plugin bundles as untrusted: vet dependencies and avoid unsafe scripts.
<br>
## Plugin Configuration File Reference
This section provides a detailed reference for all fields in the plugin configuration JSON.
For deployment details, see [Deploying Docs with Plugins](#deploying-docs-with-plugins "Learn about production deployment").
<br>
### Configuration Structure
| Field | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| `id` | String | Yes | Unique component identifier (e.g., "my-widget"). |
| `remote` | Object | Yes | Remote module details. |
| - `url` | String | Yes | Path to `remoteEntry.js` (absolute/relative). |
| - `name` | String | Yes | Federation remote name (e.g., "myPlugin"). |
| - `module` | String | Yes | Exposed module (e.g., "./Widget"). |
| `injection`| Object | Yes | Integration control. |
| - `target` | String | Yes | CSS selector for insertion point. |
| - `position` | String | No (default: "append") | Insertion position (`before`, `after`, `replace`, `prepend`, `append`). See [examples](#injection-position-examples "See injection examples"). |
| - `observerRoots` | String/Boolean | No | DOM observation: CSS selector, `true` (observe whole document), or `false` (default; disable observers). |
| `props` | Object | No | Props passed to the [plugin component](#3-creating-a-plugin-component "See how to create a component with props"). |
| `visibility` | Object | No | Visibility controls. |
| - `routes` | Array | No | Path globs (e.g., `["/docs/*", "!/docs/secret*"]`); supports `*` and `?` wildcards plus negation (`!`). |
<br>
### Injection Position Examples
The `injection.position` property controls how the plugin is inserted relative to the `target` element.
<details>
<summary>View injection examples</summary>
<br>
**before**
```json
{
"id": "my-custom-component-0",
"injection": {
"target": "#item2",
"position": "before"
}
}
```
```html
<ul id="some-element-in-the-host">
<li id="item1"></li>
<div id="plugin-container-my-custom-component-0"></div>
<li id="item2"></li>
</ul>
```
<br>
**after**
```json
{
"id": "my-custom-component-0",
"injection": {
"target": "#item1",
"position": "after"
}
}
```
```html
<ul id="some-element-in-the-host">
<li id="item1"></li>
<div id="plugin-container-my-custom-component-0"></div>
<li id="item2"></li>
</ul>
```
<br>
**prepend**
```json
{
"id": "my-custom-component-0",
"injection": {
"target": "#some-element-in-the-host",
"position": "prepend"
}
}
```
```html
<ul id="some-element-in-the-host">
<div id="plugin-container-my-custom-component-0"></div>
<li id="item1"></li>
<li id="item2"></li>
</ul>
```
<br>
**append** (default)
```json
{
"id": "my-custom-component-0",
"injection": {
"target": "#some-element-in-the-host",
"position": "append"
}
}
```
```html
<ul id="some-element-in-the-host">
<li id="item1"></li>
<li id="item2"></li>
<div id="plugin-container-my-custom-component-0"></div>
</ul>
```
<br>
**replace**
```json
{
"id": "my-custom-component-0",
"injection": {
"target": "#item1",
"position": "replace"
}
}
```
```html
<ul id="some-element-in-the-host">
<div id="plugin-container-my-custom-component-0"></div>
<li id="item1" data-pluginsystem-hidden="true"></li>
<li id="item2"></li>
</ul>
```
</details>
<br>
## Releasing a Plugin
When you are ready to release your plugin, you need to create a production build.
<br>
Run the build command in your plugin's directory:<br>
```bash
yarn build
```
This command bundles your code for production. <br>
Webpack will generate a **`dist`** folder (or similar) containing the **`remoteEntry.js`** file and other JavaScript chunks. <br>
The `remoteEntry.js` is the manifest that tells other applications what modules your plugin exposes. <br>
These are the files you will need for deployment.
<br>
The [`webpack.config.js` provided](#4-federation-configuration "See the federation configuration") is already configured to switch the `remotes` URL to the correct production path automatically, <br>
so no code changes are needed before building.
<br>
## Deploying Docs with Plugins
To use plugins in a production environment, you need to deploy both the plugin assets and the configuration file. <br>
The recommended approach is to serve the plugin's static files from the same webserver that serves the host (docs frontend).
<br>
1. **Deploy Plugin Assets**: <br>
Copy the contents of your plugin's build output directory (e.g., `dist/`) <br>
into the frontend container's `/usr/share/nginx/html/assets` directory at a chosen path.<br>
E.g.: Placing assets in `/usr/share/nginx/html/assets/plugins/my-plugin/` <br>
would make the plugin's **`remoteEntry.js`** available at `https://production.domain/assets/plugins/my-plugin/remoteEntry.js`.
<br>
2. **Deploy Plugin Configuration**: The host's [plugin configuration file](#plugin-configuration-file-reference "See the configuration file reference") must be updated to point to the deployed assets. <br>
This file is typically managed via infrastructure methods <br>
(e.g., a Kubernetes configmap replacing `/app/impress/configuration/plugins/default.json` in the backend container).
<br>
Update the **`remote.url`** to the public-facing path that matches where you deployed the assets:
```json
{
"id": "my-custom-component",
"remote": {
"url": "/assets/plugins/my-plugin/remoteEntry.js",
"name": "my_plugin",
"module": "./MyCustomComponent"
},
"injection": {
"target": "#some-element-in-the-host"
},
"props": {
"message": "Hello from production!"
}
}
```
-18
View File
@@ -32,24 +32,6 @@ Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom
----
# **Your logo** 📝
You can add your own logo in the header from the theme customization file.
### Settings 🔧
```shellscript
THEME_CUSTOMIZATION_FILE_PATH=<path>
```
### Example of JSON
You can activate it with the `header.logo` configuration: https://github.com/suitenumerique/docs/blob/main/src/helm/env.d/dev/configuration/theme/demo.json
This configuration is optional. If not set, the default logo will be used.
----
# **Footer Configuration** 📝
The footer is configurable from the theme customization file.
+3 -2
View File
@@ -67,5 +67,6 @@ DJANGO_SERVER_TO_SERVER_API_TOKENS=server-api-token
Y_PROVIDER_API_BASE_URL=http://y-provider-development:4444/api/
Y_PROVIDER_API_KEY=yprovider-api-key
# Theme customization
THEME_CUSTOMIZATION_CACHE_TIMEOUT=15
# Cache
PLUGINS_CONFIG_CACHE_TIMEOUT=0
THEME_CUSTOMIZATION_CACHE_TIMEOUT=0
+2 -6
View File
@@ -19,17 +19,13 @@
"matchPackageNames": ["redis"],
"allowedVersions": "<6.0.0"
},
{
"groupName": "allowed pylint versions",
"matchManagers": ["pep621"],
"matchPackageNames": ["pylint"],
"allowedVersions": "<4.0.0"
},
{
"enabled": false,
"groupName": "ignored js dependencies",
"matchManagers": ["npm"],
"matchPackageNames": [
"@hocuspocus/provider",
"@hocuspocus/server",
"docx",
"fetch-mock",
"node",
+2
View File
@@ -5,6 +5,7 @@ from django.contrib.auth import admin as auth_admin
from django.utils.translation import gettext_lazy as _
from treebeard.admin import TreeAdmin
from treebeard.forms import movenodeform_factory
from . import models
@@ -156,6 +157,7 @@ class DocumentAdmin(TreeAdmin):
},
),
)
form = movenodeform_factory(models.Document)
inlines = (DocumentAccessInline,)
list_display = (
"id",
+1 -11
View File
@@ -90,7 +90,6 @@ class ListDocumentSerializer(serializers.ModelSerializer):
nb_accesses_direct = serializers.IntegerField(read_only=True)
user_role = serializers.SerializerMethodField(read_only=True)
abilities = serializers.SerializerMethodField(read_only=True)
deleted_at = serializers.SerializerMethodField(read_only=True)
class Meta:
model = models.Document
@@ -103,7 +102,6 @@ class ListDocumentSerializer(serializers.ModelSerializer):
"computed_link_role",
"created_at",
"creator",
"deleted_at",
"depth",
"excerpt",
"is_favorite",
@@ -126,7 +124,6 @@ class ListDocumentSerializer(serializers.ModelSerializer):
"computed_link_role",
"created_at",
"creator",
"deleted_at",
"depth",
"excerpt",
"is_favorite",
@@ -168,10 +165,6 @@ class ListDocumentSerializer(serializers.ModelSerializer):
request = self.context.get("request")
return instance.get_role(request.user) if request else None
def get_deleted_at(self, instance):
"""Return the deleted_at of the current document."""
return instance.ancestors_deleted_at
class DocumentLightSerializer(serializers.ModelSerializer):
"""Minial document serializer for nesting in document accesses."""
@@ -200,7 +193,6 @@ class DocumentSerializer(ListDocumentSerializer):
"content",
"created_at",
"creator",
"deleted_at",
"depth",
"excerpt",
"is_favorite",
@@ -224,7 +216,6 @@ class DocumentSerializer(ListDocumentSerializer):
"computed_link_role",
"created_at",
"creator",
"deleted_at",
"depth",
"is_favorite",
"link_role",
@@ -749,8 +740,7 @@ class InvitationSerializer(serializers.ModelSerializer):
if self.instance is None:
attrs["issuer"] = user
if attrs.get("email"):
attrs["email"] = attrs["email"].lower()
attrs["email"] = attrs["email"].lower()
return attrs
+65 -58
View File
@@ -623,32 +623,12 @@ class DocumentViewSet(
The selected documents are those deleted within the cutoff period defined in the
settings (see TRASHBIN_CUTOFF_DAYS), before they are considered permanently deleted.
"""
if not request.user.is_authenticated:
return self.get_response_for_queryset(self.queryset.none())
access_documents_paths = (
models.DocumentAccess.objects.select_related("document")
.filter(
db.Q(user=self.request.user) | db.Q(team__in=self.request.user.teams),
role=models.RoleChoices.OWNER,
)
.values_list("document__path", flat=True)
)
if not access_documents_paths:
return self.get_response_for_queryset(self.queryset.none())
children_clause = db.Q()
for path in access_documents_paths:
children_clause |= db.Q(path__startswith=path)
queryset = self.queryset.filter(
children_clause,
deleted_at__isnull=False,
deleted_at__gte=models.get_trashbin_cutoff(),
)
queryset = queryset.annotate_user_roles(self.request.user)
queryset = queryset.filter(user_roles__contains=[models.RoleChoices.OWNER])
return self.get_response_for_queryset(queryset)
@@ -871,47 +851,33 @@ class DocumentViewSet(
try:
current_document = (
self.queryset.select_related(None)
.only("depth", "path", "ancestors_deleted_at")
.get(pk=pk)
self.queryset.select_related(None).only("depth", "path").get(pk=pk)
)
except models.Document.DoesNotExist as excpt:
raise drf.exceptions.NotFound() from excpt
is_deleted = current_document.ancestors_deleted_at is not None
if is_deleted:
if current_document.get_role(user) != models.RoleChoices.OWNER:
raise (
drf.exceptions.PermissionDenied()
if request.user.is_authenticated
else drf.exceptions.NotAuthenticated()
)
highest_readable = current_document
ancestors = self.queryset.select_related(None).filter(pk=pk)
else:
ancestors = (
(
current_document.get_ancestors()
| self.queryset.select_related(None).filter(pk=pk)
)
.filter(ancestors_deleted_at__isnull=True)
.order_by("path")
)
# Get the highest readable ancestor
highest_readable = (
ancestors.select_related(None)
.readable_per_se(request.user)
.only("depth", "path")
.first()
ancestors = (
(
current_document.get_ancestors()
| self.queryset.select_related(None).filter(pk=pk)
)
.filter(ancestors_deleted_at__isnull=True)
.order_by("path")
)
if highest_readable is None:
raise (
drf.exceptions.PermissionDenied()
if request.user.is_authenticated
else drf.exceptions.NotAuthenticated()
)
# Get the highest readable ancestor
highest_readable = (
ancestors.select_related(None)
.readable_per_se(request.user)
.only("depth", "path")
.first()
)
if highest_readable is None:
raise (
drf.exceptions.PermissionDenied()
if request.user.is_authenticated
else drf.exceptions.NotAuthenticated()
)
paths_links_mapping = {}
ancestors_links = []
children_clause = db.Q()
@@ -1105,7 +1071,7 @@ class DocumentViewSet(
@drf.decorators.action(
detail=True,
methods=["get", "delete"],
url_path=r"versions/(?P<version_id>[A-Za-z0-9._+\-=~]{1,1024})",
url_path="versions/(?P<version_id>[0-9a-z-]+)",
)
# pylint: disable=unused-argument
def versions_detail(self, request, pk, version_id, *args, **kwargs):
@@ -2191,7 +2157,6 @@ class ConfigView(drf.views.APIView):
"LANGUAGES",
"LANGUAGE_CODE",
"SENTRY_DSN",
"TRASHBIN_CUTOFF_DAYS",
]
dict_settings = {}
for setting in array_settings:
@@ -2199,6 +2164,7 @@ class ConfigView(drf.views.APIView):
dict_settings[setting] = getattr(settings, setting)
dict_settings["theme_customization"] = self._load_theme_customization()
dict_settings["plugins"] = self._load_plugins_config()
return drf.response.Response(dict_settings)
@@ -2236,3 +2202,44 @@ class ConfigView(drf.views.APIView):
)
return theme_customization
def _load_plugins_config(self):
if not settings.PLUGINS_CONFIG_FILE_PATH:
return []
cache_key = (
f"plugins_config_{slugify(settings.PLUGINS_CONFIG_FILE_PATH)}"
)
plugins_config = cache.get(cache_key)
if plugins_config is not None:
return plugins_config
plugins_config = []
try:
with open(
settings.PLUGINS_CONFIG_FILE_PATH, "r", encoding="utf-8"
) as f:
data = json.load(f)
# Support both array format and object with "plugins" key
if isinstance(data, list):
plugins_config = data
elif isinstance(data, dict):
plugins_config = data.get("plugins", [])
except FileNotFoundError:
logger.error(
"Plugins configuration file not found: %s",
settings.PLUGINS_CONFIG_FILE_PATH,
)
except json.JSONDecodeError:
logger.error(
"Plugins configuration file is not a valid JSON: %s",
settings.PLUGINS_CONFIG_FILE_PATH,
)
else:
cache.set(
cache_key,
plugins_config,
settings.PLUGINS_CONFIG_CACHE_TIMEOUT,
)
return plugins_config
@@ -1,19 +0,0 @@
# Generated by Django 5.2.7 on 2025-10-22 06:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0024_add_is_masked_field_to_link_trace"),
]
operations = [
migrations.AlterField(
model_name="user",
name="short_name",
field=models.CharField(
blank=True, max_length=100, null=True, verbose_name="short name"
),
),
]
+7 -10
View File
@@ -148,9 +148,7 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
)
full_name = models.CharField(_("full name"), max_length=100, null=True, blank=True)
short_name = models.CharField(
_("short name"), max_length=100, null=True, blank=True
)
short_name = models.CharField(_("short name"), max_length=20, null=True, blank=True)
email = models.EmailField(_("identity email address"), blank=True, null=True)
@@ -723,7 +721,7 @@ class Document(MP_Node, BaseModel):
# Characteristics that are based only on specific access
is_owner = role == RoleChoices.OWNER
is_deleted = self.ancestors_deleted_at
is_deleted = self.ancestors_deleted_at and not is_owner
is_owner_or_admin = (is_owner or role == RoleChoices.ADMIN) and not is_deleted
# Compute access roles before adding link roles because we don't
@@ -752,7 +750,6 @@ class Document(MP_Node, BaseModel):
role = RoleChoices.max(role, link_definition["link_role"])
can_get = bool(role) and not is_deleted
retrieve = can_get or is_owner
can_update = (
is_owner_or_admin or role == RoleChoices.EDITOR
) and not is_deleted
@@ -761,7 +758,7 @@ class Document(MP_Node, BaseModel):
is_owner
if self.is_root()
else (is_owner_or_admin or (user.is_authenticated and self.creator == user))
) and not is_deleted
)
ai_allow_reach_from = settings.AI_ALLOW_REACH_FROM
ai_access = any(
@@ -793,15 +790,15 @@ class Document(MP_Node, BaseModel):
"duplicate": can_get and user.is_authenticated,
"favorite": can_get and user.is_authenticated,
"link_configuration": is_owner_or_admin,
"invite_owner": is_owner and not is_deleted,
"invite_owner": is_owner,
"mask": can_get and user.is_authenticated,
"move": is_owner_or_admin and not is_deleted,
"move": is_owner_or_admin and not self.ancestors_deleted_at,
"partial_update": can_update,
"restore": is_owner,
"retrieve": retrieve,
"retrieve": can_get,
"media_auth": can_get,
"link_select_options": link_select_options,
"tree": retrieve,
"tree": can_get,
"update": can_update,
"versions_destroy": is_owner_or_admin,
"versions_list": has_access_role,
@@ -57,7 +57,7 @@ def test_authentication_getter_existing_user_via_email(
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
with django_assert_num_queries(4): # user by sub, user by mail, update sub
with django_assert_num_queries(3): # user by sub, user by mail, update sub
user = klass.get_or_create_user(
access_token="test-token", id_token=None, payload=None
)
@@ -214,7 +214,7 @@ def test_authentication_getter_existing_user_change_fields_sub(
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
# One and only one additional update query when a field has changed
with django_assert_num_queries(3):
with django_assert_num_queries(2):
authenticated_user = klass.get_or_create_user(
access_token="test-token", id_token=None, payload=None
)
@@ -256,7 +256,7 @@ def test_authentication_getter_existing_user_change_fields_email(
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
# One and only one additional update query when a field has changed
with django_assert_num_queries(4):
with django_assert_num_queries(3):
authenticated_user = klass.get_or_create_user(
access_token="test-token", id_token=None, payload=None
)
@@ -769,37 +769,6 @@ def test_api_document_invitations_update_authenticated_unprivileged(
assert value == old_invitation_values[key]
@pytest.mark.parametrize("via", VIA)
@pytest.mark.parametrize("role", ["administrator", "owner"])
def test_api_document_invitations_patch(via, role, mock_user_teams):
"""Partially updating an invitation should be allowed."""
user = factories.UserFactory()
invitation = factories.InvitationFactory(role="editor")
if via == USER:
factories.UserDocumentAccessFactory(
document=invitation.document, user=user, role=role
)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=invitation.document, team="lasuite", role=role
)
client = APIClient()
client.force_login(user)
response = client.patch(
f"/api/v1.0/documents/{invitation.document.id!s}/invitations/{invitation.id!s}/",
{"role": "reader"},
format="json",
)
assert response.status_code == 200
invitation.refresh_from_db()
assert invitation.role == "reader"
# Delete
@@ -41,7 +41,6 @@ def test_api_documents_children_list_anonymous_public_standalone(
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -64,7 +63,6 @@ def test_api_documents_children_list_anonymous_public_standalone(
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -117,7 +115,6 @@ def test_api_documents_children_list_anonymous_public_parent(django_assert_num_q
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -140,7 +137,6 @@ def test_api_documents_children_list_anonymous_public_parent(django_assert_num_q
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -212,7 +208,6 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -235,7 +230,6 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -293,7 +287,6 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -316,7 +309,6 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -401,7 +393,6 @@ def test_api_documents_children_list_authenticated_related_direct(
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -424,7 +415,6 @@ def test_api_documents_children_list_authenticated_related_direct(
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -485,7 +475,6 @@ def test_api_documents_children_list_authenticated_related_parent(
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -508,7 +497,6 @@ def test_api_documents_children_list_authenticated_related_parent(
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -621,7 +609,6 @@ def test_api_documents_children_list_authenticated_related_team_members(
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -644,7 +631,6 @@ def test_api_documents_children_list_authenticated_related_team_members(
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -38,7 +38,6 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -63,7 +62,6 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
"computed_link_role": grand_child.computed_link_role,
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_child.creator.id),
"deleted_at": None,
"depth": 3,
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
@@ -86,7 +84,6 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -138,7 +135,6 @@ def test_api_documents_descendants_list_anonymous_public_parent():
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -161,7 +157,6 @@ def test_api_documents_descendants_list_anonymous_public_parent():
"computed_link_role": grand_child.computed_link_role,
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_child.creator.id),
"deleted_at": None,
"depth": 5,
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
@@ -184,7 +179,6 @@ def test_api_documents_descendants_list_anonymous_public_parent():
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -257,7 +251,6 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -280,7 +273,6 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
"computed_link_role": grand_child.computed_link_role,
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_child.creator.id),
"deleted_at": None,
"depth": 3,
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
@@ -303,7 +295,6 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -361,7 +352,6 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -384,7 +374,6 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
"computed_link_role": grand_child.computed_link_role,
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_child.creator.id),
"deleted_at": None,
"depth": 5,
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
@@ -407,7 +396,6 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -486,7 +474,6 @@ def test_api_documents_descendants_list_authenticated_related_direct():
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -509,7 +496,6 @@ def test_api_documents_descendants_list_authenticated_related_direct():
"computed_link_role": grand_child.computed_link_role,
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_child.creator.id),
"deleted_at": None,
"depth": 3,
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
@@ -532,7 +518,6 @@ def test_api_documents_descendants_list_authenticated_related_direct():
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -591,7 +576,6 @@ def test_api_documents_descendants_list_authenticated_related_parent():
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -614,7 +598,6 @@ def test_api_documents_descendants_list_authenticated_related_parent():
"computed_link_role": grand_child.computed_link_role,
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_child.creator.id),
"deleted_at": None,
"depth": 5,
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
@@ -637,7 +620,6 @@ def test_api_documents_descendants_list_authenticated_related_parent():
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 4,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -742,7 +724,6 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
"computed_link_role": child1.computed_link_role,
"created_at": child1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child1.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child1.excerpt,
"id": str(child1.id),
@@ -765,7 +746,6 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
"computed_link_role": grand_child.computed_link_role,
"created_at": grand_child.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_child.creator.id),
"deleted_at": None,
"depth": 3,
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
@@ -788,7 +768,6 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
"computed_link_role": child2.computed_link_role,
"created_at": child2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(child2.creator.id),
"deleted_at": None,
"depth": 2,
"excerpt": child2.excerpt,
"id": str(child2.id),
@@ -65,7 +65,6 @@ def test_api_document_favorite_list_authenticated_with_favorite():
"computed_link_role": document.computed_link_role,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"deleted_at": None,
"content": document.content,
"depth": document.depth,
"excerpt": document.excerpt,
@@ -69,7 +69,6 @@ def test_api_documents_list_format():
"computed_link_role": document.computed_link_role,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"deleted_at": None,
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": True,
@@ -70,7 +70,6 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"content": document.content,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"deleted_at": None,
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": False,
@@ -145,7 +144,6 @@ def test_api_documents_retrieve_anonymous_public_parent():
"content": document.content,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"deleted_at": None,
"depth": 3,
"excerpt": document.excerpt,
"is_favorite": False,
@@ -254,7 +252,6 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"depth": 1,
"deleted_at": None,
"excerpt": document.excerpt,
"is_favorite": False,
"link_reach": reach,
@@ -336,7 +333,6 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": document.excerpt,
"is_favorite": False,
"link_reach": document.link_reach,
@@ -450,7 +446,6 @@ def test_api_documents_retrieve_authenticated_related_direct():
"content": document.content,
"creator": str(document.creator.id),
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"deleted_at": None,
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": False,
@@ -533,7 +528,6 @@ def test_api_documents_retrieve_authenticated_related_parent():
"creator": str(document.creator.id),
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"depth": 3,
"deleted_at": None,
"excerpt": document.excerpt,
"is_favorite": False,
"link_reach": "restricted",
@@ -689,7 +683,6 @@ def test_api_documents_retrieve_authenticated_related_team_members(
"content": document.content,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"deleted_at": None,
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": False,
@@ -756,7 +749,6 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
"content": document.content,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"deleted_at": None,
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": False,
@@ -823,7 +815,6 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
"content": document.content,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"deleted_at": None,
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": False,
@@ -48,11 +48,11 @@ def test_api_documents_trashbin_format():
other_users = factories.UserFactory.create_batch(3)
document = factories.DocumentFactory(
deleted_at=timezone.now(),
users=factories.UserFactory.create_batch(2),
favorited_by=[user, *other_users],
link_traces=other_users,
)
document.soft_delete()
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
response = client.get("/api/v1.0/documents/trashbin/")
@@ -70,40 +70,40 @@ def test_api_documents_trashbin_format():
assert results[0] == {
"id": str(document.id),
"abilities": {
"accesses_manage": False,
"accesses_view": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"can_edit": False,
"children_create": False,
"children_list": False,
"collaboration_auth": False,
"descendants": False,
"cors_proxy": False,
"content": False,
"destroy": False,
"duplicate": False,
"favorite": False,
"invite_owner": False,
"link_configuration": False,
"accesses_manage": True,
"accesses_view": True,
"ai_transform": True,
"ai_translate": True,
"attachment_upload": True,
"can_edit": True,
"children_create": True,
"children_list": True,
"collaboration_auth": True,
"descendants": True,
"cors_proxy": True,
"content": True,
"destroy": True,
"duplicate": True,
"favorite": True,
"invite_owner": True,
"link_configuration": True,
"link_select_options": {
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": False,
"media_auth": False,
"media_check": False,
"mask": True,
"media_auth": True,
"media_check": True,
"move": False, # Can't move a deleted document
"partial_update": False,
"partial_update": True,
"restore": True,
"retrieve": True,
"tree": True,
"update": False,
"versions_destroy": False,
"versions_list": False,
"versions_retrieve": False,
"update": True,
"versions_destroy": True,
"versions_list": True,
"versions_retrieve": True,
},
"ancestors_link_reach": None,
"ancestors_link_role": None,
@@ -113,7 +113,6 @@ def test_api_documents_trashbin_format():
"creator": str(document.creator.id),
"depth": 1,
"excerpt": document.excerpt,
"deleted_at": document.ancestors_deleted_at.isoformat().replace("+00:00", "Z"),
"link_reach": document.link_reach,
"link_role": document.link_role,
"nb_accesses_ancestors": 0,
@@ -166,10 +165,10 @@ def test_api_documents_trashbin_authenticated_direct(django_assert_num_queries):
expected_ids = {str(document1.id), str(document2.id), str(document3.id)}
with django_assert_num_queries(11):
with django_assert_num_queries(10):
response = client.get("/api/v1.0/documents/trashbin/")
with django_assert_num_queries(5):
with django_assert_num_queries(4):
response = client.get("/api/v1.0/documents/trashbin/")
assert response.status_code == 200
@@ -208,10 +207,10 @@ def test_api_documents_trashbin_authenticated_via_team(
expected_ids = {str(deleted_document_team1.id), str(deleted_document_team2.id)}
with django_assert_num_queries(8):
with django_assert_num_queries(7):
response = client.get("/api/v1.0/documents/trashbin/")
with django_assert_num_queries(4):
with django_assert_num_queries(3):
response = client.get("/api/v1.0/documents/trashbin/")
assert response.status_code == 200
@@ -293,29 +292,3 @@ def test_api_documents_trashbin_distinct():
content = response.json()
assert len(content["results"]) == 1
assert content["results"][0]["id"] == str(document.id)
def test_api_documents_trashbin_empty_queryset_bug():
"""
Test that users with no owner role don't see documents.
"""
# Create a new user with no owner access to any document
new_user = factories.UserFactory()
client = APIClient()
client.force_login(new_user)
# Create some deleted documents owned by other users
other_user = factories.UserFactory()
item1 = factories.DocumentFactory(users=[(other_user, "owner")])
item1.soft_delete()
item2 = factories.DocumentFactory(users=[(other_user, "owner")])
item2.soft_delete()
item3 = factories.DocumentFactory(users=[(other_user, "owner")])
item3.soft_delete()
response = client.get("/api/v1.0/documents/trashbin/")
assert response.status_code == 200
content = response.json()
assert content["count"] == 0
assert len(content["results"]) == 0
@@ -50,7 +50,6 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
),
"creator": str(child.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
@@ -74,7 +73,6 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
@@ -98,7 +96,6 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
"created_at": sibling1.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(sibling1.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": sibling1.excerpt,
"id": str(sibling1.id),
"is_favorite": False,
@@ -122,7 +119,6 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
"created_at": sibling2.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(sibling2.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": sibling2.excerpt,
"id": str(sibling2.id),
"is_favorite": False,
@@ -142,7 +138,6 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(parent.creator.id),
"depth": 1,
"deleted_at": None,
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
@@ -215,7 +210,6 @@ def test_api_documents_tree_list_anonymous_public_parent():
),
"creator": str(child.creator.id),
"depth": 5,
"deleted_at": None,
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
@@ -239,7 +233,6 @@ def test_api_documents_tree_list_anonymous_public_parent():
),
"creator": str(document.creator.id),
"depth": 4,
"deleted_at": None,
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
@@ -267,7 +260,6 @@ def test_api_documents_tree_list_anonymous_public_parent():
),
"creator": str(document_sibling.creator.id),
"depth": 4,
"deleted_at": None,
"excerpt": document_sibling.excerpt,
"id": str(document_sibling.id),
"is_favorite": False,
@@ -289,7 +281,6 @@ def test_api_documents_tree_list_anonymous_public_parent():
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(parent.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
@@ -315,7 +306,6 @@ def test_api_documents_tree_list_anonymous_public_parent():
),
"creator": str(parent_sibling.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": parent_sibling.excerpt,
"id": str(parent_sibling.id),
"is_favorite": False,
@@ -337,7 +327,6 @@ def test_api_documents_tree_list_anonymous_public_parent():
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_parent.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": grand_parent.excerpt,
"id": str(grand_parent.id),
"is_favorite": False,
@@ -417,7 +406,6 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
),
"creator": str(child.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
@@ -439,7 +427,6 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
@@ -463,7 +450,6 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(sibling.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": sibling.excerpt,
"id": str(sibling.id),
"is_favorite": False,
@@ -483,7 +469,6 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(parent.creator.id),
"depth": 1,
"deleted_at": None,
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
@@ -561,7 +546,6 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
),
"creator": str(child.creator.id),
"depth": 5,
"deleted_at": None,
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
@@ -585,7 +569,6 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
),
"creator": str(document.creator.id),
"depth": 4,
"deleted_at": None,
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
@@ -613,7 +596,6 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
),
"creator": str(document_sibling.creator.id),
"depth": 4,
"deleted_at": None,
"excerpt": document_sibling.excerpt,
"id": str(document_sibling.id),
"is_favorite": False,
@@ -635,7 +617,6 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(parent.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
@@ -661,7 +642,6 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
),
"creator": str(parent_sibling.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": parent_sibling.excerpt,
"id": str(parent_sibling.id),
"is_favorite": False,
@@ -683,7 +663,6 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_parent.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": grand_parent.excerpt,
"id": str(grand_parent.id),
"is_favorite": False,
@@ -765,7 +744,6 @@ def test_api_documents_tree_list_authenticated_related_direct():
),
"creator": str(child.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
@@ -787,7 +765,6 @@ def test_api_documents_tree_list_authenticated_related_direct():
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
@@ -811,7 +788,6 @@ def test_api_documents_tree_list_authenticated_related_direct():
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(sibling.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": sibling.excerpt,
"id": str(sibling.id),
"is_favorite": False,
@@ -831,7 +807,6 @@ def test_api_documents_tree_list_authenticated_related_direct():
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(parent.creator.id),
"depth": 1,
"deleted_at": None,
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
@@ -913,7 +888,6 @@ def test_api_documents_tree_list_authenticated_related_parent():
),
"creator": str(child.creator.id),
"depth": 5,
"deleted_at": None,
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
@@ -937,7 +911,6 @@ def test_api_documents_tree_list_authenticated_related_parent():
),
"creator": str(document.creator.id),
"depth": 4,
"deleted_at": None,
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
@@ -965,7 +938,6 @@ def test_api_documents_tree_list_authenticated_related_parent():
),
"creator": str(document_sibling.creator.id),
"depth": 4,
"deleted_at": None,
"excerpt": document_sibling.excerpt,
"id": str(document_sibling.id),
"is_favorite": False,
@@ -987,7 +959,6 @@ def test_api_documents_tree_list_authenticated_related_parent():
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(parent.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
@@ -1013,7 +984,6 @@ def test_api_documents_tree_list_authenticated_related_parent():
),
"creator": str(parent_sibling.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": parent_sibling.excerpt,
"id": str(parent_sibling.id),
"is_favorite": False,
@@ -1035,7 +1005,6 @@ def test_api_documents_tree_list_authenticated_related_parent():
"created_at": grand_parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(grand_parent.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": grand_parent.excerpt,
"id": str(grand_parent.id),
"is_favorite": False,
@@ -1125,7 +1094,6 @@ def test_api_documents_tree_list_authenticated_related_team_members(
),
"creator": str(child.creator.id),
"depth": 3,
"deleted_at": None,
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
@@ -1147,7 +1115,6 @@ def test_api_documents_tree_list_authenticated_related_team_members(
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(document.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
@@ -1171,7 +1138,6 @@ def test_api_documents_tree_list_authenticated_related_team_members(
"created_at": sibling.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(sibling.creator.id),
"depth": 2,
"deleted_at": None,
"excerpt": sibling.excerpt,
"id": str(sibling.id),
"is_favorite": False,
@@ -1191,7 +1157,6 @@ def test_api_documents_tree_list_authenticated_related_team_members(
"created_at": parent.created_at.isoformat().replace("+00:00", "Z"),
"creator": str(parent.creator.id),
"depth": 1,
"deleted_at": None,
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
@@ -1205,56 +1170,3 @@ def test_api_documents_tree_list_authenticated_related_team_members(
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
"user_role": access.role,
}
def test_api_documents_tree_list_deleted_document():
"""
Tree of a deleted document should only be accessible to the owner.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
parent = factories.DocumentFactory(link_reach="public")
document, _ = factories.DocumentFactory.create_batch(2, parent=parent)
factories.DocumentFactory(link_reach="public", parent=document)
document.soft_delete()
response = client.get(f"/api/v1.0/documents/{document.id!s}/tree/")
assert response.status_code == 403
def test_api_documents_tree_list_deleted_document_owner(django_assert_num_queries):
"""
Tree of a deleted document should only be accessible to the owner.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
parent = factories.DocumentFactory(link_reach="public", users=[(user, "owner")])
document, _ = factories.DocumentFactory.create_batch(2, parent=parent)
child = factories.DocumentFactory(parent=document)
document.soft_delete()
document.refresh_from_db()
child.refresh_from_db()
with django_assert_num_queries(9):
client.get(f"/api/v1.0/documents/{document.id!s}/tree/")
with django_assert_num_queries(5):
response = client.get(f"/api/v1.0/documents/{document.id!s}/tree/")
assert response.status_code == 200
content = response.json()
assert content["id"] == str(document.id)
assert content["deleted_at"] == document.deleted_at.isoformat().replace(
"+00:00", "Z"
)
assert len(content["children"]) == 1
assert content["children"][0]["id"] == str(child.id)
assert content["children"][0][
"deleted_at"
] == child.ancestors_deleted_at.isoformat().replace("+00:00", "Z")
+1 -2
View File
@@ -42,7 +42,6 @@ def test_api_config(is_authenticated):
response = client.get("/api/v1.0/config/")
assert response.status_code == HTTP_200_OK
assert response.json() == {
"AI_FEATURE_ENABLED": False,
"COLLABORATION_WS_URL": "http://testcollab/",
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY": True,
"CRISP_WEBSITE_ID": "123",
@@ -61,7 +60,7 @@ def test_api_config(is_authenticated):
"MEDIA_BASE_URL": "http://testserver/",
"POSTHOG_KEY": {"id": "132456", "host": "https://eu.i.posthog-test.com"},
"SENTRY_DSN": "https://sentry.test/123",
"TRASHBIN_CUTOFF_DAYS": 30,
"AI_FEATURE_ENABLED": False,
"theme_customization": {},
}
policy_list = sorted(response.headers["Content-Security-Policy"].split("; "))
@@ -375,42 +375,8 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
document.soft_delete()
document.refresh_from_db()
assert document.get_abilities(user) == {
"accesses_manage": False,
"accesses_view": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"can_edit": False,
"children_create": False,
"children_list": False,
"collaboration_auth": False,
"descendants": False,
"cors_proxy": False,
"content": False,
"destroy": False,
"duplicate": False,
"favorite": False,
"invite_owner": False,
"link_configuration": False,
"link_select_options": {
"authenticated": ["reader", "editor"],
"public": ["reader", "editor"],
"restricted": None,
},
"mask": False,
"media_auth": False,
"media_check": False,
"move": False,
"partial_update": False,
"restore": True,
"retrieve": True,
"tree": True,
"update": False,
"versions_destroy": False,
"versions_list": False,
"versions_retrieve": False,
}
expected_abilities["move"] = False
assert document.get_abilities(user) == expected_abilities
@override_settings(
@@ -0,0 +1,54 @@
[
{
"id": "my-custom-component-in-header",
"remote": {
"url": "http://localhost:3002/remoteEntry.js",
"name": "plugin_frontend",
"module": "MyCustomComponent"
},
"injection": {
"target": "body header [data-testid=\"header-logo-link\"]",
"position": "after",
"observerRoots": "body header"
},
"props": {
"customMessage": "Plugin Demo",
"showDebugInfo": true
},
"visibility": {
"routes": [
"/docs/*"
]
}
},
{
"id": "central-header-menu",
"remote": {
"url": "http://localhost:3002/remoteEntry.js",
"name": "plugin_frontend",
"module": "./MyCustomHeaderMenu"
},
"injection": {
"target": "body header [data-testid=\"header-logo-link\"]",
"position": "after",
"observerRoots": "body header"
},
"props": {
"icsBaseUrl": "http://localhost:8000",
"portalBaseUrl": "http://localhost:8001"
}
},
{
"id": "theme-demo-panel",
"remote": {
"url": "http://localhost:3002/remoteEntry.js",
"name": "plugin_frontend",
"module": "./ThemingDemo"
},
"injection": {
"target": "body",
"position": "append",
"observerRoots": false
}
}
]
+13 -1
View File
@@ -490,12 +490,24 @@ class Base(Configuration):
environ_prefix=None,
)
THEME_CUSTOMIZATION_CACHE_TIMEOUT = values.IntegerValue(
THEME_CUSTOMIZATION_CACHE_TIMEOUT = values.Value(
60 * 60 * 24,
environ_name="THEME_CUSTOMIZATION_CACHE_TIMEOUT",
environ_prefix=None,
)
PLUGINS_CONFIG_FILE_PATH = values.Value(
os.path.join(BASE_DIR, "impress/configuration/plugins/default.json"),
environ_name="PLUGINS_CONFIG_FILE_PATH",
environ_prefix=None,
)
PLUGINS_CONFIG_CACHE_TIMEOUT = values.Value(
60 * 60 * 2,
environ_name="PLUGINS_CONFIG_CACHE_TIMEOUT",
environ_prefix=None,
)
# Posthog
POSTHOG_KEY = values.DictValue(
None, environ_name="POSTHOG_KEY", environ_prefix=None
+74 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr "Titouroù personel"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Aotreoù"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Deiziadoù a-bouez"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Gwezennadur"
@@ -50,36 +50,27 @@ msgstr "Kuzhet"
msgid "Favorite"
msgstr "Sinedoù"
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "Ur restr nevez a zo bet krouet ganeoc'h!"
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr "C'hwi zo bet disklaeriet perc'henn ur restr nevez:"
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr "Ar vaezienn-mañ a zo rekis."
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Korf"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "Doare korf"
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Stumm"
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "eilenn {title}"
@@ -178,247 +169,247 @@ msgstr ""
msgid "full name"
msgstr "anv klok"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "anv berr"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "postel identelezh"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "postel ar merour"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "yezh"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr "Ar yezh a vo implijet evit etrefas an implijer."
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr "Ar gwerzhid-eur a vo implijet evit etrefas an implijer."
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "trevnad"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr "Pe vefe an implijer un aparailh pe un implijer gwirion."
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "statud ar skipailh"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr "Ma c'hall an implijer kevreañ ouzh al lec'hienn verañ-mañ."
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "oberiant"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Ma rank bezañ tretet an implijer-mañ evel oberiant. Diziuzit an dra-mañ e-plas dilemel kontoù."
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "implijer"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "implijerien"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "titl"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "bomm"
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Restr"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Restroù"
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "Restr hep titl"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} en deus rannet ur restr ganeoc'h!"
#: build/lib/core/models.py:861 core/models.py:861
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} en deus pedet ac'hanoc'h gant ar rol \"{role}\" war ar restr da-heul:"
#: build/lib/core/models.py:867 core/models.py:867
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} en deus rannet ur restr ganeoc'h: {title}"
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Roud liamm ar restr/an implijer"
#: build/lib/core/models.py:968 core/models.py:968
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Roudoù liamm ar restr/an implijer"
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "Ur roud liamm a zo dija evit an restr/an implijer."
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Restr muiañ-karet"
#: build/lib/core/models.py:998 core/models.py:998
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Restroù muiañ-karet"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Ar restr-mañ a zo ur restr muiañ karet gant an implijer-mañ."
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Liamm restr/implijer"
#: build/lib/core/models.py:1027 core/models.py:1027
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Liammoù restr/implijer"
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "An implijer-mañ a zo dija er restr-mañ."
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr "An implijer pe ar skipailh a rank bezañ termenet, ket an daou avat."
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr "Goulenn tizhout ar restr"
#: build/lib/core/models.py:1192 core/models.py:1192
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr "Goulennoù tizhout ar restr"
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr "An implijer en deus goulennet tizhout ar restr-mañ."
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} en defe c'hoant da dizhout ar restr-mañ!"
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} en defe c'hoant da dizhout ar restr da-heul:"
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} en defe c'hoant da dizhout ar restr: {title}"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "deskrivadur"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "kod"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "publik"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "M'eo foran ar patrom-mañ hag implijus gant n'eus forzh piv."
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Patrom"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Patromoù"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Liamm patrom/implijer"
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Liammoù patrom/implijer"
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "An implijer-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Ar skipailh-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "postel"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Pedadenn d'ur restr"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Pedadennoù d'ur restr"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."
#: core/templates/mail/html/template.html:153
#: core/templates/mail/html/template.html:162
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Logo ar postel"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/html/template.html:209
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Digeriñ"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs, hoc'h ostilh nevez ret-holl evit aozañ, rannañ ha kenlabourat war ar restr e skipailh. "
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+74 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr "Persönliche Daten"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: 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:61 core/admin.py:61
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Wichtige Daten"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Baumstruktur"
@@ -50,36 +50,27 @@ msgstr ""
msgid "Favorite"
msgstr "Favorit"
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr "Sie sind Besitzer eines neuen Dokuments:"
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Inhalt"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "Typ"
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "Kopie von {title}"
@@ -178,247 +169,247 @@ msgstr ""
msgid "full name"
msgstr "Name"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "Kurzbezeichnung"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "Identitäts-E-Mail-Adresse"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "Admin E-Mail-Adresse"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "Sprache"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:166 core/models.py:166
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:176 core/models.py:176
#: build/lib/core/models.py:174 core/models.py:174
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:179 core/models.py:179
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "Gerät"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:179 core/models.py:179
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:184 core/models.py:184
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "Status des Teammitgliedes"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:184 core/models.py:184
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:189 core/models.py:189
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "aktiviert"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:190 core/models.py:190
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:204 core/models.py:204
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "Benutzer"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "Benutzer"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "Titel"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "Auszug"
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Dokument"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Dokumente"
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "Unbenanntes Dokument"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
#: build/lib/core/models.py:861 core/models.py:861
#: build/lib/core/models.py:858 core/models.py:858
#, 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:867 core/models.py:867
#: build/lib/core/models.py:864 core/models.py:864
#, 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:967 core/models.py:967
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:968 core/models.py:968
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "Für dieses Dokument/ diesen Benutzer ist bereits eine Linkverfolgung vorhanden."
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Dokumentenfavorit"
#: build/lib/core/models.py:998 core/models.py:998
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Dokumentfavoriten"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1001 core/models.py:1001
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:1026 core/models.py:1026
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Dokument/Benutzerbeziehung"
#: build/lib/core/models.py:1027 core/models.py:1027
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Dokument/Benutzerbeziehungen"
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
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:1191 core/models.py:1191
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1192 core/models.py:1192
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "Beschreibung"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "Code"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "öffentlich"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Vorlage"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Vorlagen"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Vorlage/Benutzer-Beziehung"
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Vorlage/Benutzerbeziehungen"
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Dieses Team ist bereits in diesem Template."
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
#: core/templates/mail/html/template.html:153
#: core/templates/mail/html/template.html:162
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Logo-E-Mail"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/html/template.html:209
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Öffnen"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.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/template.html:224
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+74 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr ""
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: 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:61 core/admin.py:61
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr ""
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr ""
@@ -50,36 +50,27 @@ msgstr ""
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -178,247 +169,247 @@ msgstr ""
msgid "full name"
msgstr ""
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr ""
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr ""
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr ""
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr ""
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr ""
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr ""
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr ""
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:861 core/models.py:861
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:867 core/models.py:867
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:968 core/models.py:968
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:998 core/models.py:998
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1027 core/models.py:1027
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1192 core/models.py:1192
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr ""
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr ""
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr ""
#: core/templates/mail/html/template.html:153
#: core/templates/mail/html/template.html:162
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr ""
#: core/templates/mail/html/template.html:200
#: core/templates/mail/html/template.html:209
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr ""
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+74 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr "Información Personal"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Permisos"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Fechas importantes"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Estructura en árbol"
@@ -50,36 +50,27 @@ msgstr ""
msgid "Favorite"
msgstr "Favorito"
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "¡Un nuevo documento se ha creado por ti!"
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr "Se le ha concedido la propiedad de un nuevo documento :"
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Cuerpo"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "Tipo de Cuerpo"
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "copia de {title}"
@@ -178,247 +169,247 @@ msgstr "Obligatorio. 255 caracteres o menos. Solo caracteres ASCII."
msgid "full name"
msgstr "nombre completo"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "nombre abreviado"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "correo electrónico de identidad"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "correo electrónico del administrador"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "idioma"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr "El idioma en el que el usuario desea ver la interfaz."
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr "La zona horaria en la que el usuario quiere ver los tiempos."
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "dispositivo"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr "Si el usuario es un dispositivo o un usuario real."
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "rol en el equipo"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr "Si el usuario puede iniciar sesión en esta página web de administración."
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "activo"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Si este usuario debe ser considerado como activo. Deseleccionar en lugar de eliminar cuentas."
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "usuario"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "usuarios"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "título"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "resumen"
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Documento"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Documentos"
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "Documento sin título"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "¡{name} ha compartido un documento contigo!"
#: build/lib/core/models.py:861 core/models.py:861
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "Te ha invitado {name} al siguiente documento con el rol \"{role}\" :"
#: build/lib/core/models.py:867 core/models.py:867
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ha compartido un documento contigo: {title}"
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Traza del enlace de documento/usuario"
#: build/lib/core/models.py:968 core/models.py:968
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Trazas del enlace de documento/usuario"
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "Ya existe una traza de enlace para este documento/usuario."
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Documento favorito"
#: build/lib/core/models.py:998 core/models.py:998
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Documentos favoritos"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Este documento ya ha sido marcado como favorito por el usuario."
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Relación documento/usuario"
#: build/lib/core/models.py:1027 core/models.py:1027
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Relaciones documento/usuario"
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Este usuario ya forma parte del documento."
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Este equipo ya forma parte del documento."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr "Debe establecerse un usuario o un equipo, no ambos."
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr "Solicitud de acceso"
#: build/lib/core/models.py:1192 core/models.py:1192
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr "Solicitud de accesos"
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr "Este usuario ya ha solicitado acceso a este documento."
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "¡{name} desea acceder a un documento!"
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} desea acceso al siguiente documento:"
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} está pidiendo acceso al documento: {title}"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "descripción"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "código"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "público"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Si esta plantilla es pública para que cualquiera la utilice."
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Plantilla"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Plantillas"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Relación plantilla/usuario"
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Relaciones plantilla/usuario"
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Este usuario ya forma parte de la plantilla."
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Este equipo ya se encuentra en esta plantilla."
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "dirección de correo electrónico"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Invitación al documento"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Invitaciones a documentos"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Este correo electrónico está asociado a un usuario registrado."
#: core/templates/mail/html/template.html:153
#: core/templates/mail/html/template.html:162
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Logo de correo electrónico"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/html/template.html:209
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Abrir"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr "Docs, su nueva herramienta esencial para organizar, compartir y colaborar en sus documentos como equipo."
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+74 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr "Infos Personnelles"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Permissions"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Dates importantes"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Arborescence"
@@ -50,36 +50,27 @@ msgstr "Masqué"
msgid "Favorite"
msgstr "Favoris"
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "Un nouveau document a été créé pour vous !"
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
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:536 core/api/serializers.py:536
msgid "This field is required."
msgstr "Ce champ est obligatoire."
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "La portée du lien '%(link_reach)s' n'est pas autorisée en fonction de la configuration du document parent."
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Corps"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "Type de corps"
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "copie de {title}"
@@ -178,247 +169,247 @@ msgstr "Obligatoire. 255 caractères ou moins. Caractères ASCII uniquement."
msgid "full name"
msgstr "nom complet"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "nom court"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "adresse e-mail d'identité"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "adresse e-mail de l'administrateur"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "langue"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr "La langue dans laquelle l'utilisateur veut voir l'interface."
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "appareil"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "statut d'équipe"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "actif"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Si cet utilisateur doit être traité comme actif. Désélectionnez ceci au lieu de supprimer des comptes."
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "utilisateur"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "utilisateurs"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "titre"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "extrait"
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Document"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Documents"
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "Document sans titre"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} a partagé un document avec vous!"
#: build/lib/core/models.py:861 core/models.py:861
#: build/lib/core/models.py:858 core/models.py:858
#, 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:867 core/models.py:867
#: build/lib/core/models.py:864 core/models.py:864
#, 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:967 core/models.py:967
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Trace du lien document/utilisateur"
#: build/lib/core/models.py:968 core/models.py:968
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Traces du lien document/utilisateur"
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "Une trace de lien existe déjà pour ce document/utilisateur."
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Document favori"
#: build/lib/core/models.py:998 core/models.py:998
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Documents favoris"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Ce document est déjà un favori de cet utilisateur."
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Relation document/utilisateur"
#: build/lib/core/models.py:1027 core/models.py:1027
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Relations document/utilisateur"
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Cet utilisateur est déjà dans ce document."
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Cette équipe est déjà dans ce document."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr "L'utilisateur ou l'équipe doivent être définis, pas les deux."
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1192 core/models.py:1192
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr "Cet utilisateur a déjà demandé l'accès à ce document."
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} souhaiterait accéder au document suivant !"
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} souhaiterait accéder au document suivant :"
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} demande l'accès au document : {title}"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "description"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "public"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Si ce modèle est public, utilisable par n'importe qui."
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Modèle"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Modèles"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Relation modèle/utilisateur"
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Relations modèle/utilisateur"
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Cet utilisateur est déjà dans ce modèle."
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Cette équipe est déjà modèle."
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "adresse e-mail"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Invitation à un document"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Invitations à un document"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."
#: core/templates/mail/html/template.html:153
#: core/templates/mail/html/template.html:162
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Logo de l'e-mail"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/html/template.html:209
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Ouvrir"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.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/template.html:224
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+74 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr "Informazioni personali"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Permessi"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Date importanti"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Struttura ad albero"
@@ -50,36 +50,27 @@ msgstr ""
msgid "Favorite"
msgstr "Preferiti"
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "Un nuovo documento è stato creato a tuo nome!"
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr "Sei ora proprietario di un nuovo documento:"
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Corpo"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "copia di {title}"
@@ -178,247 +169,247 @@ msgstr ""
msgid "full name"
msgstr "nome completo"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "nome"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "indirizzo email di identità"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "Indirizzo email dell'amministratore"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "lingua"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr "La lingua in cui l'utente vuole vedere l'interfaccia."
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr "Il fuso orario in cui l'utente vuole vedere gli orari."
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "dispositivo"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr "Se l'utente è un dispositivo o un utente reale."
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "stato del personale"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr "Indica se l'utente può accedere a questo sito amministratore."
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "attivo"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Indica se questo utente deve essere trattato come attivo. Deseleziona invece di eliminare gli account."
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "utente"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "utenti"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "titolo"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Documento"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Documenti"
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "Documento senza titolo"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ha condiviso un documento con te!"
#: build/lib/core/models.py:861 core/models.py:861
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} ti ha invitato con il ruolo \"{role}\" nel seguente documento:"
#: build/lib/core/models.py:867 core/models.py:867
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ha condiviso un documento con te: {title}"
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:968 core/models.py:968
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Documento preferito"
#: build/lib/core/models.py:998 core/models.py:998
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Documenti preferiti"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1027 core/models.py:1027
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Questo utente è già presente in questo documento."
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Questo team è già presente in questo documento."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1192 core/models.py:1192
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "descrizione"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "pubblico"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Indica se questo modello è pubblico per chiunque."
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Modello"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Modelli"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Questo utente è già in questo modello."
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Questo team è già in questo modello."
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "indirizzo e-mail"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Invito al documento"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Inviti al documento"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Questa email è già associata a un utente registrato."
#: core/templates/mail/html/template.html:153
#: core/templates/mail/html/template.html:162
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Logo e-mail"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/html/template.html:209
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Apri"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+115 -124
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -17,22 +17,22 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr "Persoonlijke informatie"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Machtigingen"
msgstr "Toestemmingen"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Belangrijke data"
msgstr "Belangrijke datums"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Boomstructuur"
msgstr "Document structuur"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
msgid "Title"
@@ -40,46 +40,37 @@ msgstr "Titel"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
msgid "Creator is me"
msgstr "Ik ben eigenaar"
msgstr "Ik ben Eigenaar"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr "Gemaskeerd"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "Favoriet"
msgstr "Favoriete"
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "Een nieuw document is namens u gemaakt!"
msgstr "Een nieuw document was gecreëerd voor u!"
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr "U heeft eigenaarschap van een nieuw document gekregen:"
msgstr "U heeft eigenaarschap van een nieuw document:"
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr "Dit veld is verplicht."
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Link bereik '%(link_reach)s' is niet toegestaan op basis van bovenliggende documentconfiguratie."
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Text"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "Text type"
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Formaat"
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "kopie van {title}"
@@ -92,11 +83,11 @@ msgstr "Lezer"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:43 core/choices.py:36
#: core/choices.py:43
msgid "Editor"
msgstr "Redacteur"
msgstr "Bewerker"
#: build/lib/core/choices.py:44 core/choices.py:44
msgid "Administrator"
msgstr "Beheerder"
msgstr "Administrator"
#: build/lib/core/choices.py:45 core/choices.py:45
msgid "Owner"
@@ -104,7 +95,7 @@ msgstr "Eigenaar"
#: build/lib/core/choices.py:56 core/choices.py:56
msgid "Restricted"
msgstr "Beperkt"
msgstr "Niet toegestaan"
#: build/lib/core/choices.py:60 core/choices.py:60
msgid "Authenticated"
@@ -148,11 +139,11 @@ msgstr "primaire sleutel voor dossier als UUID"
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "gecreëerd op"
msgstr "gemaakt op"
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr "datum en tijd waarop dossier is gecreeërd"
msgstr "datum en tijd wanneer dossier was gecreëerd"
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
@@ -164,7 +155,7 @@ msgstr "datum en tijd waarop dossier laatst was gewijzigd"
#: build/lib/core/models.py:130 core/models.py:130
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Wij konden geen gebruiker vinden met dit id, maar de email is al geassocieerd met een geregistreerde gebruiker."
msgstr "Wij konden geen gebruiker vinden met deze id, maar de email is al geassocieerd met een geregistreerde gebruiker."
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
@@ -172,253 +163,253 @@ msgstr "id"
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "Vereist. 255 tekens of minder. Alleen ASCII tekens."
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "volledige naam"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "gebruikersnaam"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "identiteit emailadres"
msgstr "identiteit email adres"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "admin emailadres"
msgstr "admin email adres"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "taal"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr "De taal waarin de gebruiker de interface wil zien."
msgstr "De taal waarin de gebruiker de interface wilt zien."
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr "De tijdzone waarin de gebruiker de tijden wil zien."
msgstr "De tijdzone waarin de gebruiker de tijden wilt zien."
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "apparaat"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr "Of de gebruiker een apparaat is of een echte gebruiker."
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "beheerder status"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr "Of de gebruiker kan inloggen in het beheer gedeelte."
msgstr "Of de gebruiker kan inloggen in het admin gedeelte."
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "actief"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Of een gebruiker als actief moet worden beschouwd. Deselecteer dit in plaats van het account te deleten."
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "gebruiker"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "gebruikers"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "titel"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "uittreksel"
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Document"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Documenten"
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "Naamloos Document"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} heeft een document met u gedeeld!"
msgstr "{name} heeft een document met gedeeld!"
#: build/lib/core/models.py:861 core/models.py:861
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} heeft u uitgenodigd met de rol \"{role}\" op het volgende document:"
#: build/lib/core/models.py:867 core/models.py:867
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} heeft een document met u gedeeld: {title}"
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Document/gebruiker link"
msgstr "Document/gebruiker url"
#: build/lib/core/models.py:968 core/models.py:968
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Document/gebruiker link"
msgstr "Document/gebruiker url"
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "Een link bestaat al voor dit document/deze gebruiker."
msgstr "Een url bestaat al voor dit document/deze gebruiker."
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Document favoriet"
#: build/lib/core/models.py:998 core/models.py:998
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Document favorieten"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Dit document is al in gebruik als favoriet door dezelfde gebruiker."
msgstr "Dit document is al in gebruik als favoriete door dezelfde gebruiker."
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Document/gebruiker relatie"
#: build/lib/core/models.py:1027 core/models.py:1027
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Document/gebruiker relaties"
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "De gebruiker bestaat al in dit document."
msgstr "De gebruiker is al in dit document."
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Dit team bestaat al in dit document."
msgstr "Het team is al in dit document."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr "Een gebruiker of team moet gekozen worden, maar niet beide."
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr "Document verzoekt om toegang"
msgstr ""
#: build/lib/core/models.py:1192 core/models.py:1192
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr "Document verzoekt om toegangen"
msgstr ""
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr "Deze gebruiker heeft al om toegang tot dit document gevraagd."
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} verzoekt toegang tot een document!"
msgstr ""
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} verzoekt toegang tot het volgende document:"
msgstr ""
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} verzoekt toegang tot het document: {title}"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "omschrijving"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "publiek"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Of dit sjabloon door iedereen publiekelijk te gebruiken is."
msgstr "Of dit template als publiek is en door iedereen te gebruiken is."
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Sjabloon"
msgstr "Template"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Sjabloon"
msgstr "Templates"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Sjabloon/gebruiker relatie"
msgstr "Template/gebruiker relatie"
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Sjabloon/gebruiker relaties"
msgstr "Template/gebruiker relaties"
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "De gebruiker bestaat al in dit sjabloon."
msgstr "De gebruiker bestaat al in dit template."
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Het team bestaat al in dit sjabloon."
msgstr "Het team bestaat al in dit template."
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "e-mailadres"
msgstr "email adres"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Document uitnodiging"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Document uitnodigingen"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."
#: core/templates/mail/html/template.html:153
#: core/templates/mail/html/template.html:162
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Logo email"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/html/template.html:209
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Open"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs, jouw nieuwe essentiële tool voor het organiseren, delen en collaboreren van documenten als team. "
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+74 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr "Informações Pessoais"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Permissões"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Datas importantes"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Estrutura de árvore"
@@ -50,36 +50,27 @@ msgstr ""
msgid "Favorite"
msgstr "Favorito"
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "Um novo documento foi criado em seu nome!"
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr "A propriedade de um novo documento foi concedida a você:"
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Corpo"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "Tipo de corpo"
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "cópia de {title}"
@@ -178,247 +169,247 @@ msgstr ""
msgid "full name"
msgstr ""
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr ""
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr ""
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr ""
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr ""
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr ""
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr ""
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr ""
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:861 core/models.py:861
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:867 core/models.py:867
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:968 core/models.py:968
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:998 core/models.py:998
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1027 core/models.py:1027
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1192 core/models.py:1192
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr ""
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr ""
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr ""
#: core/templates/mail/html/template.html:153
#: core/templates/mail/html/template.html:162
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr ""
#: core/templates/mail/html/template.html:200
#: core/templates/mail/html/template.html:209
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr ""
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+74 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr "Личная информация"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: 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:61 core/admin.py:61
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Важные даты"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Древовидная структура"
@@ -50,36 +50,27 @@ msgstr "Скрытый"
msgid "Favorite"
msgstr "Избранное"
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "Новый документ был создан от вашего имени!"
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr "Вы назначены владельцем для нового документа:"
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr "Это поле обязательное."
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Доступ по ссылке '%(link_reach)s' запрещён в соответствии с настройками родительского документа."
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Текст сообщения"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "Тип сообщения"
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Формат"
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "копия {title}"
@@ -178,247 +169,247 @@ msgstr "Обязательно. 255 символов или меньше. Тол
msgid "full name"
msgstr "полное имя"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "короткое имя"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "личный адрес электронной почты"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "e-mail администратора"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "язык"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr "Язык, на котором пользователь хочет видеть интерфейс."
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr "Часовой пояс, в котором пользователь хочет видеть время."
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "устройство"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr "Пользователь является устройством или человеком."
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "статус сотрудника"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr "Может ли пользователь войти на этот административный сайт."
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "активный"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Должен ли пользователь рассматриваться как активный. Альтернатива удалению учётных записей."
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "пользователь"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "пользователи"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "заголовок"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "отрывок"
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Документ"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Документы"
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "Безымянный документ"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} делится с вами документом!"
#: build/lib/core/models.py:861 core/models.py:861
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} приглашает вас присоединиться к следующему документу с ролью \"{role}\":"
#: build/lib/core/models.py:867 core/models.py:867
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} делится с вами документом: {title}"
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Трассировка связи документ/пользователь"
#: build/lib/core/models.py:968 core/models.py:968
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Трассировка связей документ/пользователь"
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "Для этого документа/пользователя уже существует трассировка ссылки."
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Избранный документ"
#: build/lib/core/models.py:998 core/models.py:998
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Избранные документы"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Этот документ уже помечен как избранный для этого пользователя."
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Отношение документ/пользователь"
#: build/lib/core/models.py:1027 core/models.py:1027
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Отношения документ/пользователь"
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Этот пользователь уже имеет доступ к этому документу."
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Эта команда уже имеет доступ к этому документу."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr "Документ запрашивает доступ"
#: build/lib/core/models.py:1192 core/models.py:1192
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr "Документ запрашивает доступы"
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr "Этот пользователь уже запросил доступ к этому документу."
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хочет получить доступ к документу!"
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} хочет получить доступ к следующему документу:"
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запрашивает доступ к документу: {title}"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "описание"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "код"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "доступно всем"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Этот шаблон доступен всем пользователям."
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Шаблон"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Шаблоны"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Отношение шаблон/пользователь"
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Отношения шаблон/пользователь"
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Этот пользователь уже указан в этом шаблоне."
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Эта команда уже указана в этом шаблоне."
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "адрес электронной почты"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Приглашение для документа"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Приглашения для документов"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Этот адрес уже связан с зарегистрированным пользователем."
#: core/templates/mail/html/template.html:153
#: core/templates/mail/html/template.html:162
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Логотип email"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/html/template.html:209
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Открыть"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs, ваш новый инструмент для организации и совместного использования документов в вашей команде. "
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+74 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr "Osebni podatki"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Dovoljenja"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Pomembni datumi"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Drevesna struktura"
@@ -50,36 +50,27 @@ msgstr ""
msgid "Favorite"
msgstr "Priljubljena"
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "Nov dokument je bil ustvarjen v vašem imenu!"
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr "Dodeljeno vam je bilo lastništvo nad novim dokumentom:"
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Telo"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "Vrsta telesa"
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Oblika"
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -178,247 +169,247 @@ msgstr ""
msgid "full name"
msgstr "polno ime"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "kratko ime"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "elektronski naslov identitete"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "elektronski naslov skrbnika"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "jezik"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr "Jezik, v katerem uporabnik želi videti vmesnik."
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr "Časovni pas, v katerem želi uporabnik videti uro."
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "naprava"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr "Ali je uporabnik naprava ali pravi uporabnik."
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "kadrovski status"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr "Ali se uporabnik lahko prijavi na to skrbniško mesto."
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "aktivni"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Ali je treba tega uporabnika obravnavati kot aktivnega. Namesto brisanja računov počistite to izbiro."
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "uporabnik"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "uporabniki"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "naslov"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "odlomek"
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Dokument"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Dokumenti"
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "Dokument brez naslova"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} je delil dokument z vami!"
#: build/lib/core/models.py:861 core/models.py:861
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} vas je povabil z vlogo \"{role}\" na naslednjem dokumentu:"
#: build/lib/core/models.py:867 core/models.py:867
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} je delil dokument z vami: {title}"
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Dokument/sled povezave uporabnika"
#: build/lib/core/models.py:968 core/models.py:968
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Sledi povezav dokumenta/uporabnika"
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Priljubljeni dokument"
#: build/lib/core/models.py:998 core/models.py:998
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Priljubljeni dokumenti"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Ta dokument je že ciljno usmerjen s priljubljenim primerkom relacije za istega uporabnika."
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Odnos dokument/uporabnik"
#: build/lib/core/models.py:1027 core/models.py:1027
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Odnosi dokument/uporabnik"
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Ta uporabnik je že v tem dokumentu."
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Ta ekipa je že v tem dokumentu."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr "Nastaviti je treba bodisi uporabnika ali ekipo, a ne obojega."
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1192 core/models.py:1192
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "opis"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "koda"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "javno"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Ali je ta predloga javna za uporabo."
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Predloga"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Predloge"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Odnos predloga/uporabnik"
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Odnosi med predlogo in uporabnikom"
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Ta uporabnik je že v tej predlogi."
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Ta ekipa je že v tej predlogi."
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "elektronski naslov"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Vabilo na dokument"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Vabila na dokument"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."
#: core/templates/mail/html/template.html:153
#: core/templates/mail/html/template.html:162
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "E-pošta z logotipom"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/html/template.html:209
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Odpri"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Dokumenti, vaše novo bistveno orodje za organiziranje, skupno rabo in skupinsko sodelovanje pri dokumentih. "
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+74 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr "Personuppgifter"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:50 build/lib/core/admin.py:138 core/admin.py:50
#: core/admin.py:138
msgid "Permissions"
msgstr "Behörigheter"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Viktiga datum"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr ""
@@ -50,36 +50,27 @@ msgstr ""
msgid "Favorite"
msgstr "Favoriter"
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "Ett nytt dokument skapades åt dig!"
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr "Du har beviljats äganderätt till ett nytt dokument:"
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -178,247 +169,247 @@ msgstr ""
msgid "full name"
msgstr ""
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr ""
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr ""
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr ""
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "aktiv"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr ""
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr ""
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr ""
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:861 core/models.py:861
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:867 core/models.py:867
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:968 core/models.py:968
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:998 core/models.py:998
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1027 core/models.py:1027
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1192 core/models.py:1192
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr ""
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr ""
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "e-postadress"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Bjud in dokument"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Inbjudningar dokument"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Denna e-postadress är redan associerad med en registrerad användare."
#: core/templates/mail/html/template.html:153
#: core/templates/mail/html/template.html:162
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Logotyp e-post"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/html/template.html:209
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Öppna"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+74 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr ""
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: 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:61 core/admin.py:61
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr ""
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr ""
@@ -50,36 +50,27 @@ msgstr ""
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -178,247 +169,247 @@ msgstr ""
msgid "full name"
msgstr ""
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr ""
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr ""
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr ""
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr ""
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr ""
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr ""
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr ""
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:861 core/models.py:861
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:867 core/models.py:867
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:968 core/models.py:968
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:998 core/models.py:998
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1027 core/models.py:1027
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1192 core/models.py:1192
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr ""
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr ""
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr ""
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr ""
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr ""
#: core/templates/mail/html/template.html:153
#: core/templates/mail/html/template.html:162
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr ""
#: core/templates/mail/html/template.html:200
#: core/templates/mail/html/template.html:209
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr ""
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+74 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr "Особисті дані"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: 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:61 core/admin.py:61
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "Важливі дати"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "Ієрархічна структура"
@@ -50,36 +50,27 @@ msgstr "Приховано"
msgid "Favorite"
msgstr "Обране"
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "Новий документ був створений від вашого імені!"
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr "Ви тепер є власником нового документа:"
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr "Це поле є обов’язковим."
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Доступ до посилання '%(link_reach)s' заборонено на основі конфігурації батьківського документа."
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "Вміст"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "Тип вмісту"
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "Формат"
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "копія {title}"
@@ -178,247 +169,247 @@ msgstr "Обов'язкове. 255 символів або менше. Тіль
msgid "full name"
msgstr "повне ім'я"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "коротке ім'я"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "адреса електронної пошти особи"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "електронна адреса адміністратора"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "мова"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr "Мова, якою користувач хоче бачити інтерфейс."
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr "Часовий пояс, в якому користувач хоче бачити час."
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "пристрій"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr "Чи є користувач пристроєм чи реальним користувачем."
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "статус співробітника"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr "Чи може користувач увійти на цей сайт адміністратора."
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "активний"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Чи слід ставитися до цього користувача як до активного. Зніміть вибір замість видалення облікового запису."
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "користувач"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "користувачі"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "заголовок"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "уривок"
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "Документ"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "Документи"
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "Документ без назви"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ділиться з вами документом!"
#: build/lib/core/models.py:861 core/models.py:861
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} запрошує вас для роботи з документом із роллю \"{role}\":"
#: build/lib/core/models.py:867 core/models.py:867
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ділиться з вами документом: {title}"
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "Трасування посилання Документ/користувач"
#: build/lib/core/models.py:968 core/models.py:968
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "Трасування посилань Документ/користувач"
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "Відстеження вже існуючих посилань для цього документа/користувача."
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "Обраний документ"
#: build/lib/core/models.py:998 core/models.py:998
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "Обрані документи"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Цей документ вже вказаний як обраний для одного користувача."
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1027 core/models.py:1027
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "Цей користувач вже має доступ до цього документу."
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "Ця команда вже має доступ до цього документа."
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr "Вкажіть користувача або команду, а не обох."
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr "Запит доступу до документа"
#: build/lib/core/models.py:1192 core/models.py:1192
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr "Запит доступу для документа"
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr "Цей користувач вже попросив доступ до цього документа."
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хоче отримати доступ до документа!"
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} бажає отримати доступ до наступного документа:"
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запитує доступ до документа: {title}"
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "опис"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "код"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "публічне"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "Чи є цей шаблон публічним для будь-кого користувача."
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "Шаблон"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "Шаблони"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "Відношення шаблон/користувач"
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "Відношення шаблон/користувач"
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "Цей користувач вже має доступ до цього шаблону."
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "Ця команда вже має доступ до цього шаблону."
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "електронна адреса"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "Запрошення до редагування документа"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "Запрошення до редагування документів"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."
#: core/templates/mail/html/template.html:153
#: core/templates/mail/html/template.html:162
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Логотип пошти"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/html/template.html:209
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Відкрити"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs, ваш новий важливий інструмент для організації, обміну та командної співпраці над вашими документами. "
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+82 -91
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-23 11:01+0000\n"
"PO-Revision-Date: 2025-11-10 09:54\n"
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
"PO-Revision-Date: 2025-09-12 09:50\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:37 core/admin.py:37
msgid "Personal info"
msgstr "个人信息"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: 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:61 core/admin.py:61
#: build/lib/core/admin.py:62 core/admin.py:62
msgid "Important dates"
msgstr "重要日期"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:148 core/admin.py:148
msgid "Tree structure"
msgstr "树状结构"
@@ -44,42 +44,33 @@ msgstr "创建者是我"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr "已屏蔽"
msgstr ""
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "收藏"
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
msgid "A new document was created on your behalf!"
msgstr "已为您创建了一份新文档!"
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
msgid "You have been granted ownership of a new document:"
msgstr "您已被授予新文档的所有权:"
#: build/lib/core/api/serializers.py:536 core/api/serializers.py:536
msgid "This field is required."
msgstr "必填字段。"
#: build/lib/core/api/serializers.py:547 core/api/serializers.py:547
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:693 core/api/serializers.py:693
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
msgid "Body"
msgstr "正文"
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
msgid "Body type"
msgstr "正文类型"
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
msgid "Format"
msgstr "格式"
#: build/lib/core/api/viewsets.py:1003 core/api/viewsets.py:1003
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
#, python-brace-format
msgid "copy of {title}"
msgstr "{title} 的副本"
@@ -172,253 +163,253 @@ msgstr "sub"
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "必填项。限255个字符以内。仅支持ASCII字符。"
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "全名"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:151 core/models.py:151
msgid "short name"
msgstr "简称"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:153 core/models.py:153
msgid "identity email address"
msgstr "身份电子邮件地址"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:158 core/models.py:158
msgid "admin email address"
msgstr "管理员电子邮件地址"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:165 core/models.py:165
msgid "language"
msgstr "语言"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:166 core/models.py:166
msgid "The language in which the user wants to see the interface."
msgstr "用户希望看到的界面语言。"
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:174 core/models.py:174
msgid "The timezone in which the user wants to see times."
msgstr "用户查看时间希望的时区。"
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:177 core/models.py:177
msgid "device"
msgstr "设备"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:179 core/models.py:179
msgid "Whether the user is a device or a real user."
msgstr "用户是设备还是真实用户。"
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:182 core/models.py:182
msgid "staff status"
msgstr "员工状态"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:184 core/models.py:184
msgid "Whether the user can log into this admin site."
msgstr "用户是否可以登录该管理员站点。"
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:187 core/models.py:187
msgid "active"
msgstr "激活"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:190 core/models.py:190
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "是否应将此用户视为活跃用户。取消选择此选项而不是删除账户。"
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:202 core/models.py:202
msgid "user"
msgstr "用户"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:203 core/models.py:203
msgid "users"
msgstr "个用户"
#: build/lib/core/models.py:361 build/lib/core/models.py:1284
#: core/models.py:361 core/models.py:1284
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
#: core/models.py:359 core/models.py:1281
msgid "title"
msgstr "标题"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:360 core/models.py:360
msgid "excerpt"
msgstr "摘要"
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:409 core/models.py:409
msgid "Document"
msgstr "文档"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:410 core/models.py:410
msgid "Documents"
msgstr "个文档"
#: build/lib/core/models.py:424 build/lib/core/models.py:822 core/models.py:424
#: core/models.py:822
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
#: core/models.py:819
msgid "Untitled Document"
msgstr "未命名文档"
#: build/lib/core/models.py:857 core/models.py:857
#: build/lib/core/models.py:854 core/models.py:854
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} 与您共享了一个文档!"
#: build/lib/core/models.py:861 core/models.py:861
#: build/lib/core/models.py:858 core/models.py:858
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} 邀请您以“{role}”角色访问以下文档:"
#: build/lib/core/models.py:867 core/models.py:867
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} 与您共享了一个文档:{title}"
#: build/lib/core/models.py:967 core/models.py:967
#: build/lib/core/models.py:964 core/models.py:964
msgid "Document/user link trace"
msgstr "文档/用户链接跟踪"
#: build/lib/core/models.py:968 core/models.py:968
#: build/lib/core/models.py:965 core/models.py:965
msgid "Document/user link traces"
msgstr "个文档/用户链接跟踪"
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:971 core/models.py:971
msgid "A link trace already exists for this document/user."
msgstr "此文档/用户的链接跟踪已存在。"
#: build/lib/core/models.py:997 core/models.py:997
#: build/lib/core/models.py:994 core/models.py:994
msgid "Document favorite"
msgstr "文档收藏"
#: build/lib/core/models.py:998 core/models.py:998
#: build/lib/core/models.py:995 core/models.py:995
msgid "Document favorites"
msgstr "文档收藏夹"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1001 core/models.py:1001
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "该文档已被同一用户的收藏关系实例关联。"
#: build/lib/core/models.py:1026 core/models.py:1026
#: build/lib/core/models.py:1023 core/models.py:1023
msgid "Document/user relation"
msgstr "文档/用户关系"
#: build/lib/core/models.py:1027 core/models.py:1027
#: build/lib/core/models.py:1024 core/models.py:1024
msgid "Document/user relations"
msgstr "文档/用户关系集"
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1030 core/models.py:1030
msgid "This user is already in this document."
msgstr "该用户已在此文档中。"
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1036 core/models.py:1036
msgid "This team is already in this document."
msgstr "该团队已在此文档中。"
#: build/lib/core/models.py:1045 build/lib/core/models.py:1370
#: core/models.py:1045 core/models.py:1370
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
#: core/models.py:1042 core/models.py:1367
msgid "Either user or team must be set, not both."
msgstr "必须设置用户或团队之一,不能同时设置两者。"
#: build/lib/core/models.py:1191 core/models.py:1191
#: build/lib/core/models.py:1188 core/models.py:1188
msgid "Document ask for access"
msgstr "文档需要访问权限"
msgstr ""
#: build/lib/core/models.py:1192 core/models.py:1192
#: build/lib/core/models.py:1189 core/models.py:1189
msgid "Document ask for accesses"
msgstr "文档需要访问权限"
msgstr ""
#: build/lib/core/models.py:1198 core/models.py:1198
#: build/lib/core/models.py:1195 core/models.py:1195
msgid "This user has already asked for access to this document."
msgstr "用户已申请该文档的访问权限。"
msgstr ""
#: build/lib/core/models.py:1263 core/models.py:1263
#: build/lib/core/models.py:1260 core/models.py:1260
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} 申请访问文档!"
msgstr ""
#: build/lib/core/models.py:1267 core/models.py:1267
#: build/lib/core/models.py:1264 core/models.py:1264
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} 申请访问以下文档:"
msgstr ""
#: build/lib/core/models.py:1273 core/models.py:1273
#: build/lib/core/models.py:1270 core/models.py:1270
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name}申请文档:{title}的访问权限"
msgstr ""
#: build/lib/core/models.py:1285 core/models.py:1285
#: build/lib/core/models.py:1282 core/models.py:1282
msgid "description"
msgstr "说明"
#: build/lib/core/models.py:1286 core/models.py:1286
#: build/lib/core/models.py:1283 core/models.py:1283
msgid "code"
msgstr "代码"
#: build/lib/core/models.py:1287 core/models.py:1287
#: build/lib/core/models.py:1284 core/models.py:1284
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1289 core/models.py:1289
#: build/lib/core/models.py:1286 core/models.py:1286
msgid "public"
msgstr "公开"
#: build/lib/core/models.py:1291 core/models.py:1291
#: build/lib/core/models.py:1288 core/models.py:1288
msgid "Whether this template is public for anyone to use."
msgstr "该模板是否公开供任何人使用。"
#: build/lib/core/models.py:1297 core/models.py:1297
#: build/lib/core/models.py:1294 core/models.py:1294
msgid "Template"
msgstr "模板"
#: build/lib/core/models.py:1298 core/models.py:1298
#: build/lib/core/models.py:1295 core/models.py:1295
msgid "Templates"
msgstr "模板"
#: build/lib/core/models.py:1351 core/models.py:1351
#: build/lib/core/models.py:1348 core/models.py:1348
msgid "Template/user relation"
msgstr "模板/用户关系"
#: build/lib/core/models.py:1352 core/models.py:1352
#: build/lib/core/models.py:1349 core/models.py:1349
msgid "Template/user relations"
msgstr "模板/用户关系集"
#: build/lib/core/models.py:1358 core/models.py:1358
#: build/lib/core/models.py:1355 core/models.py:1355
msgid "This user is already in this template."
msgstr "该用户已在此模板中。"
#: build/lib/core/models.py:1364 core/models.py:1364
#: build/lib/core/models.py:1361 core/models.py:1361
msgid "This team is already in this template."
msgstr "该团队已在此模板中。"
#: build/lib/core/models.py:1441 core/models.py:1441
#: build/lib/core/models.py:1438 core/models.py:1438
msgid "email address"
msgstr "电子邮件地址"
#: build/lib/core/models.py:1460 core/models.py:1460
#: build/lib/core/models.py:1457 core/models.py:1457
msgid "Document invitation"
msgstr "文档邀请"
#: build/lib/core/models.py:1461 core/models.py:1461
#: build/lib/core/models.py:1458 core/models.py:1458
msgid "Document invitations"
msgstr "文档邀请"
#: build/lib/core/models.py:1481 core/models.py:1481
#: build/lib/core/models.py:1478 core/models.py:1478
msgid "This email is already associated to a registered user."
msgstr "此电子邮件已经与现有注册用户关联。"
#: core/templates/mail/html/template.html:153
#: core/templates/mail/html/template.html:162
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "徽标邮件"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/html/template.html:209
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "打开"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs——您的全新必备工具,帮助团队组织、共享和协作处理文档。 "
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:233
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+30 -30
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "3.9.0"
version = "3.7.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -25,42 +25,42 @@ license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"beautifulsoup4==4.14.2",
"boto3==1.40.59",
"Brotli==1.2.0",
"beautifulsoup4==4.13.4",
"boto3==1.39.4",
"Brotli==1.1.0",
"celery[redis]==5.5.3",
"django-configurations==2.5.1",
"django-cors-headers==4.9.0",
"django-cors-headers==4.7.0",
"django-countries==7.6.1",
"django-csp==4.0",
"django-filter==25.2",
"django-lasuite[all]==0.0.16",
"django-filter==25.1",
"django-lasuite[all]==0.0.14",
"django-parler==2.3",
"django-redis==6.0.0",
"django-storages[s3]==1.14.6",
"django-timezone-field>=5.1",
"django==5.2.8",
"django==5.2.7",
"django-treebeard==4.7.1",
"djangorestframework==3.16.1",
"djangorestframework==3.16.0",
"drf_spectacular==0.28.0",
"dockerflow==2024.4.2",
"easy_thumbnails==2.10.1",
"easy_thumbnails==2.10",
"factory_boy==3.3.3",
"gunicorn==23.0.0",
"jsonschema==4.25.1",
"lxml==6.0.2",
"markdown==3.9",
"jsonschema==4.24.0",
"lxml==6.0.0",
"markdown==3.8.2",
"mozilla-django-oidc==4.0.1",
"nested-multipart-parser==1.6.0",
"openai==2.6.1",
"psycopg[binary]==3.2.12",
"pycrdt==0.12.42",
"nested-multipart-parser==1.5.0",
"openai==1.95.0",
"psycopg[binary]==3.2.9",
"pycrdt==0.12.25",
"PyJWT==2.10.1",
"python-magic==0.4.27",
"redis<6.0.0",
"requests==2.32.5",
"sentry-sdk==2.42.1",
"whitenoise==6.11.0",
"requests==2.32.4",
"sentry-sdk==2.32.0",
"whitenoise==6.9.0",
]
[project.urls]
@@ -73,21 +73,21 @@ dependencies = [
dev = [
"django-extensions==4.1",
"django-test-migrations==1.5.0",
"drf-spectacular-sidecar==2025.10.1",
"freezegun==1.5.5",
"drf-spectacular-sidecar==2025.7.1",
"freezegun==1.5.2",
"ipdb==0.13.13",
"ipython==9.6.0",
"pyfakefs==5.10.0",
"ipython==9.4.0",
"pyfakefs==5.9.1",
"pylint-django==2.6.1",
"pylint<4.0.0",
"pytest-cov==7.0.0",
"pylint==3.3.7",
"pytest-cov==6.2.1",
"pytest-django==4.11.1",
"pytest==8.4.2",
"pytest==8.4.1",
"pytest-icdiff==0.9",
"pytest-xdist==3.8.0",
"responses==0.25.8",
"ruff==0.14.2",
"types-requests==2.32.4.20250913",
"responses==0.25.7",
"ruff==0.12.2",
"types-requests==2.32.4.20250611",
]
[tool.setuptools]
@@ -13,11 +13,7 @@ import {
} from './utils-common';
import { getEditor, openSuggestionMenu, writeInEditor } from './utils-editor';
import { connectOtherUserToDoc, updateShareLink } from './utils-share';
import {
createRootSubPage,
getTreeRow,
navigateToPageFromTree,
} from './utils-sub-pages';
import { createRootSubPage, navigateToPageFromTree } from './utils-sub-pages';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -494,7 +490,7 @@ test.describe('Doc Editor', () => {
if (request.method().includes('GET')) {
await route.fulfill({
json: {
status: requestCount > 1 ? 'ready' : 'processing',
status: requestCount ? 'ready' : 'processing',
file: '/anything.html',
},
});
@@ -518,12 +514,6 @@ test.describe('Doc Editor', () => {
await fileChooser.setFiles(path.join(__dirname, 'assets/test.html'));
await expect(editor.getByText('Analyzing file...')).toBeVisible();
// To be sure the retry happens even after a page reload
await page.reload();
await expect(editor.getByText('Analyzing file...')).toBeVisible();
// The retry takes a few seconds
await expect(editor.getByText('test.html')).toBeVisible({
timeout: 7000,
@@ -686,9 +676,10 @@ test.describe('Doc Editor', () => {
await calloutBlock.locator('.inline-content').fill('example text');
await expect(
page.locator('.bn-block-content[data-content-type="callout"]').first(),
).toHaveAttribute('data-background-color', 'yellow');
await expect(page.locator('.bn-block').first()).toHaveAttribute(
'data-background-color',
'yellow',
);
const emojiButton = calloutBlock.getByRole('button');
await expect(emojiButton).toHaveText('💡');
@@ -712,9 +703,10 @@ test.describe('Doc Editor', () => {
await page.locator('.mantine-Menu-dropdown > button').last().click();
await page.locator('.bn-color-picker-dropdown > button').last().click();
await expect(
page.locator('.bn-block-content[data-content-type="callout"]').first(),
).toHaveAttribute('data-background-color', 'pink');
await expect(page.locator('.bn-block').first()).toHaveAttribute(
'data-background-color',
'pink',
);
});
test('it checks interlink feature', async ({ page, browserName }) => {
@@ -738,13 +730,7 @@ test.describe('Doc Editor', () => {
await verifyDocName(page, docChild2);
const treeRow = await getTreeRow(page, docChild2);
await treeRow.locator('.--docs--doc-icon').click();
await page.getByRole('button', { name: '😀' }).first().click();
await navigateToPageFromTree({ page, title: docChild1 });
await openSuggestionMenu({ page });
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Link a doc').first().click();
const input = page.locator(
@@ -758,16 +744,6 @@ test.describe('Doc Editor', () => {
await expect(searchContainer.getByText(docChild1)).toBeVisible();
await expect(searchContainer.getByText(docChild2)).toBeVisible();
const searchContainerRow = searchContainer
.getByRole('option')
.filter({
hasText: docChild2,
})
.first();
await expect(searchContainerRow).toContainText('😀');
await expect(searchContainerRow.locator('svg').first()).toBeHidden();
await input.pressSequentially('-child');
await expect(searchContainer.getByText(docChild1)).toBeVisible();
@@ -782,30 +758,32 @@ test.describe('Doc Editor', () => {
await expect(searchContainer).toBeHidden();
// Wait for the interlink to be created and rendered
const editor = await getEditor({ page });
const editor = page.locator('.ProseMirror.bn-editor');
const interlinkChild2 = editor.getByRole('button', {
const interlink = editor.getByRole('link', {
name: docChild2,
});
await expect(interlinkChild2).toBeVisible({ timeout: 10000 });
await expect(interlinkChild2).toContainText('😀');
await expect(interlinkChild2.locator('svg').first()).toBeHidden();
await interlinkChild2.click();
await expect(interlink).toBeVisible({ timeout: 10000 });
await interlink.click();
await verifyDocName(page, docChild2);
});
test('it checks interlink shortcut @', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-interlink', browserName, 1);
await verifyDocName(page, randomDoc);
const editor = page.locator('.bn-block-outer').last();
await editor.click();
await page.keyboard.press('@');
await input.fill(docChild1);
await searchContainer.getByText(docChild1).click();
const interlinkChild1 = editor.getByRole('button', {
name: docChild1,
});
await expect(interlinkChild1).toBeVisible({ timeout: 10000 });
await expect(interlinkChild1.locator('svg').first()).toBeVisible();
await expect(
page.locator(
"span[data-inline-content-type='interlinkingSearchInline'] input",
),
).toBeVisible();
});
test('it checks multiple big doc scroll to the top', async ({
@@ -866,12 +844,13 @@ test.describe('Doc Editor', () => {
await expect(pdfBlock).toBeVisible();
await page.getByText(/Add (PDF|file)/).click();
await page.getByText('Add PDF').click();
const fileChooserPromise = page.waitForEvent('filechooser');
const downloadPromise = page.waitForEvent('download');
await page.getByText(/Upload (PDF|file)/).click();
await page.getByText('Upload file').click();
const fileChooser = await fileChooserPromise;
console.log(path.join(__dirname, 'assets/test-pdf.pdf'));
await fileChooser.setFiles(path.join(__dirname, 'assets/test-pdf.pdf'));
// Wait for the media-check to be processed
@@ -2,11 +2,12 @@ import path from 'path';
import { expect, test } from '@playwright/test';
import cs from 'convert-stream';
import { PDFParse } from 'pdf-parse';
import pdf from 'pdf-parse';
import {
TestLanguage,
createDoc,
randomName,
verifyDocName,
waitForLanguageSwitch,
} from './utils-common';
@@ -58,16 +59,20 @@ test.describe('Doc Export', () => {
await verifyDocName(page, randomDoc);
const editor = await writeInEditor({ page, text: 'Hello' });
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 openSuggestionMenu({ page });
await editor.locator('.bn-block-outer').last().fill('/');
await page.getByText('Page Break').click();
await expect(
editor.locator('div[data-content-type="pageBreak"]'),
).toBeVisible();
await expect(editor.locator('.bn-page-break')).toBeVisible();
await writeInEditor({ page, text: 'World' });
await page.keyboard.press('Enter');
await editor.locator('.bn-block-outer').last().fill('World');
await page
.getByRole('button', {
@@ -85,16 +90,11 @@ test.describe('Doc Export', () => {
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfParse = new PDFParse({ data: pdfBuffer });
const pdfInfo = await pdfParse.getInfo();
const pdfText = await pdfParse.getText();
const pdfData = await pdf(pdfBuffer);
expect(pdfInfo.total).toBe(2);
expect(pdfText.pages).toStrictEqual([
{ text: 'Hello', num: 1 },
{ text: 'World', num: 2 },
]);
expect(pdfInfo?.info.Title).toBe(randomDoc);
expect(pdfData.numpages).toBe(2);
expect(pdfData.text).toContain('\n\nHello\n\nWorld'); // This is the doc text
expect(pdfData.info.Title).toBe(randomDoc);
});
test('it exports the doc to docx', async ({ page, browserName }) => {
@@ -223,10 +223,10 @@ test.describe('Doc Export', () => {
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfExport = await pdf(pdfBuffer);
const pdfText = pdfExport.text;
const pdfParse = new PDFParse({ data: pdfBuffer });
const pdfText = await pdfParse.getText();
expect(pdfText.text).toContain('Hello World');
expect(pdfText).toContain('Hello World');
});
test('it exports the doc with quotes', async ({ page, browserName }) => {
@@ -269,9 +269,52 @@ test.describe('Doc Export', () => {
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfParse = new PDFParse({ data: pdfBuffer });
const pdfText = await pdfParse.getText();
expect(pdfText.text).toContain('Hello World');
const pdfData = await pdf(pdfBuffer);
expect(pdfData.text).toContain('Hello World'); // This is the pdf text
});
/**
* We cannot assert the line break is visible in the pdf, but we can assert the
* line break is visible in the editor and that the pdf is generated.
*/
test('it exports the doc with divider', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'export-divider', browserName, 1);
const editor = page.locator('.ProseMirror');
await editor.click();
await editor.fill('Hello World');
// Trigger slash menu to show menu
await editor.locator('.bn-block-outer').last().fill('/');
await page.getByText('Add a horizontal line').click();
await expect(
editor.locator('.bn-block-content[data-content-type="divider"]'),
).toBeVisible();
await page
.getByRole('button', {
name: 'Export the document',
})
.click();
await expect(
page.getByTestId('doc-open-modal-download-button'),
).toBeVisible();
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
});
void page.getByTestId('doc-export-download-button').click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfData = await pdf(pdfBuffer);
expect(pdfData.text).toContain('Hello World');
});
test('it exports the doc with multi columns', async ({
@@ -324,33 +367,46 @@ test.describe('Doc Export', () => {
expect(download.suggestedFilename()).toBe(`${randomDoc}.pdf`);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfParse = new PDFParse({ data: pdfBuffer });
const pdfText = await pdfParse.getText();
expect(pdfText.text).toContain('Column 1');
expect(pdfText.text).toContain('Column 2');
expect(pdfText.text).toContain('Column 3');
const pdfData = await pdf(pdfBuffer);
expect(pdfData.text).toContain('Column 1');
expect(pdfData.text).toContain('Column 2');
expect(pdfData.text).toContain('Column 3');
});
test('it injects the correct language attribute into PDF export', async ({
page,
browserName,
}) => {
const [randomDocFrench] = await createDoc(
page,
'doc-language-export-french',
browserName,
1,
);
await waitForLanguageSwitch(page, TestLanguage.French);
// Wait for the page to be ready after language switch
await page.waitForLoadState('domcontentloaded');
await writeInEditor({
page,
text: 'Contenu de test pour export en français',
});
const header = page.locator('header').first();
await header.locator('h1').getByText('Docs').click();
const randomDocFrench = randomName(
'doc-language-export-french',
browserName,
1,
)[0];
await page
.getByRole('button', {
name: 'Nouveau doc',
})
.click();
const input = page.getByRole('textbox', { name: 'Titre du document' });
await expect(input).toBeVisible();
await expect(input).toHaveText('', { timeout: 10000 });
await input.click();
await input.fill(randomDocFrench);
await input.blur();
const editor = page.locator('.ProseMirror.bn-editor');
await editor.click();
await editor.fill('Contenu de test pour export en français');
await page
.getByRole('button', {
@@ -416,7 +472,7 @@ test.describe('Doc Export', () => {
// Search the interlinking link in the editor (not in the document tree)
const editor = page.locator('.ProseMirror.bn-editor');
const interlink = editor.getByRole('button', {
const interlink = editor.getByRole('link', {
name: randomDoc,
});
@@ -438,8 +494,8 @@ test.describe('Doc Export', () => {
expect(download.suggestedFilename()).toBe(`${docChild}.pdf`);
const pdfBuffer = await cs.toBuffer(await download.createReadStream());
const pdfParse = new PDFParse({ data: pdfBuffer });
const pdfText = await pdfParse.getText();
expect(pdfText.text).toContain(randomDoc);
const pdfData = await pdf(pdfBuffer);
expect(pdfData.text).toContain(randomDoc);
});
});
@@ -1,7 +1,6 @@
import { expect, test } from '@playwright/test';
import { createDoc, getGridRow, verifyDocName } from './utils-common';
import { addNewMember, connectOtherUserToDoc } from './utils-share';
import { createDoc, getGridRow } from './utils-common';
type SmallDoc = {
id: string;
@@ -12,7 +11,7 @@ test.describe('Documents Grid mobile', () => {
test.use({ viewport: { width: 500, height: 1200 } });
test('it checks the grid when mobile', async ({ page }) => {
await page.route(/.*\/documents\/.*/, async (route) => {
await page.route('**/documents/**', async (route) => {
const request = route.request();
if (request.method().includes('GET') && request.url().includes('page=')) {
await route.fulfill({
@@ -134,15 +133,13 @@ test.describe('Document grid item options', () => {
test('it deletes the document', async ({ page, browserName }) => {
const [docTitle] = await createDoc(page, `delete doc`, browserName);
await verifyDocName(page, docTitle);
await page.goto('/');
await expect(page.getByText(docTitle)).toBeVisible();
const row = await getGridRow(page, docTitle);
await row.getByText(`more_horiz`).click();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('menuitem', { name: 'Remove' }).click();
await expect(
page.getByRole('heading', { name: 'Delete a doc' }),
@@ -164,7 +161,7 @@ test.describe('Document grid item options', () => {
test("it checks if the delete option is disabled if we don't have the destroy capability", async ({
page,
}) => {
await page.route(/.*\/api\/v1.0\/documents\/\?page=1/, async (route) => {
await page.route('*/**/api/v1.0/documents/?page=1', async (route) => {
await route.fulfill({
json: {
results: [
@@ -195,68 +192,90 @@ test.describe('Document grid item options', () => {
});
await page.goto('/');
const button = page
.getByTestId(`docs-grid-actions-button-mocked-document-id`)
.first();
const button = page.getByTestId(
`docs-grid-actions-button-mocked-document-id`,
);
await expect(button).toBeVisible();
await button.click();
const removeButton = page
.getByTestId(`docs-grid-actions-remove-mocked-document-id`)
.first();
const removeButton = page.getByTestId(
`docs-grid-actions-remove-mocked-document-id`,
);
await expect(removeButton).toBeVisible();
await removeButton.isDisabled();
});
});
test.describe('Documents filters', () => {
test('it checks the left panel filters', async ({ page, browserName }) => {
test('it checks the prebuild left panel filters', async ({ page }) => {
void page.goto('/');
// Create my doc
const [docName] = await createDoc(page, 'my-doc', browserName, 1);
await verifyDocName(page, docName);
// Another user create a doc and share it with me
const { cleanup, otherPage, otherBrowserName } =
await connectOtherUserToDoc({
browserName,
docUrl: '/',
});
const [docShareName] = await createDoc(
otherPage,
'my-share-doc',
otherBrowserName,
1,
);
await verifyDocName(otherPage, docShareName);
await otherPage.getByRole('button', { name: 'Share' }).click();
await addNewMember(otherPage, 0, 'Editor', browserName);
// Let's check the filters
await page.getByRole('button', { name: 'Back to homepage' }).click();
const row = await getGridRow(page, docName);
const rowShare = await getGridRow(page, docShareName);
// All Docs
await expect(row).toBeVisible();
await expect(rowShare).toBeVisible();
const response = await page.waitForResponse(
(response) =>
response.url().endsWith('documents/?page=1') &&
response.status() === 200,
);
const result = await response.json();
const allCount = result.count as number;
await expect(page.getByTestId('grid-loader')).toBeHidden();
// My Docs
await page.getByRole('link', { name: 'My docs' }).click();
await expect(row).toBeVisible();
await expect(rowShare).toBeHidden();
const allDocs = page.getByLabel('All docs');
const myDocs = page.getByLabel('My docs');
const sharedWithMe = page.getByLabel('Shared with me');
// Initial state
await expect(allDocs).toBeVisible();
await expect(allDocs).toHaveAttribute('aria-current', 'page');
await expect(myDocs).toBeVisible();
await expect(myDocs).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
await expect(myDocs).not.toHaveAttribute('aria-current');
await expect(sharedWithMe).toBeVisible();
await expect(sharedWithMe).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)',
);
await expect(sharedWithMe).not.toHaveAttribute('aria-current');
await allDocs.click();
await page.waitForURL('**/?target=all_docs');
let url = new URL(page.url());
let target = url.searchParams.get('target');
expect(target).toBe('all_docs');
// My docs
await myDocs.click();
url = new URL(page.url());
target = url.searchParams.get('target');
expect(target).toBe('my_docs');
const responseMyDocs = await page.waitForResponse(
(response) =>
response.url().endsWith('documents/?page=1&is_creator_me=true') &&
response.status() === 200,
);
const resultMyDocs = await responseMyDocs.json();
const countMyDocs = resultMyDocs.count as number;
await expect(page.getByTestId('grid-loader')).toBeHidden();
expect(countMyDocs).toBeLessThanOrEqual(allCount);
// Shared with me
await page.getByRole('link', { name: 'Shared with me' }).click();
await expect(row).toBeHidden();
await expect(rowShare).toBeVisible();
await cleanup();
await sharedWithMe.click();
url = new URL(page.url());
target = url.searchParams.get('target');
expect(target).toBe('shared_with_me');
const responseSharedWithMe = await page.waitForResponse(
(response) =>
response.url().includes('documents/?page=1&is_creator_me=false') &&
response.status() === 200,
);
const resultSharedWithMe = await responseSharedWithMe.json();
const countSharedWithMe = resultSharedWithMe.count as number;
await expect(page.getByTestId('grid-loader')).toBeHidden();
expect(countSharedWithMe).toBeLessThanOrEqual(allCount);
expect(countSharedWithMe + countMyDocs).toEqual(allCount);
});
});
@@ -8,7 +8,7 @@ import {
verifyDocName,
} from './utils-common';
import { mockedAccesses, mockedInvitations } from './utils-share';
import { createRootSubPage, getTreeRow } from './utils-sub-pages';
import { createRootSubPage } from './utils-sub-pages';
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -65,61 +65,25 @@ test.describe('Doc Header', () => {
page,
browserName,
}) => {
await createDoc(page, 'doc-update-emoji', browserName, 1);
const emojiPicker = page.locator('.--docs--doc-title').getByRole('button');
const optionMenu = page.getByLabel('Open the document options');
const addEmojiMenuItem = page.getByRole('menuitem', { name: 'Add emoji' });
const removeEmojiMenuItem = page.getByRole('menuitem', {
name: 'Remove emoji',
});
// Top parent should not have emoji picker
await expect(emojiPicker).toBeHidden();
await optionMenu.click();
await expect(addEmojiMenuItem).toBeHidden();
await expect(removeEmojiMenuItem).toBeHidden();
await page.keyboard.press('Escape');
const { name: docChild } = await createRootSubPage(
page,
browserName,
'doc-update-emoji-child',
);
await verifyDocName(page, docChild);
// Emoji picker should be hidden initially
await expect(emojiPicker).toBeHidden();
// Add emoji
await optionMenu.click();
await expect(removeEmojiMenuItem).toBeHidden();
await addEmojiMenuItem.click();
await expect(emojiPicker).toHaveText('📄');
// Change emoji
await emojiPicker.click({
delay: 100,
});
await page.getByRole('button', { name: '😀' }).first().click();
await expect(emojiPicker).toHaveText('😀');
// Update title
await createDoc(page, 'doc-update', browserName, 1);
const docTitle = page.getByRole('textbox', { name: 'Document title' });
await docTitle.fill('Hello Emoji World');
await expect(docTitle).toBeVisible();
await docTitle.fill('👍 Hello Emoji World');
await docTitle.blur();
await verifyDocName(page, 'Hello Emoji World');
await verifyDocName(page, '👍 Hello Emoji World');
// Check the tree
const row = await getTreeRow(page, 'Hello Emoji World');
await expect(row.getByText('😀')).toBeVisible();
const docTree = page.getByTestId('doc-tree');
await expect(docTree.getByText('Hello Emoji World')).toBeVisible();
await expect(docTree.getByTestId('doc-emoji-icon')).toBeVisible();
await expect(docTree.getByTestId('doc-simple-icon')).toBeHidden();
// Remove emoji
await optionMenu.click();
await expect(addEmojiMenuItem).toBeHidden();
await removeEmojiMenuItem.click();
await expect(emojiPicker).toBeHidden();
await page.getByTestId('home-button').click();
// Check the documents grid
const gridRow = await getGridRow(page, 'Hello Emoji World');
await expect(gridRow.getByTestId('doc-emoji-icon')).toBeVisible();
await expect(gridRow.getByTestId('doc-simple-icon')).toBeHidden();
});
test('it deletes the doc', async ({ page, browserName }) => {
@@ -132,7 +96,7 @@ test.describe('Doc Header', () => {
page.getByRole('heading', { name: 'Delete a doc' }),
).toBeVisible();
await expect(page.getByText(`This document will be`)).toBeVisible();
await expect(page.getByText(`This document and any sub-`)).toBeVisible();
await page
.getByRole('button', {
@@ -7,7 +7,6 @@ import {
randomName,
verifyDocName,
} from './utils-common';
import { writeInEditor } from './utils-editor';
import { connectOtherUserToDoc, updateRoleUser } from './utils-share';
import { createRootSubPage } from './utils-sub-pages';
@@ -203,19 +202,9 @@ test.describe('Document create member', () => {
);
await expect(userInvitation).toBeVisible();
const responsePromisePatchInvitation = page.waitForResponse(
(response) =>
response.url().includes('/invitations/') &&
response.status() === 200 &&
response.request().method() === 'PATCH',
);
await userInvitation.getByLabel('doc-role-dropdown').click();
await page.getByRole('menuitem', { name: 'Reader' }).click();
const responsePatchInvitation = await responsePromisePatchInvitation;
expect(responsePatchInvitation.ok()).toBeTruthy();
const moreActions = userInvitation.getByRole('button', {
name: 'Open invitation actions menu',
});
@@ -241,7 +230,11 @@ test.describe('Document create member', () => {
await verifyDocName(page, docTitle);
await writeInEditor({ page, text: 'Hello World' });
await page
.locator('.ProseMirror')
.locator('.bn-block-outer')
.last()
.fill('Hello World');
const docUrl = page.url();
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { createDoc, verifyDocName } from './utils-common';
import { createDoc, goToGridDoc, verifyDocName } from './utils-common';
import { addNewMember } from './utils-share';
test.beforeEach(async ({ page }) => {
@@ -8,14 +8,8 @@ test.beforeEach(async ({ page }) => {
});
test.describe('Document list members', () => {
test('it checks a big list of members', async ({ page, browserName }) => {
const [docTitle] = await createDoc(
page,
'members-big-members-list',
browserName,
1,
);
test('it checks a big list of members', async ({ page }) => {
const docTitle = await goToGridDoc(page);
await verifyDocName(page, docTitle);
// Get the current URL and extract the last part
@@ -31,7 +25,7 @@ test.describe('Document list members', () => {
return cleanUrl.split('/').pop() || '';
})();
await page.route(/.*\/documents\/.*\/accesses\//, async (route) => {
await page.route('**/documents/**/accesses/', async (route) => {
const request = route.request();
const url = new URL(request.url());
const pageId = url.searchParams.get('page') ?? '1';
@@ -79,7 +73,7 @@ test.describe('Document list members', () => {
await expect(loadMore).toBeHidden();
});
test('it checks a big list of invitations', async ({ page, browserName }) => {
test('it checks a big list of invitations', async ({ page }) => {
await page.route(
/.*\/documents\/.*\/invitations\/\?page=.*/,
async (route) => {
@@ -114,12 +108,7 @@ test.describe('Document list members', () => {
},
);
const [docTitle] = await createDoc(
page,
'members-big-invitation-list',
browserName,
1,
);
const docTitle = await goToGridDoc(page);
await verifyDocName(page, docTitle);
await page.getByRole('button', { name: 'Share' }).click();
@@ -9,7 +9,6 @@ import {
mockedDocument,
verifyDocName,
} from './utils-common';
import { writeInEditor } from './utils-editor';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Doc Routing', () => {
@@ -59,39 +58,37 @@ test.describe('Doc Routing', () => {
await createRootSubPage(page, browserName, '401-doc-child');
await writeInEditor({ page, text: 'Hello World' });
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
const responsePromise = page.route(
/.*\/documents\/.*\/$|users\/me\/$/,
async (route) => {
const request = route.request();
// Wait for the doc link (via its dynamic title) to be visible
const docLink = page.getByRole('link', { name: docTitle });
await expect(docLink).toBeVisible();
// When we quit a document, a PATCH request is sent to save the document.
// We intercept this request to simulate a 401 error from the backend.
// The GET request to users/me is also intercepted to simulate the user
// being logged out when trying to fetch user info.
// This way we can test the 401 error handling when saving the document
if (
(request.url().includes('/documents/') &&
request.method().includes('PATCH')) ||
(request.url().includes('/users/me/') &&
request.method().includes('GET'))
) {
await route.fulfill({
status: 401,
json: {
detail: 'Log in to access the document',
},
});
} else {
await route.continue();
}
},
// Intercept GET/PATCH requests to return 401
await page.route(/.*\/documents\/.*\/$|users\/me\/$/, async (route) => {
const request = route.request();
if (
request.method().includes('PATCH') ||
request.method().includes('GET')
) {
await route.fulfill({
status: 401,
json: { detail: 'Log in to access the document' },
});
} else {
await route.continue();
}
});
// Explicitly wait for a 401 response after clicking
const wait401 = page.waitForResponse(
(resp) =>
resp.status() === 401 &&
/\/(documents\/[^/]+\/|users\/me\/)$/.test(resp.url()),
);
await page.getByRole('link', { name: '401-doc-parent' }).click();
await responsePromise;
await docLink.click();
await wait401;
await expect(page.getByText('Log in to access the document.')).toBeVisible({
timeout: 10000,
@@ -1,155 +0,0 @@
import { expect, test } from '@playwright/test';
import {
clickInEditorMenu,
clickInGridMenu,
createDoc,
getGridRow,
verifyDocName,
} from './utils-common';
import { addNewMember } from './utils-share';
import {
addChild,
createRootSubPage,
navigateToPageFromTree,
} from './utils-sub-pages';
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test.describe('Doc Trashbin', () => {
test('it controls UI and interaction from the grid page', async ({
page,
browserName,
}) => {
const [title1] = await createDoc(page, 'my-trash-doc-1', browserName, 1);
const [title2] = await createDoc(page, 'my-trash-doc-2', browserName, 1);
await verifyDocName(page, title2);
await page.getByRole('button', { name: 'Share' }).click();
await addNewMember(page, 0, 'Editor');
await page.getByRole('button', { name: 'close' }).click();
await page.getByRole('button', { name: 'Back to homepage' }).click();
const row1 = await getGridRow(page, title1);
await clickInGridMenu(page, row1, 'Delete');
await page.getByRole('button', { name: 'Delete document' }).click();
await expect(row1.getByText(title1)).toBeHidden();
const row2 = await getGridRow(page, title2);
await clickInGridMenu(page, row2, 'Delete');
await page.getByRole('button', { name: 'Delete document' }).click();
await expect(row2.getByText(title2)).toBeHidden();
await page.getByRole('link', { name: 'Trashbin' }).click();
const docsGrid = page.getByTestId('docs-grid');
await expect(docsGrid.getByText('Days remaining')).toBeVisible();
await expect(row1.getByText(title1)).toBeVisible();
await expect(row1.getByText('30 days')).toBeVisible();
await expect(row2.getByText(title2)).toBeVisible();
await expect(
row2.getByRole('button', {
name: 'Open the sharing settings for the document',
}),
).toBeVisible();
await expect(
row2.getByRole('button', {
name: 'Open the sharing settings for the document',
}),
).toBeDisabled();
await clickInGridMenu(page, row2, 'Restore');
await expect(row2.getByText(title2)).toBeHidden();
await page.getByRole('link', { name: 'All docs' }).click();
const row2Restored = await getGridRow(page, title2);
await expect(row2Restored.getByText(title2)).toBeVisible();
await row2Restored.getByRole('link', { name: /Open document/ }).click();
await verifyDocName(page, title2);
await page.getByRole('button', { name: 'Back to homepage' }).click();
await expect(row2.getByText(title2)).toBeVisible();
await expect(
row2.getByRole('button', {
name: 'Open the sharing settings for the document',
}),
).toBeEnabled();
await page.getByRole('link', { name: 'Trashbin' }).click();
await expect(row2.getByText(title2)).toBeHidden();
});
test('it controls UI and interaction from the doc page', async ({
page,
browserName,
}) => {
const [topParent] = await createDoc(
page,
'my-trash-editor-doc',
browserName,
1,
);
await verifyDocName(page, topParent);
const { name: subDocName } = await createRootSubPage(
page,
browserName,
'my-trash-editor-subdoc',
);
const subsubDocName = await addChild({
page,
browserName,
docParent: subDocName,
docName: 'my-trash-editor-subsubdoc',
});
await verifyDocName(page, subsubDocName);
await navigateToPageFromTree({ page, title: subDocName });
await verifyDocName(page, subDocName);
await clickInEditorMenu(page, 'Delete sub-document');
await page.getByRole('button', { name: 'Delete document' }).click();
await verifyDocName(page, topParent);
await page.getByRole('button', { name: 'Back to homepage' }).click();
await page.getByRole('link', { name: 'Trashbin' }).click();
const row = await getGridRow(page, subDocName);
await row.getByText(subDocName).click();
await verifyDocName(page, subDocName);
await expect(
page.locator('.--docs--editor-container.--docs--doc-deleted'),
).toBeVisible();
await expect(page.getByLabel('Alert deleted document')).toBeVisible();
await expect(page.getByRole('button', { name: 'Share' })).toBeDisabled();
await expect(page.locator('.bn-editor')).toHaveAttribute(
'contenteditable',
'false',
);
const docTree = page.getByTestId('doc-tree');
await expect(docTree.getByText(topParent)).toBeHidden();
await expect(
docTree.getByText(subDocName, {
exact: true,
}),
).toBeVisible();
await expect(docTree.getByText(subsubDocName)).toBeVisible();
await expect(
docTree
.locator(".--docs-sub-page-item[aria-disabled='true']")
.getByText(subsubDocName),
).toBeVisible();
await page.getByRole('button', { name: 'Restore' }).click();
await expect(page.getByLabel('Alert deleted document')).toBeHidden();
await expect(page.locator('.bn-editor')).toHaveAttribute(
'contenteditable',
'true',
);
await expect(page.getByRole('button', { name: 'Share' })).toBeEnabled();
await expect(docTree.getByText(topParent)).toBeVisible();
});
});
@@ -9,11 +9,7 @@ import {
verifyDocName,
} from './utils-common';
import { addNewMember } from './utils-share';
import {
clickOnAddRootSubPage,
createRootSubPage,
getTreeRow,
} from './utils-sub-pages';
import { clickOnAddRootSubPage, createRootSubPage } from './utils-sub-pages';
test.describe('Doc Tree', () => {
test.beforeEach(async ({ page }) => {
@@ -302,58 +298,6 @@ test.describe('Doc Tree', () => {
// Now test keyboard navigation on sub-document
await expect(docTree.getByText(docChild)).toBeVisible();
});
test('it updates the child icon from the tree', async ({
page,
browserName,
}) => {
const [docParent] = await createDoc(
page,
'doc-child-emoji',
browserName,
1,
);
await verifyDocName(page, docParent);
const { name: docChild } = await createRootSubPage(
page,
browserName,
'doc-child-emoji-child',
);
const row = await getTreeRow(page, docChild);
// Check Remove emoji is not present initially
await row.hover();
const menu = row.getByText(`more_horiz`);
await menu.click();
await expect(
page.getByRole('menuitem', { name: 'Remove emoji' }),
).toBeHidden();
// Close the menu
await page.keyboard.press('Escape');
// Update the emoji from the tree
await row.locator('.--docs--doc-icon').click();
await page.getByRole('button', { name: '😀' }).first().click();
// Verify the emoji is updated in the tree and in the document title
await expect(row.getByText('😀')).toBeVisible();
const titleEmojiPicker = page
.locator('.--docs--doc-title')
.getByRole('button');
await expect(titleEmojiPicker).toHaveText('😀');
// Now remove the emoji using the new action
await row.hover();
await menu.click();
await page.getByRole('menuitem', { name: 'Remove emoji' }).click();
await expect(row.getByText('😀')).toBeHidden();
await expect(titleEmojiPicker).toBeHidden();
});
});
test.describe('Doc Tree: Inheritance', () => {
@@ -7,7 +7,6 @@ import {
keyCloakSignIn,
verifyDocName,
} from './utils-common';
import { getEditor, writeInEditor } from './utils-editor';
import { addNewMember, connectOtherUserToDoc } from './utils-share';
import { createRootSubPage } from './utils-sub-pages';
@@ -152,15 +151,18 @@ test.describe('Doc Visibility: Restricted', () => {
await verifyDocName(page, docTitle);
await writeInEditor({ page, text: 'Hello World' });
await page
.locator('.ProseMirror')
.locator('.bn-block-outer')
.last()
.fill('Hello World');
const docUrl = page.url();
const { otherBrowserName, otherPage, cleanup } =
await connectOtherUserToDoc({
browserName,
docUrl,
});
const { otherBrowserName, otherPage } = await connectOtherUserToDoc({
browserName,
docUrl,
});
await expect(
otherPage.getByText('Insufficient access rights to view the document.'),
@@ -173,23 +175,20 @@ test.describe('Doc Visibility: Restricted', () => {
await addNewMember(page, 0, 'Reader', otherBrowserName);
await otherPage.reload();
await expect(otherPage.getByText('Hello World')).toBeVisible({
timeout: 10000,
});
await cleanup();
await expect(otherPage.getByText('Hello World')).toBeVisible();
});
});
test.describe('Doc Visibility: Public', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test.use({ storageState: { cookies: [], origins: [] } });
test('It checks a public doc in read only mode', async ({
page,
browserName,
}) => {
await page.goto('/');
await keyCloakSignIn(page, browserName);
const [docTitle] = await createDoc(
page,
'Public read only',
@@ -199,8 +198,6 @@ test.describe('Doc Visibility: Public', () => {
await verifyDocName(page, docTitle);
await writeInEditor({ page, text: 'Hello Public Viewonly' });
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click();
@@ -242,67 +239,49 @@ test.describe('Doc Visibility: Public', () => {
await expect(page.getByTestId('search-docs-button')).toBeVisible();
await expect(page.getByTestId('new-doc-button')).toBeVisible();
const docUrl = page.url();
const urlDoc = page.url();
const { otherPage, cleanup } = await connectOtherUserToDoc({
browserName,
docUrl,
withoutSignIn: true,
});
await page
.getByRole('button', {
name: 'Logout',
})
.click();
await expect(otherPage.locator('h2').getByText(docTitle)).toBeVisible();
await expect(otherPage.getByTestId('search-docs-button')).toBeHidden();
await expect(otherPage.getByTestId('new-doc-button')).toBeHidden();
await expect(
otherPage.getByRole('button', { name: 'Share' }),
).toBeVisible();
const card = otherPage.getByLabel('It is the card information');
await expectLoginPage(page);
await page.goto(urlDoc);
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
await expect(page.getByTestId('search-docs-button')).toBeHidden();
await expect(page.getByTestId('new-doc-button')).toBeHidden();
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
const card = page.getByLabel('It is the card information');
await expect(card).toBeVisible();
await expect(card.getByText('Reader')).toBeVisible();
await page.getByRole('button', { name: 'Share' }).click();
await expect(
otherPage.locator('.--docs--editor-container.--docs--doc-readonly'),
).toBeVisible();
const otherEditor = await getEditor({ page: otherPage });
await expect(otherEditor).toHaveAttribute('contenteditable', 'false');
await expect(otherEditor.getByText('Hello Public Viewonly')).toBeVisible();
// Cursor and selection of the anonymous user are not visible
await otherEditor.getByText('Hello Public').selectText();
await expect(
page.locator('.collaboration-cursor-custom__base'),
).toBeHidden();
await expect(page.locator('.ProseMirror-yjs-selection')).toBeHidden();
// Can still see changes made by others
await writeInEditor({ page, text: 'Can you see it ?' });
await expect(otherEditor.getByText('Can you see it ?')).toBeVisible();
await otherPage.getByRole('button', { name: 'Share' }).click();
await expect(
otherPage.getByText(
page.getByText(
'You can view this document but need additional access to see its members or modify settings.',
),
).toBeVisible();
await expect(
otherPage.getByRole('button', { name: 'Request access' }),
page.getByRole('button', { name: 'Request access' }),
).toBeHidden();
await cleanup();
});
test('It checks a public doc in editable mode', async ({
page,
browserName,
}) => {
await page.goto('/');
await keyCloakSignIn(page, browserName);
const [docTitle] = await createDoc(page, 'Public editable', browserName, 1);
await verifyDocName(page, docTitle);
await writeInEditor({ page, text: 'Hello Public Editable' });
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click();
@@ -336,47 +315,20 @@ test.describe('Doc Visibility: Public', () => {
cardContainer.getByText('Public document', { exact: true }),
).toBeVisible();
const docUrl = page.url();
const urlDoc = page.url();
const { otherPage, cleanup } = await connectOtherUserToDoc({
browserName,
docUrl,
withoutSignIn: true,
docTitle,
});
await page
.getByRole('button', {
name: 'Logout',
})
.click();
await expect(otherPage.getByTestId('search-docs-button')).toBeHidden();
await expect(otherPage.getByTestId('new-doc-button')).toBeHidden();
await expectLoginPage(page);
const otherEditor = await getEditor({ page: otherPage });
await expect(otherEditor).toHaveAttribute('contenteditable', 'true');
await expect(otherEditor.getByText('Hello Public Editable')).toBeVisible();
await page.goto(urlDoc);
// We can see the collaboration cursor of the anonymous user
await otherEditor.getByText('Hello Public').selectText();
await expect(
page.locator('.collaboration-cursor-custom__base').getByText('Anonymous'),
).toBeVisible();
await expect(
otherPage.getByRole('button', { name: 'Share' }),
).toBeVisible();
const card = otherPage.getByLabel('It is the card information');
await expect(card).toBeVisible();
await expect(card.getByText('Editor')).toBeVisible();
await otherPage.getByRole('button', { name: 'Share' }).click();
await expect(
otherPage.getByText(
'You can view this document but need additional access to see its members or modify settings.',
),
).toBeVisible();
await expect(
otherPage.getByRole('button', { name: 'Request access' }),
).toBeHidden();
await cleanup();
await verifyDocName(page, docTitle);
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
});
});
@@ -32,15 +32,6 @@ test.describe('Header', () => {
}) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
theme_customization: {
header: {
logo: {
src: '/assets/logo-gouv.svg',
width: '220px',
alt: 'Gouvernement Logo',
},
},
},
});
await page.goto('/');
@@ -107,15 +98,6 @@ test.describe('Header mobile', () => {
test('it checks the header when mobile with DSFR theme', async ({ page }) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
theme_customization: {
header: {
logo: {
src: '/assets/logo-gouv.svg',
width: '220px',
alt: 'Gouvernement Logo',
},
},
},
});
await page.goto('/');
@@ -149,30 +131,3 @@ test.describe('Header: Log out', () => {
await expectLoginPage(page);
});
});
test.describe('Header: Override configuration', () => {
test('checks the header is correctly overrided', async ({ page }) => {
await overrideConfig(page, {
FRONTEND_THEME: 'dsfr',
theme_customization: {
header: {
logo: {
src: '/assets/logo-gouv.svg',
width: '220px',
alt: '',
},
},
},
});
await page.goto('/');
const header = page.locator('header').first();
const logoImage = header.getByTestId('header-icon-docs');
await expect(logoImage).toBeVisible();
await expect(logoImage).not.toHaveAttribute('src', '/assets/icon-docs.svg');
await expect(logoImage).toHaveAttribute('src', '/assets/logo-gouv.svg');
await expect(logoImage).toHaveAttribute('alt', '');
});
});
@@ -1,7 +1,6 @@
import { expect, test } from '@playwright/test';
import { TestLanguage, createDoc, waitForLanguageSwitch } from './utils-common';
import { openSuggestionMenu } from './utils-editor';
test.describe('Language', () => {
test.beforeEach(async ({ page }) => {
@@ -52,7 +51,6 @@ test.describe('Language', () => {
await expect(page.locator('html')).toHaveAttribute('lang', 'en');
await expect(languagePicker).toContainText('English');
});
test('can switch language using only keyboard', async ({ page }) => {
await page.goto('/');
await waitForLanguageSwitch(page, TestLanguage.English);
@@ -108,18 +106,18 @@ test.describe('Language', () => {
}) => {
await createDoc(page, 'doc-toolbar', browserName, 1);
const editor = await openSuggestionMenu({ page });
const editor = page.locator('.ProseMirror');
// Trigger slash menu to show english menu
await editor.click();
await editor.fill('/');
await expect(page.getByText('Headings', { exact: true })).toBeVisible();
await editor.click(); // close the menu
await expect(page.getByText('Headings', { exact: true })).toBeHidden();
// Change language to French
await waitForLanguageSwitch(page, TestLanguage.French);
// Trigger slash menu to show french menu
await openSuggestionMenu({ page });
await editor.locator('.bn-block-outer').last().fill('/');
await expect(page.getByText('Titres', { exact: true })).toBeVisible();
});
});
@@ -1,7 +1,5 @@
import { expect, test } from '@playwright/test';
import { createDoc } from './utils-common';
test.describe('Left panel desktop', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
@@ -13,53 +11,6 @@ test.describe('Left panel desktop', () => {
await expect(page.getByTestId('home-button')).toBeVisible();
await expect(page.getByTestId('new-doc-button')).toBeVisible();
});
test('checks resize handle is present and functional on document page', async ({
page,
browserName,
}) => {
// On home page, resize handle should NOT be present
let resizeHandle = page.locator('[data-panel-resize-handle-id]');
await expect(resizeHandle).toBeHidden();
// Create and navigate to a document
await createDoc(page, 'doc-resize-test', browserName, 1);
// Now resize handle should be visible on document page
resizeHandle = page.locator('[data-panel-resize-handle-id]').first();
await expect(resizeHandle).toBeVisible();
const leftPanel = page.getByTestId('left-panel-desktop');
await expect(leftPanel).toBeVisible();
// Get initial panel width
const initialBox = await leftPanel.boundingBox();
expect(initialBox).not.toBeNull();
// Get handle position
const handleBox = await resizeHandle.boundingBox();
expect(handleBox).not.toBeNull();
// Test resize by dragging the handle
await page.mouse.move(
handleBox!.x + handleBox!.width / 2,
handleBox!.y + handleBox!.height / 2,
);
await page.mouse.down();
await page.mouse.move(
handleBox!.x + 100,
handleBox!.y + handleBox!.height / 2,
);
await page.mouse.up();
// Wait for resize to complete
await page.waitForTimeout(200);
// Verify the panel has been resized
const newBox = await leftPanel.boundingBox();
expect(newBox).not.toBeNull();
expect(newBox!.width).toBeGreaterThan(initialBox!.width);
});
});
test.describe('Left panel mobile', () => {
@@ -96,12 +47,4 @@ test.describe('Left panel mobile', () => {
await expect(languageButton).toBeInViewport();
await expect(logoutButton).toBeInViewport();
});
test('checks resize handle is not present on mobile', async ({ page }) => {
await page.goto('/');
// Verify the resize handle is NOT present on mobile
const resizeHandle = page.locator('[data-panel-resize-handle-id]');
await expect(resizeHandle).toBeHidden();
});
});
@@ -1,4 +1,4 @@
import { Locator, Page, expect } from '@playwright/test';
import { Page, expect } from '@playwright/test';
export type BrowserName = 'chromium' | 'firefox' | 'webkit';
export const BROWSERS: BrowserName[] = ['chromium', 'webkit', 'firefox'];
@@ -23,7 +23,6 @@ export const CONFIG = {
LANGUAGE_CODE: 'en-us',
POSTHOG_KEY: {},
SENTRY_DSN: null,
TRASHBIN_CUTOFF_DAYS: 30,
theme_customization: {},
} as const;
@@ -31,7 +30,7 @@ export const overrideConfig = async (
page: Page,
newConfig: { [_K in keyof typeof CONFIG]?: unknown },
) =>
await page.route(/.*\/api\/v1.0\/config\/.*/, async (route) => {
await page.route('**/api/v1.0/config/', async (route) => {
const request = route.request();
if (request.method().includes('GET')) {
await route.fulfill({
@@ -48,7 +47,7 @@ export const overrideConfig = async (
export const keyCloakSignIn = async (
page: Page,
browserName: string,
fromHome = true,
fromHome: boolean = true,
) => {
if (fromHome) {
await page.getByRole('button', { name: 'Start Writing' }).first().click();
@@ -79,8 +78,8 @@ export const createDoc = async (
page: Page,
docName: string,
browserName: string,
length = 1,
isMobile = false,
length: number = 1,
isMobile: boolean = false,
) => {
const randomDocs = randomName(docName, browserName, length);
@@ -204,7 +203,7 @@ export const waitForResponseCreateDoc = (page: Page) => {
};
export const mockedDocument = async (page: Page, data: object) => {
await page.route(/\**\/documents\/\**/, async (route) => {
await page.route('**/documents/**/', async (route) => {
const request = route.request();
if (
request.method().includes('GET') &&
@@ -256,7 +255,7 @@ export const mockedDocument = async (page: Page, data: object) => {
};
export const mockedListDocs = async (page: Page, data: object[] = []) => {
await page.route(/\**\/documents\/\**/, async (route) => {
await page.route('**/documents/**/', async (route) => {
const request = route.request();
if (request.method().includes('GET') && request.url().includes('page=')) {
await route.fulfill({
@@ -277,7 +276,6 @@ export const expectLoginPage = async (page: Page) =>
).toBeVisible({
timeout: 10000,
});
// language helper
export const TestLanguage = {
English: {
@@ -301,7 +299,7 @@ export async function waitForLanguageSwitch(
page: Page,
lang: TestLanguageValue,
) {
await page.route(/\**\/api\/v1.0\/users\/\**/, async (route, request) => {
await page.route('**/api/v1.0/users/**', async (route, request) => {
if (request.method().includes('PATCH')) {
await route.fulfill({
json: {
@@ -327,19 +325,3 @@ export async function waitForLanguageSwitch(
await page.getByRole('menuitem', { name: lang.label }).click();
}
export const clickInEditorMenu = async (page: Page, textButton: string) => {
await page.getByRole('button', { name: 'Open the document options' }).click();
await page.getByRole('menuitem', { name: textButton }).click();
};
export const clickInGridMenu = async (
page: Page,
row: Locator,
textButton: string,
) => {
await row
.getByRole('button', { name: /Open the menu of actions for the document/ })
.click();
await page.getByRole('menuitem', { name: textButton }).click();
};
@@ -9,7 +9,7 @@ export const getEditor = async ({ page }: { page: Page }) => {
export const openSuggestionMenu = async ({ page }: { page: Page }) => {
const editor = await getEditor({ page });
await editor.click();
await writeInEditor({ page, text: '/' });
await page.locator('.bn-block-outer').last().fill('/');
return editor;
};
@@ -22,6 +22,6 @@ export const writeInEditor = async ({
text: string;
}) => {
const editor = await getEditor({ page });
await editor.locator('.bn-block-outer .bn-inline-content').last().fill(text);
editor.locator('.bn-block-outer').last().fill(text);
return editor;
};
@@ -15,7 +15,7 @@ export const addNewMember = async (
page: Page,
index: number,
role: 'Administrator' | 'Owner' | 'Editor' | 'Reader',
fillText = 'user.test',
fillText: string = 'user.test',
) => {
const responsePromiseSearchUser = page.waitForResponse(
(response) =>
@@ -118,16 +118,12 @@ export const connectOtherUserToDoc = async ({
await otherPage.goto(docUrl);
if (!withoutSignIn) {
const loginFromApp = otherPage
await otherPage
.getByRole('main', { name: 'Main content' })
.getByLabel('Login');
const loginFromHome = otherPage.getByRole('button', {
name: 'Start Writing',
});
await loginFromApp.or(loginFromHome).first().click({
timeout: 15000,
});
.getByLabel('Login')
.click({
timeout: 15000,
});
await keyCloakSignIn(otherPage, otherBrowserName, false);
}
@@ -163,7 +159,7 @@ export const mockedInvitations = async (page: Page, json?: object) => {
...json,
},
];
await page.route(/.*\/invitations\/.*/, async (route) => {
await page.route('**/invitations/**/', async (route) => {
const request = route.request();
if (
request.method().includes('GET') &&
@@ -184,7 +180,7 @@ export const mockedInvitations = async (page: Page, json?: object) => {
});
await page.route(
/.*\/invitations\/120ec765-43af-4602-83eb-7f4e1224548a\/.*/,
'**/invitations/120ec765-43af-4602-83eb-7f4e1224548a/**/',
async (route) => {
const request = route.request();
if (request.method().includes('DELETE')) {
@@ -199,7 +195,7 @@ export const mockedInvitations = async (page: Page, json?: object) => {
};
export const mockedAccesses = async (page: Page, json?: object) => {
await page.route(/.*\/accesses\/.*/, async (route) => {
await page.route('**/accesses/**/', async (route) => {
const request = route.request();
if (
@@ -1,7 +1,6 @@
import { Page, expect } from '@playwright/test';
import {
BrowserName,
randomName,
updateDocTitle,
verifyDocName,
@@ -10,9 +9,9 @@ import {
export const createRootSubPage = async (
page: Page,
browserName: BrowserName,
browserName: string,
docName: string,
isMobile = false,
isMobile: boolean = false,
) => {
if (isMobile) {
await page
@@ -68,63 +67,6 @@ export const clickOnAddRootSubPage = async (page: Page) => {
await rootItem.getByTestId('doc-tree-item-actions-add-child').click();
};
export const addChild = async ({
page,
browserName,
docParent,
docName,
}: {
page: Page;
browserName: BrowserName;
docParent: string;
docName: string;
}) => {
let item = page.getByTestId('doc-tree-root-item');
const isParent = await item
.filter({
hasText: docParent,
})
.first()
.count();
if (!isParent) {
const items = page.getByRole('treeitem');
item = items
.filter({
hasText: docParent,
})
.first();
}
await item.hover();
await item.getByTestId('doc-tree-item-actions-add-child').click();
const [name] = randomName(docName, browserName, 1);
await updateDocTitle(page, name);
return name;
};
export const getTreeRow = async (page: Page, title: string) => {
const docTree = page.getByTestId('doc-tree');
const row = docTree
.getByRole('treeitem')
.filter({
hasText: title,
})
.first();
await expect(row).toBeVisible();
return row;
};
export const navigateToTopParentFromTree = async ({ page }: { page: Page }) => {
await page.getByRole('link', { name: /Open root document/ }).click();
};
export const navigateToPageFromTree = async ({
page,
title,
@@ -133,10 +75,6 @@ export const navigateToPageFromTree = async ({
title: string;
}) => {
const docTree = page.getByTestId('doc-tree');
await docTree
.getByText(title, {
exact: true,
})
.click();
await docTree.getByText(title).click();
await verifyDocName(page, title);
};
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "3.9.0",
"version": "3.7.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -15,7 +15,7 @@
"test:ui::chromium": "yarn test:ui --project=chromium"
},
"devDependencies": {
"@playwright/test": "1.56.1",
"@playwright/test": "1.55.0",
"@types/node": "*",
"@types/pdf-parse": "1.1.5",
"eslint-plugin-docs": "*",
@@ -23,7 +23,7 @@
},
"dependencies": {
"convert-stream": "1.0.2",
"pdf-parse": "2.4.5"
"pdf-parse": "1.1.1"
},
"packageManager": "yarn@1.22.22"
}
@@ -0,0 +1,2 @@
@mf-types/
node_modules/
@@ -0,0 +1,14 @@
# Impress Plugin
Minimal Module Federation Plugin for Impress
## Development
```bash
yarn install
yarn dev
```
Server runs on http://localhost:3002
Remote entry point: http://localhost:3002/remoteEntry.js
@@ -0,0 +1,37 @@
{
"name": "app-impress-plugin",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "webpack serve --mode development",
"build": "webpack --mode production"
},
"devDependencies": {
"@module-federation/native-federation-typescript": "0.6.2",
"@openfun/cunningham-react": "3.2.3",
"@types/react": "19.1.1",
"@types/react-dom": "19.1.1",
"css-loader": "^7.1.2",
"html-webpack-plugin": "^5.6.3",
"style-loader": "^4.0.0",
"ts-loader": "^9.5.1",
"typescript": "*",
"webpack": "^5.101.3",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.2.0"
},
"dependencies": {
"react": "19.1.1",
"react-dom": "19.1.1",
"styled-components": "6.1.19",
"@openfun/cunningham-react": "3.2.3",
"react-i18next": "15.7.3",
"react-aria-components": "1.12.1",
"@gouvfr-lasuite/ui-kit": "0.16.1",
"yjs": "13.6.27",
"clsx": "2.1.1",
"cmdk": "1.1.1",
"react-intersection-observer": "9.16.0",
"@tanstack/react-query": "5.87.4"
}
}
@@ -0,0 +1,489 @@
import React, { useState, useRef, useEffect } from 'react';
import { Button, Popover } from 'react-aria-components';
import styled from 'styled-components';
import { useTranslation } from 'react-i18next';
import { useAuthQuery } from 'impress/features/auth/api';
import { Icon, Loading, Box, Text } from 'impress/components';
// Styled Components showcasing styled-components integration
const StyledButton = styled(Button)`
cursor: pointer;
border: 1px solid #0066cc;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 8px 16px;
border-radius: 8px;
font-family: Marianne, Arial, serif;
font-weight: 500;
font-size: 0.875rem;
transition: all 0.2s ease-in-out;
display: flex;
align-items: center;
gap: 8px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
&:active {
transform: translateY(0);
}
&:focus-visible {
outline: 2px solid #667eea;
outline-offset: 2px;
}
`;
const StyledPopover = styled(Popover)`
background-color: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
border: 1px solid #e0e0e0;
min-width: 400px;
max-width: 500px;
max-height: 600px;
overflow: hidden;
display: flex;
flex-direction: column;
`;
const TabContainer = styled.div`
display: flex;
border-bottom: 2px solid #f0f0f0;
background-color: #fafafa;
`;
const Tab = styled.button<{ $active: boolean }>`
flex: 1;
padding: 12px 16px;
border: none;
background: ${props => props.$active ? 'white' : 'transparent'};
color: ${props => props.$active ? '#667eea' : '#666'};
font-weight: ${props => props.$active ? '600' : '400'};
font-size: 0.875rem;
cursor: pointer;
border-bottom: 2px solid ${props => props.$active ? '#667eea' : 'transparent'};
margin-bottom: -2px;
transition: all 0.2s ease-in-out;
&:hover {
background-color: ${props => props.$active ? 'white' : '#f5f5f5'};
color: ${props => props.$active ? '#667eea' : '#333'};
}
`;
const TabContent = styled.div`
padding: 20px;
overflow-y: auto;
max-height: 500px;
`;
const InfoCard = styled.div`
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
border-left: 4px solid #667eea;
`;
const InfoRow = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
&:last-child {
border-bottom: none;
}
`;
const Label = styled.span`
font-weight: 600;
color: #333;
font-size: 0.875rem;
`;
const Value = styled.span`
color: #666;
font-size: 0.875rem;
font-family: 'Courier New', monospace;
background-color: rgba(0, 0, 0, 0.05);
padding: 2px 8px;
border-radius: 4px;
`;
const FeatureList = styled.ul`
list-style: none;
padding: 0;
margin: 0;
`;
const FeatureItem = styled.li`
display: flex;
align-items: center;
gap: 12px;
padding: 10px;
margin-bottom: 8px;
background-color: #f9f9f9;
border-radius: 6px;
transition: background-color 0.2s ease-in-out;
&:hover {
background-color: #f0f0f0;
}
`;
interface ComponentProps {
customMessage?: string;
showDebugInfo?: boolean;
}
const MyCustomComponent: React.FC<ComponentProps> = ({
customMessage = 'Plugin Showcase',
showDebugInfo = true
}) => {
const { t, i18n } = useTranslation();
const { data: authData, isLoading: authLoading } = useAuthQuery();
const [isOpen, setIsOpen] = useState(false);
const [activeTab, setActiveTab] = useState<'user' | 'features' | 'system' | 'routing'>('user');
const [loadingDemo, setLoadingDemo] = useState(false);
const [currentPath, setCurrentPath] = useState<string>('');
const triggerRef = useRef<HTMLButtonElement>(null);
// Get current pathname from window.location (works in plugins)
useEffect(() => {
setCurrentPath(window.location.pathname);
// Listen for route changes via popstate
const handleRouteChange = () => {
setCurrentPath(window.location.pathname);
};
window.addEventListener('popstate', handleRouteChange);
return () => window.removeEventListener('popstate', handleRouteChange);
}, []);
const handleButtonPress = () => {
if (!isOpen) {
// Simulate a loading state when opening
setLoadingDemo(true);
setTimeout(() => {
setLoadingDemo(false);
}, 800);
}
setIsOpen(!isOpen);
};
// Derive authenticated from authData
const authenticated = !!authData?.id;
// Example of accessing host features
const userFeatures = [
{ icon: 'person', label: 'Authentication', value: authenticated ? 'Active' : 'Inactive' },
{ icon: 'language', label: 'Language', value: i18n.language || 'en' },
{ icon: 'email', label: 'User Email', value: authData?.email || 'N/A' },
{ icon: 'badge', label: 'User ID', value: authData?.id || 'N/A' },
];
const systemFeatures = [
{ icon: 'check_circle', label: 'React Hook (useAuthQuery)', enabled: true },
{ icon: 'check_circle', label: 'Window Location API', enabled: true },
{ icon: 'check_circle', label: 'PopState Events', enabled: true },
{ icon: 'check_circle', label: 'i18next Integration', enabled: true },
{ icon: 'check_circle', label: 'styled-components', enabled: true },
{ icon: 'check_circle', label: 'react-aria-components', enabled: true },
{ icon: 'check_circle', label: 'Host UI Components', enabled: true },
{ icon: 'check_circle', label: '@tanstack/react-query', enabled: true },
];
const pluginCapabilities = [
'Access authentication state from host',
'Use host UI components (Icon, Box, Text, Loading)',
'Leverage host hooks and utilities (useAuthQuery)',
'Read current route via window.location',
'Listen to route changes via popstate events',
'Integrate with i18n for translations',
'Use styled-components for styling',
'Implement accessible UI with react-aria',
'Receive props from plugin configuration',
'React to route changes via visibility config',
];
const renderUserTab = () => (
<TabContent>
<InfoCard>
<Text $weight="bold" $size="l" style={{ marginBottom: '12px', display: 'block' }}>
{t('User Information')}
</Text>
{authLoading ? (
<Box $align="center" $justify="center" $padding="medium">
<Loading />
</Box>
) : (
<>
{userFeatures.map((feature, idx) => (
<InfoRow key={idx}>
<Label>
<Icon iconName={feature.icon} $size="s" style={{ marginRight: '8px' }} />
{feature.label}
</Label>
<Value>{feature.value}</Value>
</InfoRow>
))}
</>
)}
</InfoCard>
{showDebugInfo && authData && (
<InfoCard>
<Text $weight="bold" $size="m" style={{ marginBottom: '8px', display: 'block' }}>
Raw Auth Data
</Text>
<pre style={{
fontSize: '0.75rem',
overflow: 'auto',
maxHeight: '200px',
backgroundColor: 'rgba(0,0,0,0.05)',
padding: '12px',
borderRadius: '4px'
}}>
{JSON.stringify(authData, null, 2)}
</pre>
</InfoCard>
)}
</TabContent>
);
const renderFeaturesTab = () => (
<TabContent>
<Text $weight="bold" $size="l" style={{ marginBottom: '16px', display: 'block' }}>
{t('Plugin Capabilities')}
</Text>
<FeatureList>
{pluginCapabilities.map((capability, idx) => (
<FeatureItem key={idx}>
<Icon iconName="check_circle" variant="filled" $color="#4caf50" />
<Text $size="s">{capability}</Text>
</FeatureItem>
))}
</FeatureList>
<InfoCard style={{ marginTop: '20px' }}>
<Text $weight="bold" $size="m" style={{ marginBottom: '12px', display: 'block' }}>
Props from Config
</Text>
<InfoRow>
<Label>Custom Message</Label>
<Value>{customMessage}</Value>
</InfoRow>
<InfoRow>
<Label>Debug Mode</Label>
<Value>{showDebugInfo ? 'Enabled' : 'Disabled'}</Value>
</InfoRow>
</InfoCard>
</TabContent>
);
const renderSystemTab = () => (
<TabContent>
<Text $weight="bold" $size="l" style={{ marginBottom: '16px', display: 'block' }}>
{t('Integrated Host Features')}
</Text>
<FeatureList>
{systemFeatures.map((feature, idx) => (
<FeatureItem key={idx}>
<Icon
iconName={feature.enabled ? 'check_circle' : 'cancel'}
variant="filled"
$color={feature.enabled ? '#4caf50' : '#f44336'}
/>
<Text $size="s">{feature.label}</Text>
</FeatureItem>
))}
</FeatureList>
<InfoCard style={{ marginTop: '20px' }}>
<Text $weight="bold" $size="m" style={{ marginBottom: '12px', display: 'block' }}>
Available Host Components
</Text>
<Text $size="xs" style={{ lineHeight: '1.6' }}>
Icon, Loading, Box, Text, Link, Card, Modal, DropButton, DropdownMenu,
InfiniteScroll, QuickSearch, Separators, TextErrors, and more...
</Text>
</InfoCard>
</TabContent>
);
const renderRoutingTab = () => (
<TabContent>
<Text $weight="bold" $size="l" style={{ marginBottom: '16px', display: 'block' }}>
{t('Route Information (Plugin-Safe)')}
</Text>
<InfoCard>
<Text $weight="bold" $size="m" style={{ marginBottom: '12px', display: 'block' }}>
Current Route Information
</Text>
<InfoRow>
<Label>
<Icon iconName="route" $size="s" style={{ marginRight: '8px' }} />
Current Path
</Label>
<Value>{currentPath || '/'}</Value>
</InfoRow>
<InfoRow>
<Label>
<Icon iconName="link" $size="s" style={{ marginRight: '8px' }} />
Full URL
</Label>
<Value style={{ fontSize: '0.75rem', maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{typeof window !== 'undefined' ? window.location.href : 'N/A'}
</Value>
</InfoRow>
<InfoRow>
<Label>
<Icon iconName="tag" $size="s" style={{ marginRight: '8px' }} />
URL Hash
</Label>
<Value>{typeof window !== 'undefined' ? (window.location.hash || 'none') : 'N/A'}</Value>
</InfoRow>
</InfoCard>
<InfoCard>
<Text $weight="bold" $size="m" style={{ marginBottom: '12px', display: 'block' }}>
Plugin Routing Capabilities
</Text>
<FeatureList>
<FeatureItem>
<Icon iconName="check_circle" variant="filled" $color="#4caf50" />
<Text $size="s">Read pathname via window.location</Text>
</FeatureItem>
<FeatureItem>
<Icon iconName="check_circle" variant="filled" $color="#4caf50" />
<Text $size="s">Listen to popstate events for route changes</Text>
</FeatureItem>
<FeatureItem>
<Icon iconName="check_circle" variant="filled" $color="#4caf50" />
<Text $size="s">Use visibility.routes in config for conditional rendering</Text>
</FeatureItem>
<FeatureItem>
<Icon iconName="info" variant="filled" $color="#2196f3" />
<Text $size="s">Navigate using standard anchor tags or window APIs</Text>
</FeatureItem>
<FeatureItem>
<Icon iconName="warning" variant="filled" $color="#ff9800" />
<Text $size="s">Next.js router hooks not available (outside RouterContext)</Text>
</FeatureItem>
</FeatureList>
</InfoCard>
<InfoCard style={{ background: 'linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%)', borderLeft: '4px solid #ff9800' }}>
<Text $weight="bold" $size="s" style={{ marginBottom: '8px', display: 'block' }}>
Important Note
</Text>
<Text $size="xs" style={{ lineHeight: '1.6', marginBottom: '8px' }}>
Plugins render outside the Next.js RouterContext, so useRouter() and usePathname()
are not available. Instead, use:
</Text>
<ul style={{ fontSize: '0.75rem', lineHeight: '1.8', marginLeft: '20px' }}>
<li><code>window.location.pathname</code> - Get current path</li>
<li><code>window.addEventListener('popstate')</code> - Detect route changes</li>
<li>Plugin config <code>visibility.routes</code> - Control when plugin appears</li>
</ul>
</InfoCard>
<InfoCard style={{ background: 'linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%)', borderLeft: '4px solid #4caf50' }}>
<Text $weight="bold" $size="s" style={{ marginBottom: '8px', display: 'block' }}>
Best Practice
</Text>
<Text $size="xs" style={{ lineHeight: '1.6' }}>
For route-aware plugins, use the <code>visibility.routes</code> config option with
glob patterns (e.g., <code>["/docs/*", "!/docs/secret"]</code>). The plugin system
automatically shows/hides your plugin based on the current route!
</Text>
</InfoCard>
</TabContent>
);
if (authLoading) {
return (
<Box $padding="small">
<Loading />
</Box>
);
}
return (
<>
<StyledButton
ref={triggerRef}
onPress={handleButtonPress}
aria-label="Open Plugin Showcase"
aria-expanded={isOpen}
>
<Icon iconName="extension" variant="filled" />
{customMessage}
</StyledButton>
<StyledPopover
triggerRef={triggerRef}
isOpen={isOpen}
onOpenChange={setIsOpen}
offset={10}
>
{loadingDemo ? (
<Box
$padding="large"
$align="center"
$justify="center"
$height="300px"
>
<Loading />
<Text $size="s" style={{ marginTop: '16px', color: '#666' }}>
Fake Loading...
</Text>
</Box>
) : (
<>
<TabContainer>
<Tab
$active={activeTab === 'user'}
onClick={() => setActiveTab('user')}
>
<Icon iconName="person" $size="s" /> User
</Tab>
<Tab
$active={activeTab === 'features'}
onClick={() => setActiveTab('features')}
>
<Icon iconName="settings" $size="s" /> Features
</Tab>
<Tab
$active={activeTab === 'system'}
onClick={() => setActiveTab('system')}
>
<Icon iconName="integration_instructions" $size="s" /> System
</Tab>
<Tab
$active={activeTab === 'routing'}
onClick={() => setActiveTab('routing')}
>
<Icon iconName="route" $size="s" /> Routing
</Tab>
</TabContainer>
{activeTab === 'user' && renderUserTab()}
{activeTab === 'features' && renderFeaturesTab()}
{activeTab === 'system' && renderSystemTab()}
{activeTab === 'routing' && renderRoutingTab()}
</>
)}
</StyledPopover>
</>
);
};
export default MyCustomComponent;
@@ -0,0 +1,109 @@
#central-menu-wrapper {
flex-direction: row;
align-self: stretch;
align-items: stretch;
gap: 25px;
}
#central-menu-wrapper > * {
display: flex;
align-items: center;
height: auto;
}
#central-menu-wrapper > a > div {
height: 100%;
margin: unset;
}
#central-menu-wrapper > a > div > svg {
width: 82px;
}
#central-menu-wrapper + div > button {
display: none;
}
#central-menu-wrapper #central-menu {
position: relative;
color: var(--c--theme--colors--greyscale-text);
display: inline-block;
}
#central-menu-wrapper #nav-button {
background: none;
border: none;
cursor: pointer;
height: 100%;
padding: 0 22px;
outline: none;
}
#central-menu-wrapper #nav-button:hover {
background-color: var(
--c--components--button--primary-text--background--color-hover
);
}
#central-menu-wrapper #nav-button.active {
background-color: var(--c--components--button--primary--background--color);
color: var(--c--theme--colors--greyscale-000);
}
[data-testid="od-menu-popover"] {
background: unset !important;
border: unset !important;
}
#nav-content {
position: absolute;
width: max-content;
background: var(--c--theme--colors--greyscale-000);
border-radius: 8px;
border: 1px solid var(--c--theme--colors--card-border);
border-top: 4px solid var(--c--components--button--primary--background--color);
max-width: 280px;
left: 50%;
transform: translateX(-50%);
padding: 4px 0 20px;
z-index: 1000;
}
#nav-content .menu-list {
list-style: none;
margin: 0;
padding: 0;
}
#nav-content .menu-category {
font-weight: bold;
display: block;
margin: 20px 24px 8px;
}
#nav-content .menu-entries {
list-style: none;
padding: 0;
margin: 0;
}
#nav-content .menu-link {
display: flex;
padding: 4px 24px;
align-items: center;
text-decoration: none;
color: inherit;
}
#nav-content .menu-link:hover {
background-color: var(
--c--components--button--primary-text--background--color-hover
);
}
#nav-content .menu-icon {
width: 24px;
height: 24px;
margin-right: 8px;
}
@@ -0,0 +1,283 @@
import './MyCustomHeaderMenu.css';
import React, { useState, useRef, useEffect } from 'react';
import { Button, Popover } from 'react-aria-components';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import { useAuthQuery } from 'impress/features/auth/api';
import { Icon, Loading } from 'impress/components';
interface NavigationCategory {
identifier: string;
display_name: string;
entries: NavigationEntry[];
}
interface NavigationEntry {
identifier: string;
link: string;
target: string;
display_name: string;
icon_url: string;
}
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;
`;
const StyledButton = styled(Button)`
cursor: pointer;
border: none;
background: none;
outline: none;
transition: all 0.2s ease-in-out;
font-family: Marianne, Arial, serif;
font-weight: 500;
font-size: 0.938rem;
padding: 0;
text-wrap: nowrap;
`;
// Fake navigation response for development/debugging
const fakeNavigationData = {
categories: [
{
identifier: 'fake-cat',
display_name: 'Dummy Category',
entries: [
{
identifier: 'fake-entry-1',
link: 'https://www.google.com',
target: '_blank',
display_name: 'Google',
icon_url: 'https://placehold.co/24',
},
{
identifier: 'fake-entry-2',
link: 'https://www.example.com',
target: '_blank',
display_name: 'Example',
icon_url: 'https://placehold.co/24',
},
],
},
],
};
const formatLanguage = (language: string): string => {
const [lang, region] = language.split('-');
return region
? `${lang}-${lang.toUpperCase()}`
: `${language}-${language.toUpperCase()}`;
};
const fetchNavigation = async (
language: string,
baseUrl: string,
): Promise<NavigationCategory[] | null> => {
// Uncomment below for development/debugging with fake data
return fakeNavigationData.categories;
try {
if (!baseUrl) {
console.warn('[CentralMenu] ICS_BASE_URL not configured');
return null;
}
const response = await fetch(
`${baseUrl}/navigation.json?language=${language}`,
{
method: 'GET',
credentials: 'include',
redirect: 'follow',
},
);
if (response.ok) {
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
const jsonData = await response.json() as Record<string, unknown>;
if (
jsonData &&
typeof jsonData === 'object' &&
'categories' in jsonData &&
Array.isArray(jsonData.categories)
) {
return jsonData.categories as NavigationCategory[];
} else {
console.warn('[CentralMenu] Invalid JSON format in navigation response.');
return null;
}
} else {
console.warn('[CentralMenu] Unexpected content type:', contentType);
return null;
}
} else {
console.warn('[CentralMenu] Navigation fetch failed. Status:', response.status);
return null;
}
} catch (error) {
console.error('[CentralMenu] Error fetching navigation:', error);
return null;
}
};
interface CentralMenuProps {
icsBaseUrl?: string;
portalBaseUrl?: string;
}
const CentralMenu: React.FC<CentralMenuProps> = ({
icsBaseUrl = '',
portalBaseUrl = '',
}) => {
const { i18n, t } = useTranslation();
const { data: auth } = useAuthQuery();
const [isOpen, setIsOpen] = useState(false);
const [navigation, setNavigation] = useState<NavigationCategory[] | null>(null);
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
const iframeRef = useRef<HTMLIFrameElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
const handleToggle = () => {
setIsOpen(!isOpen);
};
const handleIframeLoad = async () => {
const language = i18n.language ? formatLanguage(i18n.language) : 'en-US';
const navData = await fetchNavigation(language, icsBaseUrl);
if (navData) {
setNavigation(navData);
setStatus('success');
} else {
setStatus('error');
}
};
// Handle language changes - refetch navigation when language changes
useEffect(() => {
// Only refetch if iframe has already loaded (navigation exists or error occurred)
if (status !== 'loading') {
handleIframeLoad();
}
}, [i18n.language]);
if (!auth?.id) {
return null;
}
const renderNavigation = () => {
if (!navigation) {
return null;
}
return navigation.map((category) => (
<li
key={category.identifier}
data-testid="od-menu-app-category"
>
<span className="menu-category">{category.display_name}</span>
<ul className="menu-entries" data-testid="od-menu-apps">
{category.entries.map((entry) => (
<li key={entry.identifier} data-testid="od-menu-app">
<a
href={entry.link}
onClick={(e) => {
e.stopPropagation();
setIsOpen(false);
}}
target={entry.target}
className="menu-link"
role="menuitem"
>
<img
alt={entry.display_name}
src={entry.icon_url}
className="menu-icon"
width={24}
height={24}
/>
<span>{entry.display_name}</span>
</a>
</li>
))}
</ul>
</li>
));
};
return (
<>
{icsBaseUrl && (
<iframe
ref={iframeRef}
title="opendesk login"
src={`${icsBaseUrl}/silent`}
data-testid="od-menu-iframe"
hidden
onLoad={handleIframeLoad}
style={{ display: 'none', visibility: 'hidden' }}
/>
)}
<nav id="central-menu" role="navigation" aria-label="Main Navigation">
<StyledButton
id="nav-button"
className={isOpen ? 'active' : ''}
ref={triggerRef}
onPress={handleToggle}
aria-label="Toggle Central Menu"
aria-expanded={isOpen}
aria-controls="nav-content"
data-testid="od-menu-open-button"
>
<Icon iconName="apps" aria-hidden="true" />
</StyledButton>
<StyledPopover
triggerRef={triggerRef}
isOpen={isOpen}
onOpenChange={setIsOpen}
offset={0}
containerPadding={0}
data-testid="od-menu-popover"
>
<div id="nav-content">
<ul className="menu-list">
{status === 'error' ? (
<li className="menu-category" data-testid="od-menu-app-category">
<small>
{t('Navigation could not be accessed.')}
{portalBaseUrl && (
<>
<br />
<a
href={portalBaseUrl}
onClick={(e) => e.stopPropagation()}
>
{t('Try logging out and logging in again.')}
</a>
</>
)}
</small>
</li>
) : status === 'loading' ? (
<li className="menu-category" data-testid="od-menu-app-category">
<Loading />
</li>
) : (
renderNavigation()
)}
</ul>
</div>
</StyledPopover>
</nav>
</>
);
};
export default CentralMenu;
@@ -0,0 +1,220 @@
import React, { useEffect } from 'react';
interface ThemeColors {
// Primary colors
'primary-100'?: string;
'primary-200'?: string;
'primary-300'?: string;
'primary-400'?: string;
'primary-500'?: string;
'primary-600'?: string;
'primary-700'?: string;
'primary-800'?: string;
'primary-900'?: string;
'primary-text'?: string;
'primary-bg'?: string;
// Secondary colors
'secondary-100'?: string;
'secondary-200'?: string;
'secondary-300'?: string;
'secondary-400'?: string;
'secondary-500'?: string;
'secondary-600'?: string;
'secondary-700'?: string;
'secondary-800'?: string;
'secondary-900'?: string;
'secondary-text'?: string;
'secondary-bg'?: string;
// Info colors
'info-100'?: string;
'info-200'?: string;
'info-300'?: string;
'info-400'?: string;
'info-500'?: string;
'info-600'?: string;
'info-700'?: string;
'info-800'?: string;
'info-900'?: string;
'info-text'?: string;
// Success colors
'success-100'?: string;
'success-200'?: string;
'success-300'?: string;
'success-400'?: string;
'success-500'?: string;
'success-600'?: string;
// Warning colors
'warning-100'?: string;
'warning-200'?: string;
'warning-300'?: string;
'warning-400'?: string;
'warning-500'?: string;
'warning-600'?: string;
// Error colors
'error-100'?: string;
'error-200'?: string;
'error-300'?: string;
'error-400'?: string;
'error-500'?: string;
'error-600'?: string;
// Greyscale
'greyscale-000'?: string;
'greyscale-100'?: string;
'greyscale-200'?: string;
'greyscale-300'?: string;
'greyscale-400'?: string;
'greyscale-500'?: string;
'greyscale-600'?: string;
'greyscale-700'?: string;
'greyscale-800'?: string;
'greyscale-900'?: string;
}
interface ThemeSpacings {
'xxxxs'?: string;
'xxxs'?: string;
'xxs'?: string;
'xs'?: string;
's'?: string;
'b'?: string;
'st'?: string;
't'?: string;
'l'?: string;
'xl'?: string;
'xxl'?: string;
'xxxl'?: string;
}
interface ThemeFontSizes {
'xxxxs'?: string;
'xxxs'?: string;
'xxs'?: string;
'xs'?: string;
's'?: string;
'm'?: string;
'l'?: string;
'xl'?: string;
'xxl'?: string;
'xxxl'?: string;
'xxxxl'?: string;
}
interface ThemingComponentProps {
colors?: ThemeColors;
spacings?: ThemeSpacings;
fontSizes?: ThemeFontSizes;
customVars?: Record<string, string>;
}
/**
* ThemingComponent - Override Cunningham design tokens from a plugin
*
* This component allows plugins to customize the host application's theme by
* setting CSS custom properties (CSS variables).
*
* IMPORTANT: This component ONLY updates CSS variables, not the Zustand store.
* This means:
* - Components using CSS variables like `var(--c--theme--colors--primary-text)` WILL update
* - Components using JS values like `colorsTokens['primary-text']` will NOT update
*
* For host components to be themeable by plugins, they must use CSS variables instead of
* JS token values. Example:
*
* Don't use: `$color={colorsTokens['primary-text']}`
* Use instead: `$theme="primary" $variation="text"` (which generates CSS vars)
*
* @example
* ```tsx
* <ThemingComponent
* colors={{
* 'primary-500': '#FF6B6B',
* 'primary-600': '#EE5A6F',
* 'primary-text': '#FF6B6B',
* 'secondary-500': '#4ECDC4',
* }}
* spacings={{
* 's': '12px',
* 'm': '24px',
* }}
* customVars={{
* '--custom-header-height': '80px',
* }}
* />
* ```
*/
const ThemingComponent: React.FC<ThemingComponentProps> = ({
colors = {},
spacings = {},
fontSizes = {},
customVars = {},
}) => {
useEffect(() => {
const root = document.documentElement;
// Store original values for cleanup
const originalValues: Record<string, string> = {};
console.log('[ThemingComponent] Applying CSS variable overrides');
// Apply CSS custom properties (for CSS variable-based usage)
Object.entries(colors).forEach(([key, value]) => {
const cssVar = `--c--theme--colors--${key}`;
originalValues[cssVar] = root.style.getPropertyValue(cssVar);
root.style.setProperty(cssVar, value);
});
// Apply spacing tokens
Object.entries(spacings).forEach(([key, value]) => {
const cssVar = `--c--theme--spacings--${key}`;
originalValues[cssVar] = root.style.getPropertyValue(cssVar);
root.style.setProperty(cssVar, value);
});
// Apply font size tokens
Object.entries(fontSizes).forEach(([key, value]) => {
const cssVar = `--c--theme--font--sizes--${key}`;
originalValues[cssVar] = root.style.getPropertyValue(cssVar);
root.style.setProperty(cssVar, value);
});
// Apply custom CSS variables
Object.entries(customVars).forEach(([key, value]) => {
originalValues[key] = root.style.getPropertyValue(key);
root.style.setProperty(key, value);
});
console.log('[ThemingComponent] Applied CSS variable overrides:', {
colors: Object.keys(colors).length,
spacings: Object.keys(spacings).length,
fontSizes: Object.keys(fontSizes).length,
customVars: Object.keys(customVars).length,
});
// Cleanup: restore original values on unmount
return () => {
console.log('[ThemingComponent] Restoring original CSS variables');
// Restore CSS variables
Object.entries(originalValues).forEach(([cssVar, originalValue]) => {
if (originalValue) {
root.style.setProperty(cssVar, originalValue);
} else {
root.style.removeProperty(cssVar);
}
});
console.log('[ThemingComponent] Restored original CSS variables');
};
}, [colors, spacings, fontSizes, customVars]);
// This component doesn't render anything visible
return null;
};
export default ThemingComponent;
@@ -0,0 +1,163 @@
import React, { useState } from 'react';
import ThemingComponent from './ThemingComponent';
/**
* ThemingDemo - Interactive demo component to test theme overrides
*
* This component demonstrates the ThemingComponent by providing
* a UI to dynamically change theme tokens and see the results.
*/
const ThemingDemo: React.FC = () => {
const [primaryColor, setPrimaryColor] = useState('#667eea');
const [secondaryColor, setSecondaryColor] = useState('#764ba2');
const [enabled, setEnabled] = useState(true);
return (
<>
{enabled && (
<ThemingComponent
colors={{
// Primary color variants
'primary-500': primaryColor,
'primary-600': secondaryColor,
'primary-text': primaryColor,
'primary-bg': primaryColor,
// Secondary color variants
'secondary-500': secondaryColor,
'secondary-text': secondaryColor,
'secondary-bg': secondaryColor,
}}
customVars={{
'--demo-gradient': `linear-gradient(135deg, ${primaryColor} 0%, ${secondaryColor} 100%)`,
}}
/>
)}
<div style={{
position: 'fixed',
bottom: '20px',
right: '20px',
backgroundColor: 'white',
border: '1px solid var(--c--theme--colors--greyscale-300)',
borderRadius: '12px',
padding: '20px',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
zIndex: 9999,
minWidth: '300px',
}}>
<h3 style={{
margin: '0 0 16px 0',
fontSize: '16px',
fontWeight: 'bold',
color: 'var(--c--theme--colors--greyscale-900)',
}}>
🎨 Theme Override Demo
</h3>
<div style={{ marginBottom: '12px' }}>
<label style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '14px',
cursor: 'pointer',
}}>
<input
type="checkbox"
checked={enabled}
onChange={(e) => setEnabled(e.target.checked)}
/>
Enable Theme Override
</label>
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{
display: 'block',
fontSize: '12px',
fontWeight: '600',
marginBottom: '4px',
color: 'var(--c--theme--colors--greyscale-700)',
}}>
Primary Color
</label>
<input
type="color"
value={primaryColor}
onChange={(e) => setPrimaryColor(e.target.value)}
disabled={!enabled}
style={{
width: '100%',
height: '36px',
border: '1px solid var(--c--theme--colors--greyscale-300)',
borderRadius: '6px',
cursor: enabled ? 'pointer' : 'not-allowed',
}}
/>
<span style={{
fontSize: '11px',
color: 'var(--c--theme--colors--greyscale-500)',
}}>
{primaryColor}
</span>
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{
display: 'block',
fontSize: '12px',
fontWeight: '600',
marginBottom: '4px',
color: 'var(--c--theme--colors--greyscale-700)',
}}>
Secondary Color
</label>
<input
type="color"
value={secondaryColor}
onChange={(e) => setSecondaryColor(e.target.value)}
disabled={!enabled}
style={{
width: '100%',
height: '36px',
border: '1px solid var(--c--theme--colors--greyscale-300)',
borderRadius: '6px',
cursor: enabled ? 'pointer' : 'not-allowed',
}}
/>
<span style={{
fontSize: '11px',
color: 'var(--c--theme--colors--greyscale-500)',
}}>
{secondaryColor}
</span>
</div>
<div style={{
marginTop: '16px',
padding: '12px',
background: 'var(--demo-gradient, linear-gradient(135deg, #667eea 0%, #764ba2 100%))',
borderRadius: '8px',
color: 'white',
fontSize: '12px',
textAlign: 'center',
fontWeight: '600',
}}>
Preview: Gradient with current colors
</div>
<div style={{
marginTop: '12px',
fontSize: '11px',
color: 'var(--c--theme--colors--greyscale-600)',
lineHeight: '1.4',
}}>
💡 This demo overrides primary-500, primary-600, and secondary-500 css variables. It only works with CSS.
</div>
</div>
</>
);
};
export default ThemingDemo;
@@ -0,0 +1,2 @@
// This file is just to satisfy webpack's entry point requirement
// The actual component is exposed via Module Federation
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"outDir": "./dist",
"baseUrl": ".",
"paths": {
"*": ["./@mf-types/*"],
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
@@ -0,0 +1,104 @@
const path = require('path');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const {
NativeFederationTypeScriptHost,
} = require('@module-federation/native-federation-typescript/webpack');
const {
NativeFederationTypeScriptHost: NativeFederationTypeScriptHostCore,
} = require('@module-federation/native-federation-typescript');
const moduleFederationConfig = {
name: 'plugin_frontend',
filename: 'remoteEntry.js',
exposes: {
'./MyCustomComponent': './src/MyCustomComponent.tsx',
'./MyCustomHeaderMenu': './src/MyCustomHeaderMenu.tsx',
'./ThemingComponent': './src/ThemingComponent.tsx',
'./ThemingDemo': './src/ThemingDemo.tsx',
},
remotes: {
impress: 'impress@http://localhost:3000/_next/static/chunks/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
'styled-components': { singleton: true },
'react-aria-components': { singleton: true },
'@openfun/cunningham-react': { singleton: true },
'react-i18next': { singleton: true },
'yjs': { singleton: true },
'@gouvfr-lasuite/ui-kit': { singleton: true },
'clsx': { singleton: true },
'cmdk': { singleton: true },
'react-intersection-observer': { singleton: true },
'@tanstack/react-query': { singleton: true },
'zustand': { singleton: true },
},
};
let mfTypesReady;
const ensureFederatedTypesPlugin = {
apply(compiler) {
compiler.hooks.beforeCompile.tapPromise(
'EnsureFederatedTypes',
async () => {
if (!mfTypesReady) {
const downloader = NativeFederationTypeScriptHostCore.raw({
moduleFederationConfig,
});
mfTypesReady = downloader.writeBundle();
}
await mfTypesReady;
},
);
},
};
module.exports = (env, argv) => {
const dev = argv.mode !== 'production';
return {
entry: './src/index.tsx',
mode: dev ? 'development' : 'production',
devServer: dev ? {
port: 3002,
hot: true,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
},
} : undefined,
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: dev ? 'http://localhost:3002/' : undefined,
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.jsx'],
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new ModuleFederationPlugin(moduleFederationConfig),
...(dev
? [
ensureFederatedTypesPlugin,
NativeFederationTypeScriptHost({
moduleFederationConfig,
}),
]
: []),
],
};
};
+3 -3
View File
@@ -1,3 +1,3 @@
NEXT_PUBLIC_API_ORIGIN=
NEXT_PUBLIC_SW_DEACTIVATED=
NEXT_PUBLIC_PUBLISH_AS_MIT=true
NEXT_PUBLIC_API_ORIGIN=http://localhost:8071
NEXT_PUBLIC_SW_DEACTIVATED=false
NEXT_PUBLIC_PUBLISH_AS_MIT=false
@@ -1,3 +1,5 @@
NEXT_PUBLIC_API_ORIGIN=http://localhost:8071
NEXT_PUBLIC_PUBLISH_AS_MIT=false
NEXT_PUBLIC_SW_DEACTIVATED=true
NEXT_PUBLIC_DEVELOP_PLUGINS=false
@@ -1,3 +0,0 @@
out/**/*
public/assets/fonts/**/*
node_modules/**/*
+305
View File
@@ -0,0 +1,305 @@
const path = require('path');
const ts = require('typescript');
/**
* @file This file configures Module Federation for the 'impress' application.
* It automatically exposes components and shares dependencies.
*/
/**
* Extracts the root name of a package from an import specifier.
* e.g., '@scope/name/foo' becomes '@scope/name', and 'next/link' becomes 'next'.
* @param {string} spec - The import specifier.
* @returns {string} The root package name.
*/
function pkgRootName(spec) {
// '@scope/name/foo' -> '@scope/name', 'next/link' -> 'next'
if (spec.startsWith('@')) {
const [scope, name] = spec.split('/');
return `${scope}/${name || ''}`;
}
return spec.split('/')[0];
}
/**
* Checks if an import specifier is a relative path or a path alias.
* @param {string} spec - The import specifier.
* @returns {boolean} True if the path is relative or an alias.
*/
function isRelativeOrAlias(spec) {
return spec.startsWith('.') || spec.startsWith('/') || spec.startsWith('@/');
}
/**
* Builds the key for the 'exposes' object from an absolute file path.
* This creates a public path like './components/Button' from a file path.
* @param {string} absPath - The absolute path to the file.
* @param {string} root - The project root directory.
* @returns {string} The key for the 'exposes' object.
*/
function buildExposeKey(absPath, root) {
const relPath = path.relative(root, absPath).replace(/\\/g, '/'); // Convert backslashes to forward slashes
// Remove 'src/' prefix to create cleaner public paths
const pathWithoutSrc = relPath.startsWith('src/')
? relPath.substring(4)
: relPath;
const relNoExt = pathWithoutSrc.replace(/\.(t|j)sx?$/, ''); // Remove file extension
// Remove '/index' from the end of the path to allow importing the directory
const withoutIndex = relNoExt.replace(/\/index$/, '');
return './' + withoutIndex;
}
/**
* Scans specified folders for TypeScript/JavaScript files to expose via Module Federation.
* It uses the TypeScript compiler API to find all files with actual runtime exports.
* @param {string[]} folders - An array of folder paths to scan.
* @returns {{exposes: Record<string, string>, program: ts.Program}} An object containing the exposed modules and the TypeScript program instance.
*/
function makeExposes(folders) {
const projectRoot = process.cwd();
const tsconfigPath = path.resolve(projectRoot, 'tsconfig.json');
// Read and parse the tsconfig.json file
const cfg = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
if (cfg.error) {
throw new Error(formatTsError('Failed to read tsconfig', cfg.error));
}
const parsed = ts.parseJsonConfigFileContent(
cfg.config,
ts.sys,
path.dirname(tsconfigPath),
undefined,
tsconfigPath,
);
// Create a TypeScript program to analyze the source files
const program = ts.createProgram({
rootNames: parsed.fileNames,
options: parsed.options,
});
const checker = program.getTypeChecker();
const dirSet = new Set(
folders.map((f) => path.resolve(process.cwd(), f).replace(/\\/g, '/')),
);
const exposes = {};
const used = new Set(); // Tracks used expose keys to detect duplicates
for (const sf of program.getSourceFiles()) {
// Skip declaration files (.d.ts)
if (sf.isDeclarationFile) {
continue;
}
const abs = sf.fileName.replace(/\\/g, '/');
// Only include files from the specified folders
if (
![...dirSet].some((dir) =>
abs.startsWith(dir.endsWith('/') ? dir : dir + '/'),
)
) {
continue;
}
// Only include valid script files
const ext = path.extname(abs).toLowerCase();
if (!['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {
continue;
}
// Exclude node_modules, test files, and mocks
if (
abs.includes('/node_modules/') ||
/[.-](test|spec|stories)\.(t|j)sx?$/.test(abs) ||
abs.includes('/__tests__/') ||
abs.includes('/__mocks__/')
) {
continue;
}
const moduleSymbol = sf.symbol;
if (!moduleSymbol) {
continue;
}
// Check if the module has any runtime exports (values, not just types)
const hasRuntime = checker.getExportsOfModule(moduleSymbol).some((sym) => {
const tgt =
sym.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(sym) : sym;
return (tgt.getFlags() & ts.SymbolFlags.Value) !== 0;
});
if (!hasRuntime) {
continue;
}
const key = buildExposeKey(abs, process.cwd());
const rel = './' + path.relative(process.cwd(), abs).replace(/\\/g, '/');
// Handle duplicate keys, preferring non-index files
if (used.has(key)) {
const isNewFileIndex = path.basename(abs, path.extname(abs)) === 'index';
const existingPath = exposes[key];
console.warn(
`[makeExposes] Duplicate expose key "${key}". ` +
`${isNewFileIndex ? 'Skipping index file' : 'Keeping first occurrence'}: "${rel}". ` +
`Existing: "${existingPath}".`,
);
if (isNewFileIndex) {
continue; // Index files lose in case of conflicts
}
// Otherwise: the first file encountered keeps the key
}
exposes[key] = rel;
used.add(key);
}
return { exposes, program };
}
/**
* Resolves the installed version of a package.
* @param {string} pkgName - The name of the package.
* @param {string} projectRoot - The root directory of the project.
* @returns {string|null} The installed version, or null if not found.
*/
function resolveInstalledVersion(pkgName, projectRoot) {
try {
const pkgJsonPath = require.resolve(`${pkgName}/package.json`, {
paths: [projectRoot],
});
const { version } = require(pkgJsonPath);
return version || null; // e.g. "18.3.1"
} catch {
return null; // not installed or cannot resolve
}
}
/**
* Automatically determines which dependencies to share based on the imports
* of the exposed files.
* @param {object} options - The options for making shared dependencies.
* @param {ts.Program} options.program - The TypeScript program instance.
* @param {Record<string, string>} options.exposes - The exposed modules.
* @param {string[]} [options.include=[]] - A list of packages to include in sharing (e.g., 'react', 'styled-components').
* @param {string[]} [options.exclude=[]] - A list of packages to exclude from sharing.
* @returns {Record<string, object>} The shared dependencies configuration.
*/
function makeSharedAuto({ program, exposes, include = [], exclude = [] }) {
const projectRoot = process.cwd();
const { dependencies = {}, peerDependencies = {} } = require(
path.join(projectRoot, 'package.json'),
);
const declared = { ...dependencies, ...peerDependencies };
const wanted = new Set(include);
const exposedFiles = new Set(
Object.values(exposes).map((p) =>
path.resolve(projectRoot, p.replace(/^\.\//, '')).replace(/\\/g, '/'),
),
);
// Analyze imports of exposed files to find dependencies to share
for (const sf of program.getSourceFiles()) {
const abs = sf.fileName.replace(/\\/g, '/');
if (!exposedFiles.has(abs)) {
continue;
}
const specs = (sf.imports || []).map((n) => n.text).filter(Boolean);
for (const spec of specs) {
if (isRelativeOrAlias(spec)) {
continue;
}
wanted.add(pkgRootName(spec));
}
}
// Prune excluded packages
for (const name of [...wanted]) {
if (exclude.includes(name)) {
wanted.delete(name);
}
}
// Build the shared object for Webpack
return Object.fromEntries(
[...wanted].map((name) => {
// Prefer the actually installed version for 'requiredVersion'
const resolved = resolveInstalledVersion(name, projectRoot);
// Fallback to the version range from package.json if resolution fails
const range = declared[name] || undefined;
const requiredVersion = resolved || range; // Use concrete version when possible
return [
name,
{
singleton: true,
eager: false,
requiredVersion,
strictVersion: !!resolved, // Enforce exact match if we know the concrete version
allowNodeModulesSuffixMatch: true,
},
];
}),
);
}
/**
* Formats a TypeScript diagnostic error into a readable string.
* @param {string} context - A string describing the context of the error.
* @param {ts.Diagnostic} diag - The TypeScript diagnostic object.
* @returns {string} The formatted error message.
*/
function formatTsError(context, diag) {
const message = ts.flattenDiagnosticMessageText(diag.messageText, '\n');
return `${context}: ${message}`;
}
// ---------- Final Module Federation Config ----------
// Define which folders should be scanned for exposable modules.
const foldersToExpose = [
'./src/components',
'./src/features/auth',
'./src/cunningham',
];
const { exposes, program } = makeExposes(foldersToExpose);
// Automatically determine shared dependencies based on the imports of exposed files.
const shared = makeSharedAuto({
program,
exposes,
include: ['react', 'react-dom', 'styled-components', 'yjs'],
exclude: ['next'], // Packages to never share
});
const moduleFederationConfig = {
name: 'impress',
filename: 'static/chunks/remoteEntry.js',
extraOptions: { skipSharingNextInternals: true },
// The modules exposed by this federated module.
exposes,
// The shared dependencies for this federated module.
shared,
// TypeScript type generation for plugin development.
// This is enabled when NEXT_PUBLIC_DEVELOP_PLUGINS is 'true'.
dts:
process.env.NEXT_PUBLIC_DEVELOP_PLUGINS === 'true'
? {
generateTypes: {
extractThirdParty: true,
tsConfigPath: './tsconfig.json',
},
}
: false,
};
module.exports = { moduleFederationConfig };
+67 -14
View File
@@ -1,9 +1,15 @@
const crypto = require('crypto');
const path = require('path');
const {
NativeFederationTypeScriptRemote,
} = require('@module-federation/native-federation-typescript/webpack');
const { NextFederationPlugin } = require('@module-federation/nextjs-mf');
const CopyPlugin = require('copy-webpack-plugin');
const { InjectManifest } = require('workbox-webpack-plugin');
const { moduleFederationConfig } = require('./mf.config.js');
const buildId = crypto.randomBytes(256).toString('hex').slice(0, 8);
/** @type {import('next').NextConfig} */
@@ -21,7 +27,18 @@ const nextConfig = {
env: {
NEXT_PUBLIC_BUILD_ID: buildId,
},
webpack(config, { isServer }) {
webpack(config, { isServer, dev }) {
// Prevent rebuild loops by ignoring node_modules and generated types/outputs
config.watchOptions = {
ignored: [
'**/node_modules/**',
'**/.next/**',
'**/dist/**',
'**/@mf-types/**',
'**/@mf-types.zip',
],
};
// Grab the existing rule that handles SVG imports
const fileLoaderRule = config.module.rules.find((rule) =>
rule.test?.test?.('.svg'),
@@ -67,23 +84,59 @@ const nextConfig = {
}),
);
if (!isServer && process.env.NEXT_PUBLIC_SW_DEACTIVATED !== 'true') {
config.plugins.push(
new InjectManifest({
swSrc: './src/features/service-worker/service-worker.ts',
swDest: '../public/service-worker.js',
include: [
({ asset }) => {
return !!asset.name.match(/.*(static).*/);
},
],
}),
);
if (!isServer) {
// Host configuration for Module Federation (PluginSystem)
config.plugins.push(new NextFederationPlugin(moduleFederationConfig));
if (dev && process.env.NEXT_PUBLIC_DEVELOP_PLUGINS === 'true') {
console.log('[DEBUG] moduleFederationConfig:');
console.log(moduleFederationConfig);
config.plugins.push(
// Allow the plugin to get types/intellisense from the host at development time
NativeFederationTypeScriptRemote({
moduleFederationConfig,
}),
);
// Copy the generated @mf-types.zip to .next/static/chunks so it's served at /_next/static/chunks/
const mfTypesSource = path.resolve(__dirname, '.next', '@mf-types.zip');
const mfTypesDest = path.resolve(
__dirname,
'.next',
'static',
'chunks',
'@mf-types.zip',
);
config.plugins.push(
new CopyPlugin({
patterns: [
{
from: mfTypesSource,
to: mfTypesDest,
force: true,
noErrorOnMissing: true,
},
],
}),
);
}
if (process.env.NEXT_PUBLIC_SW_DEACTIVATED !== 'true') {
config.plugins.push(
new InjectManifest({
swSrc: './src/features/service-worker/service-worker.ts',
swDest: '../public/service-worker.js',
include: [
({ asset }) => {
return !!asset.name.match(/.*(static).*/);
},
],
}),
);
}
}
// Modify the file loader rule to ignore *.svg, since we have it handled now.
fileLoaderRule.exclude = /\.svg$/i;
return config;
},
};
+38 -40
View File
@@ -1,13 +1,13 @@
{
"name": "app-impress",
"version": "3.9.0",
"version": "3.7.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
"private": true,
"scripts": {
"dev": "next dev",
"build": "prettier --check . && yarn stylelint && next build",
"dev": "NEXT_PRIVATE_LOCAL_WEBPACK=true next dev",
"build": "prettier --check . && yarn stylelint && NEXT_PRIVATE_LOCAL_WEBPACK=true next build",
"build:ci": "cp .env.development .env.local && yarn build",
"build-theme": "cunningham -g css,ts -o src/cunningham --utility-classes && yarn prettier && yarn stylelint --fix",
"start": "npx -y serve@latest out",
@@ -19,51 +19,48 @@
},
"dependencies": {
"@ag-media/react-pdf-table": "2.0.3",
"@blocknote/code-block": "0.41.1",
"@blocknote/core": "0.41.1",
"@blocknote/mantine": "0.41.1",
"@blocknote/react": "0.41.1",
"@blocknote/xl-docx-exporter": "0.41.1",
"@blocknote/xl-multi-column": "0.41.1",
"@blocknote/xl-pdf-exporter": "0.41.1",
"@blocknote/code-block": "0.37.0",
"@blocknote/core": "0.37.0",
"@blocknote/mantine": "0.37.0",
"@blocknote/react": "0.37.0",
"@blocknote/xl-docx-exporter": "0.37.0",
"@blocknote/xl-multi-column": "0.37.0",
"@blocknote/xl-pdf-exporter": "0.37.0",
"@dnd-kit/core": "6.3.1",
"@dnd-kit/modifiers": "9.0.0",
"@emoji-mart/data": "1.2.1",
"@emoji-mart/react": "1.1.1",
"@fontsource-variable/inter": "5.2.8",
"@fontsource-variable/material-symbols-outlined": "5.2.28",
"@fontsource/material-icons": "5.2.7",
"@fontsource/material-icons": "5.2.5",
"@gouvfr-lasuite/integration": "1.0.3",
"@gouvfr-lasuite/ui-kit": "0.16.2",
"@hocuspocus/provider": "3.4.0",
"@mantine/core": "8.3.6",
"@mantine/hooks": "8.3.6",
"@gouvfr-lasuite/ui-kit": "0.16.1",
"@hocuspocus/provider": "2.15.2",
"@module-federation/runtime": "0.19.1",
"@module-federation/runtime-core": "0.19.1",
"@openfun/cunningham-react": "3.2.3",
"@react-pdf/renderer": "4.3.1",
"@sentry/nextjs": "10.22.0",
"@tanstack/react-query": "5.90.6",
"@tiptap/extensions": "3.10.1",
"@react-pdf/renderer": "4.3.0",
"@sentry/nextjs": "10.11.0",
"@tanstack/react-query": "5.87.4",
"canvg": "4.0.3",
"clsx": "2.1.1",
"cmdk": "1.1.1",
"crisp-sdk-web": "1.0.26",
"docx": "*",
"crisp-sdk-web": "1.0.25",
"docx": "9.5.0",
"emoji-datasource-apple": "16.0.0",
"emoji-mart": "5.6.0",
"emoji-regex": "10.6.0",
"i18next": "25.6.0",
"emoji-regex": "10.5.0",
"i18next": "25.5.2",
"i18next-browser-languagedetector": "8.2.0",
"idb": "8.0.3",
"lodash": "4.17.21",
"luxon": "3.7.2",
"next": "15.5.4",
"posthog-js": "1.284.0",
"next": "15.5.3",
"posthog-js": "1.264.2",
"react": "*",
"react-aria-components": "1.13.0",
"react-aria-components": "1.12.1",
"react-dom": "*",
"react-i18next": "16.2.3",
"react-intersection-observer": "10.0.0",
"react-resizable-panels": "3.0.6",
"react-i18next": "15.7.3",
"react-intersection-observer": "9.16.0",
"react-select": "5.10.2",
"styled-components": "6.1.19",
"use-debounce": "10.0.6",
@@ -72,10 +69,12 @@
"zustand": "5.0.8"
},
"devDependencies": {
"@module-federation/nextjs-mf": "8.8.42",
"@module-federation/native-federation-typescript": "0.6.2",
"@svgr/webpack": "8.1.0",
"@tanstack/react-query-devtools": "5.90.2",
"@tanstack/react-query-devtools": "5.87.4",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.9.1",
"@testing-library/jest-dom": "6.8.0",
"@testing-library/react": "16.3.0",
"@testing-library/user-event": "14.6.1",
"@types/lodash": "4.17.20",
@@ -83,22 +82,21 @@
"@types/node": "*",
"@types/react": "*",
"@types/react-dom": "*",
"@vitejs/plugin-react": "5.1.0",
"@vitejs/plugin-react": "5.0.2",
"copy-webpack-plugin": "13.0.1",
"cross-env": "10.1.0",
"dotenv": "17.2.3",
"cross-env": "10.0.0",
"dotenv": "17.2.2",
"eslint-plugin-docs": "*",
"fetch-mock": "9.11.0",
"jsdom": "27.1.0",
"jsdom": "26.1.0",
"node-fetch": "2.7.0",
"prettier": "3.6.2",
"stylelint": "16.25.0",
"stylelint-config-standard": "39.0.1",
"stylelint": "16.24.0",
"stylelint-config-standard": "39.0.0",
"stylelint-prettier": "5.0.3",
"typescript": "*",
"vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.6",
"webpack": "5.102.1",
"vitest": "3.2.4",
"workbox-webpack-plugin": "7.1.0"
},
"packageManager": "yarn@1.22.22"
+1 -1
View File
@@ -18,5 +18,5 @@ export const backendUrl = () =>
* @param apiVersion - The version of the API (defaults to '1.0').
* @returns The full versioned API base URL as a string.
*/
export const baseApiUrl = (apiVersion = '1.0') =>
export const baseApiUrl = (apiVersion: string = '1.0') =>
`${backendUrl()}/api/v${apiVersion}/`;
@@ -36,30 +36,7 @@ export interface BoxProps {
$position?: CSSProperties['position'];
$radius?: CSSProperties['borderRadius'];
$shrink?: CSSProperties['flexShrink'];
$theme?:
| 'primary'
| 'primary-text'
| 'secondary'
| 'secondary-text'
| 'info'
| 'success'
| 'warning'
| 'danger'
| 'greyscale';
$transition?: CSSProperties['transition'];
$variation?:
| 'text'
| '000'
| '100'
| '200'
| '300'
| '400'
| '500'
| '600'
| '700'
| '800'
| '900'
| '1000';
$width?: CSSProperties['width'];
$wrap?: CSSProperties['flexWrap'];
$zIndex?: CSSProperties['zIndex'];
@@ -96,12 +73,6 @@ export const Box = styled('div')<BoxProps>`
${({ $position }) => $position && `position: ${$position};`}
${({ $radius }) => $radius && `border-radius: ${$radius};`}
${({ $shrink }) => $shrink && `flex-shrink: ${$shrink};`}
${({ $theme, $variation }) => {
if (!$theme || !$variation) {
return '';
}
return `color: var(--c--theme--colors--${$theme}-${$variation});`;
}}
${({ $transition }) => $transition && `transition: ${$transition};`}
${({ $width }) => $width && `width: ${$width};`}
${({ $wrap }) => $wrap && `flex-wrap: ${$wrap};`}
@@ -24,9 +24,6 @@ export type BoxButtonType = BoxType & {
*/
const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
({ $css, ...props }, ref) => {
const theme = props.$theme || 'greyscale';
const variation = props.$variation || '400';
return (
<Box
ref={ref}
@@ -40,16 +37,10 @@ const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
border: none;
outline: none;
font-family: inherit;
color: ${props.disabled
? `var(--c--theme--colors--${theme}-400) !important`
: `inherit`};
&:focus-visible {
transition: none;
outline: 2px solid var(--c--theme--colors--${theme}-${variation});
border-radius: 1px;
outline-offset: 4px;
}
color: ${props.disabled
? 'var(--c--theme--colors--greyscale-400) !important'
: 'inherit'};
${$css || ''}
`}
{...props}
@@ -7,7 +7,6 @@ import { Box, BoxType } from '.';
export const Card = ({
children,
className,
$css,
...props
}: PropsWithChildren<BoxType>) => {
@@ -15,7 +14,7 @@ export const Card = ({
return (
<Box
className={`--docs--card ${className || ''}`}
className={`--docs--card ${props.className || ''}`}
$background="white"
$radius="4px"
$css={css`
@@ -31,7 +31,7 @@ const StyledButton = styled(Button)<StyledButtonProps>`
font-weight: 500;
font-size: 0.938rem;
padding: 0;
border-radius: 4px;
${({ $css }) => $css};
&:hover {
background-color: var(
--c--components--button--primary-text--background--color-hover
@@ -41,7 +41,6 @@ const StyledButton = styled(Button)<StyledButtonProps>`
box-shadow: 0 0 0 2px var(--c--theme--colors--primary-400);
border-radius: 4px;
}
${({ $css }) => $css};
`;
export interface DropButtonProps {
@@ -4,16 +4,12 @@ import { css } from 'styled-components';
import { Text, TextType } from '@/components';
type IconProps = TextType & {
disabled?: boolean;
iconName: string;
variant?: 'filled' | 'outlined' | 'symbols-outlined';
variant?: 'filled' | 'outlined';
};
export const Icon = ({
className,
iconName,
disabled,
variant = 'outlined',
$variation = 'text',
...textProps
}: IconProps) => {
const hasLabel = 'aria-label' in textProps || 'aria-labelledby' in textProps;
@@ -22,15 +18,12 @@ export const Icon = ({
return (
<Text
{...textProps}
aria-hidden={ariaHidden}
className={clsx('--docs--icon-bg', className, {
className={clsx('--docs--icon-bg', textProps.className, {
'material-icons-filled': variant === 'filled',
'material-icons': variant === 'outlined',
'material-symbols-outlined': variant === 'symbols-outlined',
})}
$variation={disabled ? '300' : $variation}
aria-disabled={disabled}
{...textProps}
>
{iconName}
</Text>
@@ -41,19 +34,15 @@ type IconOptionsProps = TextType & {
isHorizontal?: boolean;
};
export const IconOptions = ({
isHorizontal,
$css,
...props
}: IconOptionsProps) => {
export const IconOptions = ({ isHorizontal, ...props }: IconOptionsProps) => {
return (
<Icon
{...props}
iconName={isHorizontal ? 'more_horiz' : 'more_vert'}
$css={css`
user-select: none;
${$css}
${props.$css}
`}
{...props}
/>
);
};
@@ -1,36 +0,0 @@
import { PropsWithChildren } from 'react';
import { css } from 'styled-components';
import { Box, BoxType } from '.';
type OverlayerProps = PropsWithChildren<{
isOverlay: boolean;
}> &
Partial<BoxType>;
export const Overlayer = ({
children,
className,
$css,
isOverlay,
...props
}: OverlayerProps) => {
if (!isOverlay) {
return children;
}
return (
<Box
className={`--docs--overlayer ${className || ''}`}
$opacity="0.4"
$zIndex="10"
$css={css`
${$css}
pointer-events: none;
`}
{...props}
>
{children}
</Box>
);
};
@@ -15,6 +15,29 @@ export interface TextProps extends BoxProps {
$textAlign?: CSSProperties['textAlign'];
$textTransform?: CSSProperties['textTransform'];
$size?: TextSizes | (string & {});
$theme?:
| 'primary'
| 'primary-text'
| 'secondary'
| 'secondary-text'
| 'info'
| 'success'
| 'warning'
| 'danger'
| 'greyscale';
$variation?:
| 'text'
| '000'
| '100'
| '200'
| '300'
| '400'
| '500'
| '600'
| '700'
| '800'
| '900'
| '1000';
}
export type TextType = ComponentPropsWithRef<typeof Text>;
@@ -27,6 +50,8 @@ export const TextStyled = styled(Box)<TextProps>`
${({ $size }) =>
$size &&
`font-size: ${$size in sizes ? sizes[$size as TextSizes] : $size};`}
${({ $theme, $variation }) =>
`color: var(--c--theme--colors--${$theme}-${$variation});`}
${({ $color }) => $color && `color: ${$color};`}
${({ $ellipsis }) =>
$ellipsis &&
@@ -25,6 +25,8 @@ export const TextErrors = ({
canClose = false,
...textProps
}: TextErrorsProps) => {
const { t } = useTranslation();
return (
<AlertStyled
canClose={canClose}
@@ -32,47 +34,31 @@ export const TextErrors = ({
icon={icon}
className="--docs--text-errors"
>
<TextOnlyErrors
causes={causes}
defaultMessage={defaultMessage}
{...textProps}
/>
</AlertStyled>
);
};
<Box $direction="column" $gap="0.2rem">
{causes &&
causes.map((cause, i) => (
<Text
key={`causes-${i}`}
$theme="danger"
$variation="600"
$textAlign="center"
{...textProps}
>
{cause}
</Text>
))}
export const TextOnlyErrors = ({
causes,
defaultMessage,
...textProps
}: TextErrorsProps) => {
const { t } = useTranslation();
return (
<Box $direction="column" $gap="0.2rem">
{causes &&
causes.map((cause, i) => (
{!causes && (
<Text
key={`causes-${i}`}
$theme="danger"
$variation="600"
$textAlign="center"
{...textProps}
>
{cause}
{defaultMessage || t('Something bad happens, please retry.')}
</Text>
))}
{!causes && (
<Text
$theme="danger"
$variation="600"
$textAlign="center"
{...textProps}
>
{defaultMessage || t('Something bad happens, please retry.')}
</Text>
)}
</Box>
)}
</Box>
</AlertStyled>
);
};
@@ -2,7 +2,6 @@ import { HorizontalSeparator } from '@gouvfr-lasuite/ui-kit';
import {
Fragment,
PropsWithChildren,
ReactNode,
useCallback,
useEffect,
useRef,
@@ -16,7 +15,7 @@ import { useCunninghamTheme } from '@/cunningham';
import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav';
export type DropdownMenuOption = {
icon?: ReactNode;
icon?: string;
label: string;
testId?: string;
value?: string;
@@ -221,7 +220,7 @@ export const DropdownMenu = ({
$align="center"
$gap={spacingsTokens['base']}
>
{option.icon && typeof option.icon === 'string' && (
{option.icon && (
<Icon
$size="20px"
$theme="greyscale"
@@ -230,9 +229,6 @@ export const DropdownMenu = ({
aria-hidden="true"
/>
)}
{option.icon &&
typeof option.icon !== 'string' &&
option.icon}
<Text $variation={isDisabled ? '400' : '1000'}>
{option.label}
</Text>
@@ -1,3 +1,4 @@
export * from './modal/AlertModal';
export * from './Box';
export * from './BoxButton';
export * from './Card';
@@ -8,8 +9,7 @@ export * from './Icon';
export * from './InfiniteScroll';
export * from './Link';
export * from './Loading';
export * from './modal';
export * from './Overlayer';
export * from './modal/SideModal';
export * from './separators';
export * from './Text';
export * from './TextErrors';
@@ -3,7 +3,7 @@ import React from 'react';
import { Box } from '@/components';
export const ButtonCloseModal = (props: ButtonProps) => {
const ButtonCloseModal = (props: ButtonProps) => {
return (
<Button
type="button"
@@ -18,3 +18,5 @@ export const ButtonCloseModal = (props: ButtonProps) => {
/>
);
};
export default ButtonCloseModal;
@@ -1,2 +0,0 @@
export * from './AlertModal';
export * from './ButtonCloseModal';
@@ -12,6 +12,7 @@ import { Auth, KEY_AUTH, setAuthUrl } from '@/features/auth';
import { useResponsiveStore } from '@/stores/';
import { ConfigProvider } from './config/';
import { PluginSystemProvider } from './plugin/PluginSystemProvider';
export const DEFAULT_QUERY_RETRY = 1;
@@ -73,9 +74,11 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<CunninghamProvider theme={theme}>
<ConfigProvider>
<Auth>{children}</Auth>
</ConfigProvider>
<PluginSystemProvider>
<ConfigProvider>
<Auth>{children}</Auth>
</ConfigProvider>
</PluginSystemProvider>
</CunninghamProvider>
</QueryClientProvider>
);
@@ -2,15 +2,14 @@ import { useQuery } from '@tanstack/react-query';
import { Resource } from 'i18next';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { PluginConfig } from '@/core/plugin';
import { Theme } from '@/cunningham/';
import { FooterType } from '@/features/footer';
import { HeaderType } from '@/features/header';
import { PostHogConf } from '@/services';
interface ThemeCustomization {
footer?: FooterType;
translations?: Resource;
header?: HeaderType;
}
export interface ConfigResponse {
@@ -27,8 +26,8 @@ export interface ConfigResponse {
MEDIA_BASE_URL?: string;
POSTHOG_KEY?: PostHogConf;
SENTRY_DSN?: string;
TRASHBIN_CUTOFF_DAYS?: number;
theme_customization?: ThemeCustomization;
plugins?: PluginConfig[];
}
const LOCAL_STORAGE_KEY = 'docs_config';
+1 -1
View File
@@ -1,2 +1,2 @@
export * from './AppProvider';
export * from './config';
export * from './plugin';
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
export * from './PluginSystemProvider';
@@ -44,11 +44,6 @@
contain: content;
}
.c__button--medium {
min-height: var(--c--components--button--medium-height);
height: auto;
}
/**
* Modal
*/
@@ -44,7 +44,7 @@ export function useAuthQuery(
staleTime: 1000 * 60 * 15, // 15 minutes
retry: (failureCount, error) => {
// we assume that a 401 means the user is not logged in
if (error.status === 401) {
if (error.status == 401) {
return false;
}
return failureCount < DEFAULT_QUERY_RETRY;
@@ -1,8 +1,9 @@
import { Loader } from '@openfun/cunningham-react';
import { useRouter } from 'next/router';
import { PropsWithChildren, useEffect, useState } from 'react';
import { PropsWithChildren } from 'react';
import { Loading } from '@/components';
import { useConfig } from '@/core';
import { Box } from '@/components';
import { useConfig } from '@/core/config';
import { HOME_URL } from '../conf';
import { useAuth } from '../hooks';
@@ -13,65 +14,57 @@ export const Auth = ({ children }: PropsWithChildren) => {
useAuth();
const { replace, pathname } = useRouter();
const { data: config } = useConfig();
const [isRedirecting, setIsRedirecting] = useState(false);
if (isLoading && !isFetchedAfterMount) {
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
<Loader />
</Box>
);
}
/**
* If the user is authenticated and initially wanted to access a specific page, redirect him to that page now.
* If the user is authenticated and wanted initially to access a document,
* we redirect to the document page.
*/
useEffect(() => {
if (!authenticated || isRedirecting) {
return;
}
if (authenticated) {
const authUrl = getAuthUrl();
if (authUrl) {
setIsRedirecting(true);
void replace(authUrl).then(() => setIsRedirecting(false));
void replace(authUrl);
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
<Loader />
</Box>
);
}
}, [authenticated, isRedirecting, pathname, replace]);
}
/**
* If the user is not authenticated and not on a allowed pages
* If the user is not authenticated and the path is not allowed, we redirect to the login page.
*/
useEffect(() => {
if (isLoading || authenticated || pathAllowed || isRedirecting) {
return;
}
/**
* The homepage feature is enabled, redirect them to the homepage
*/
if (!authenticated && !pathAllowed) {
if (config?.FRONTEND_HOMEPAGE_FEATURE_ENABLED) {
if (pathname !== HOME_URL) {
setIsRedirecting(true);
void replace(HOME_URL).then(() => setIsRedirecting(false));
}
return;
void replace(HOME_URL);
} else {
gotoLogin();
}
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
<Loader />
</Box>
);
}
/**
* Redirect them to login page
*/
setIsRedirecting(true);
gotoLogin();
}, [
authenticated,
pathAllowed,
config?.FRONTEND_HOMEPAGE_FEATURE_ENABLED,
replace,
isLoading,
isRedirecting,
pathname,
]);
const shouldShowLoader =
(isLoading && !isFetchedAfterMount) ||
isRedirecting ||
(!authenticated && !pathAllowed);
if (shouldShowLoader) {
return <Loading $height="100vh" $width="100vw" />;
/**
* If the user is authenticated and the path is the home page, we redirect to the index.
*/
if (pathname === HOME_URL && authenticated) {
void replace('/');
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
<Loader />
</Box>
);
}
return children;
@@ -1,15 +1,7 @@
import { terminateCrispSession } from '@/services/Crisp';
import {
HOME_URL,
LOGIN_URL,
LOGOUT_URL,
PATH_AUTH_LOCAL_STORAGE,
} from './conf';
import { LOGIN_URL, LOGOUT_URL, PATH_AUTH_LOCAL_STORAGE } from './conf';
/**
* Get the stored auth URL from local storage
*/
export const getAuthUrl = () => {
const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE);
if (path_auth) {
@@ -18,15 +10,8 @@ export const getAuthUrl = () => {
}
};
/**
* Store the current path in local storage if it's not the homepage or root
* so we can redirect the user to this path after login
*/
export const setAuthUrl = () => {
if (
window.location.pathname !== '/' &&
window.location.pathname !== `${HOME_URL}/`
) {
if (window.location.pathname !== '/') {
localStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.pathname);
}
};
@@ -1,5 +1,4 @@
import { APIError, errorCauses } from '@/api';
import { sleep } from '@/utils';
interface CheckDocMediaStatusResponse {
file?: string;
@@ -26,28 +25,3 @@ export const checkDocMediaStatus = async ({
return response.json() as Promise<CheckDocMediaStatusResponse>;
};
/**
* Upload file can be analyzed on the server side,
* we had this function to wait for the analysis to be done
* before returning the file url. It will keep the loader
* on the upload button until the analysis is done.
* @param url
* @returns Promise<CheckDocMediaStatusResponse> status_code
* @description Waits for the upload to be analyzed by checking the status of the file.
*/
export const loopCheckDocMediaStatus = async (
url: string,
): Promise<CheckDocMediaStatusResponse> => {
const SLEEP_TIME = 5000;
const response = await checkDocMediaStatus({
urlMedia: url,
});
if (response.status === 'ready') {
return response;
} else {
await sleep(SLEEP_TIME);
return await loopCheckDocMediaStatus(url);
}
};
@@ -1,8 +1,3 @@
<svg viewBox="0 0 18 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.57954 5.93282C4.44486 5.79021 4.37793 5.61484 4.37793 5.41299C4.37793 5.21112 4.44495 5.03795 4.58154 4.90136C4.72456 4.75833 4.90437 4.6875 5.11367 4.6875H12.8964C13.0991 4.6875 13.2722 4.75857 13.408 4.90133C13.5508 5.03718 13.6219 5.21027 13.6219 5.41299C13.6219 5.61613 13.5506 5.7918 13.409 5.93387C13.273 6.07732 13.0996 6.14873 12.8964 6.14873H5.11367C4.90437 6.14873 4.72456 6.0779 4.58154 5.93487L4.57954 5.93282ZM4.57954 9.51144C4.44486 9.36882 4.37793 9.19346 4.37793 8.9916C4.37793 8.78973 4.44495 8.61656 4.58154 8.47997C4.72456 8.33695 4.90437 8.26611 5.11367 8.26611H12.8964C13.0991 8.26611 13.2722 8.33719 13.408 8.47995C13.5508 8.61579 13.6219 8.78888 13.6219 8.9916C13.6219 9.19475 13.5506 9.37042 13.409 9.51249C13.273 9.65593 13.0996 9.72734 12.8964 9.72734H5.11367C4.90437 9.72734 4.72456 9.65651 4.58154 9.51348L4.57954 9.51144ZM4.57954 13.1003C4.44561 12.9585 4.37793 12.7869 4.37793 12.5907C4.37793 12.3831 4.44414 12.204 4.57954 12.0606L4.58151 12.0586C4.72453 11.9155 4.90437 11.8447 5.11367 11.8447H8.79482C9.00363 11.8447 9.18092 11.9153 9.3177 12.0596C9.46006 12.2024 9.53057 12.3819 9.53057 12.5907C9.53057 12.7887 9.45812 12.9609 9.31671 13.1024C9.17936 13.2397 9.00235 13.306 8.79482 13.306H5.11367C4.90609 13.306 4.72695 13.2397 4.58358 13.1043L4.57954 13.1003ZM1.09476 0.851519C1.65317 0.285946 2.47955 0.0117188 3.55508 0.0117188H14.4447C15.52 0.0117188 16.3433 0.28583 16.895 0.851748C17.4529 1.41698 17.7234 2.24966 17.7234 3.33145V18.8866C17.7234 19.975 17.4531 20.8082 16.8945 21.3668C16.3427 21.9256 15.5196 22.1961 14.4447 22.1961H3.55508C2.47988 22.1961 1.65367 21.9255 1.09521 21.367C0.543652 20.8083 0.276367 19.9747 0.276367 18.8866V3.33145C0.276367 2.24984 0.543796 1.41679 1.09476 0.851519ZM15.5624 20.0351C15.2958 20.3085 14.8959 20.4452 14.3627 20.4452H3.63711C3.10391 20.4452 2.70059 20.3085 2.42715 20.0351L2.49875 19.9652L2.49786 19.9643L2.42715 20.0351C2.16055 19.7616 2.02725 19.3686 2.02725 18.8559V3.36221C2.02725 2.84951 2.16055 2.45645 2.42715 2.18301C2.70059 1.90273 3.10391 1.7626 3.63711 1.7626H14.3627C14.8959 1.7626 15.2958 1.90273 15.5624 2.18301C15.8358 2.45645 15.9726 2.84951 15.9726 3.36221V18.8559C15.9726 19.3686 15.8358 19.7616 15.5624 20.0351Z"
fill="#3A3A3A"
/>
<svg width="18" height="23" viewBox="0 0 18 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.57954 5.93282C4.44486 5.79021 4.37793 5.61484 4.37793 5.41299C4.37793 5.21112 4.44495 5.03795 4.58154 4.90136C4.72456 4.75833 4.90437 4.6875 5.11367 4.6875H12.8964C13.0991 4.6875 13.2722 4.75857 13.408 4.90133C13.5508 5.03718 13.6219 5.21027 13.6219 5.41299C13.6219 5.61613 13.5506 5.7918 13.409 5.93387C13.273 6.07732 13.0996 6.14873 12.8964 6.14873H5.11367C4.90437 6.14873 4.72456 6.0779 4.58154 5.93487L4.57954 5.93282ZM4.57954 9.51144C4.44486 9.36882 4.37793 9.19346 4.37793 8.9916C4.37793 8.78973 4.44495 8.61656 4.58154 8.47997C4.72456 8.33695 4.90437 8.26611 5.11367 8.26611H12.8964C13.0991 8.26611 13.2722 8.33719 13.408 8.47995C13.5508 8.61579 13.6219 8.78888 13.6219 8.9916C13.6219 9.19475 13.5506 9.37042 13.409 9.51249C13.273 9.65593 13.0996 9.72734 12.8964 9.72734H5.11367C4.90437 9.72734 4.72456 9.65651 4.58154 9.51348L4.57954 9.51144ZM4.57954 13.1003C4.44561 12.9585 4.37793 12.7869 4.37793 12.5907C4.37793 12.3831 4.44414 12.204 4.57954 12.0606L4.58151 12.0586C4.72453 11.9155 4.90437 11.8447 5.11367 11.8447H8.79482C9.00363 11.8447 9.18092 11.9153 9.3177 12.0596C9.46006 12.2024 9.53057 12.3819 9.53057 12.5907C9.53057 12.7887 9.45812 12.9609 9.31671 13.1024C9.17936 13.2397 9.00235 13.306 8.79482 13.306H5.11367C4.90609 13.306 4.72695 13.2397 4.58358 13.1043L4.57954 13.1003ZM1.09476 0.851519C1.65317 0.285946 2.47955 0.0117188 3.55508 0.0117188H14.4447C15.52 0.0117188 16.3433 0.28583 16.895 0.851748C17.4529 1.41698 17.7234 2.24966 17.7234 3.33145V18.8866C17.7234 19.975 17.4531 20.8082 16.8945 21.3668C16.3427 21.9256 15.5196 22.1961 14.4447 22.1961H3.55508C2.47988 22.1961 1.65367 21.9255 1.09521 21.367C0.543652 20.8083 0.276367 19.9747 0.276367 18.8866V3.33145C0.276367 2.24984 0.543796 1.41679 1.09476 0.851519ZM15.5624 20.0351C15.2958 20.3085 14.8959 20.4452 14.3627 20.4452H3.63711C3.10391 20.4452 2.70059 20.3085 2.42715 20.0351L2.49875 19.9652L2.49786 19.9643L2.42715 20.0351C2.16055 19.7616 2.02725 19.3686 2.02725 18.8559V3.36221C2.02725 2.84951 2.16055 2.45645 2.42715 2.18301C2.70059 1.90273 3.10391 1.7626 3.63711 1.7626H14.3627C14.8959 1.7626 15.2958 1.90273 15.5624 2.18301C15.8358 2.45645 15.9726 2.84951 15.9726 3.36221V18.8559C15.9726 19.3686 15.8358 19.7616 15.5624 20.0351Z" fill="#3A3A3A"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

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