Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e8dc8f6b3 | |||
| a714d5da4c | |||
| 2b5a9e1af8 | |||
| a833fdc7a1 | |||
| b3cc2bf833 | |||
| 18feab10cb | |||
| 2777488d24 | |||
| a11258f778 | |||
| 33647f124f | |||
| e339cda5c6 | |||
| 4ce65c654f | |||
| c048b2ae95 | |||
| 5908afb098 | |||
| e2298a3658 | |||
| 278eb233e9 | |||
| b056dbfad4 | |||
| 771ef2417f | |||
| 8d5262c2f2 | |||
| 1125f441dc | |||
| 16f2de4c75 | |||
| f19fa93600 | |||
| af3d90db3b | |||
| 127c90ca5f | |||
| fa7cf7a594 | |||
| 6523165ea0 | |||
| de4d11732f | |||
| 37138c1a23 | |||
| 2c1a9ff74f | |||
| 31389bcae2 | |||
| f772801fd0 | |||
| 390a615f48 | |||
| 5bdf5d2210 | |||
| ed336558ac | |||
| 4fbd588198 | |||
| 546f97c956 | |||
| af01c6e466 | |||
| 8023720da3 | |||
| 91eba31735 | |||
| 45d6c1beef |
@@ -0,0 +1,27 @@
|
||||
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
|
||||
@@ -75,3 +75,6 @@ db.sqlite3
|
||||
.vscode/
|
||||
*.iml
|
||||
.devcontainer
|
||||
|
||||
# Cursor rules
|
||||
.cursorrules
|
||||
|
||||
+48
-4
@@ -6,12 +6,50 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️(backend) increase user short_name field length
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) fix duplicate document entries in grid #1479
|
||||
- 🐛(frontend) show full nested doc names with ajustable bar #1456
|
||||
- 🐛(backend) fix trashbin list
|
||||
|
||||
## [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
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) show full nested doc names with ajustable bar #1456
|
||||
|
||||
### 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
|
||||
@@ -30,10 +68,11 @@ 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
|
||||
- 🐛(backend) duplicate sub docs as root for reader users #1385
|
||||
- ⚗️(service-worker) remove index from cache first strategy #1395
|
||||
- 🐛(frontend) fix 404 page when reload 403 page #1402
|
||||
- 🐛(frontend) fix legacy role computation #1376
|
||||
@@ -44,8 +83,11 @@ 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
|
||||
- 🐛(backend) filter invitation with case insensitive email #1457
|
||||
- 🐛(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
|
||||
|
||||
## [3.7.0] - 2025-09-12
|
||||
|
||||
@@ -109,7 +151,6 @@ 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
|
||||
|
||||
@@ -781,7 +822,10 @@ 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.7.0...main
|
||||
[unreleased]: https://github.com/suitenumerique/docs/compare/v3.8.2...main
|
||||
[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
|
||||
[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
|
||||
|
||||
@@ -99,8 +99,6 @@ 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 | |
|
||||
|
||||
@@ -1,589 +0,0 @@
|
||||
# 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!"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -32,6 +32,24 @@ 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.
|
||||
|
||||
@@ -67,6 +67,5 @@ 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
|
||||
|
||||
# Cache
|
||||
PLUGINS_CONFIG_CACHE_TIMEOUT=0
|
||||
THEME_CUSTOMIZATION_CACHE_TIMEOUT=0
|
||||
# Theme customization
|
||||
THEME_CUSTOMIZATION_CACHE_TIMEOUT=15
|
||||
@@ -5,7 +5,6 @@ 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
|
||||
|
||||
@@ -157,7 +156,6 @@ class DocumentAdmin(TreeAdmin):
|
||||
},
|
||||
),
|
||||
)
|
||||
form = movenodeform_factory(models.Document)
|
||||
inlines = (DocumentAccessInline,)
|
||||
list_display = (
|
||||
"id",
|
||||
|
||||
@@ -90,6 +90,7 @@ 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
|
||||
@@ -102,6 +103,7 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
||||
"computed_link_role",
|
||||
"created_at",
|
||||
"creator",
|
||||
"deleted_at",
|
||||
"depth",
|
||||
"excerpt",
|
||||
"is_favorite",
|
||||
@@ -124,6 +126,7 @@ class ListDocumentSerializer(serializers.ModelSerializer):
|
||||
"computed_link_role",
|
||||
"created_at",
|
||||
"creator",
|
||||
"deleted_at",
|
||||
"depth",
|
||||
"excerpt",
|
||||
"is_favorite",
|
||||
@@ -165,6 +168,10 @@ 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."""
|
||||
@@ -193,6 +200,7 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
"content",
|
||||
"created_at",
|
||||
"creator",
|
||||
"deleted_at",
|
||||
"depth",
|
||||
"excerpt",
|
||||
"is_favorite",
|
||||
@@ -216,6 +224,7 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
"computed_link_role",
|
||||
"created_at",
|
||||
"creator",
|
||||
"deleted_at",
|
||||
"depth",
|
||||
"is_favorite",
|
||||
"link_role",
|
||||
@@ -740,7 +749,8 @@ class InvitationSerializer(serializers.ModelSerializer):
|
||||
if self.instance is None:
|
||||
attrs["issuer"] = user
|
||||
|
||||
attrs["email"] = attrs["email"].lower()
|
||||
if attrs.get("email"):
|
||||
attrs["email"] = attrs["email"].lower()
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
@@ -623,12 +623,32 @@ 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)
|
||||
|
||||
@@ -851,33 +871,47 @@ class DocumentViewSet(
|
||||
|
||||
try:
|
||||
current_document = (
|
||||
self.queryset.select_related(None).only("depth", "path").get(pk=pk)
|
||||
self.queryset.select_related(None)
|
||||
.only("depth", "path", "ancestors_deleted_at")
|
||||
.get(pk=pk)
|
||||
)
|
||||
except models.Document.DoesNotExist as excpt:
|
||||
raise drf.exceptions.NotFound() from excpt
|
||||
|
||||
ancestors = (
|
||||
(
|
||||
current_document.get_ancestors()
|
||||
| self.queryset.select_related(None).filter(pk=pk)
|
||||
)
|
||||
.filter(ancestors_deleted_at__isnull=True)
|
||||
.order_by("path")
|
||||
)
|
||||
is_deleted = current_document.ancestors_deleted_at is not None
|
||||
|
||||
# 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()
|
||||
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()
|
||||
)
|
||||
|
||||
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()
|
||||
@@ -2157,6 +2191,7 @@ class ConfigView(drf.views.APIView):
|
||||
"LANGUAGES",
|
||||
"LANGUAGE_CODE",
|
||||
"SENTRY_DSN",
|
||||
"TRASHBIN_CUTOFF_DAYS",
|
||||
]
|
||||
dict_settings = {}
|
||||
for setting in array_settings:
|
||||
@@ -2164,7 +2199,6 @@ 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)
|
||||
|
||||
@@ -2202,44 +2236,3 @@ 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
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# 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"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -148,7 +148,9 @@ 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=20, null=True, blank=True)
|
||||
short_name = models.CharField(
|
||||
_("short name"), max_length=100, null=True, blank=True
|
||||
)
|
||||
|
||||
email = models.EmailField(_("identity email address"), blank=True, null=True)
|
||||
|
||||
@@ -721,7 +723,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 and not is_owner
|
||||
is_deleted = self.ancestors_deleted_at
|
||||
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
|
||||
@@ -750,6 +752,7 @@ 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
|
||||
@@ -758,7 +761,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(
|
||||
@@ -790,15 +793,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,
|
||||
"invite_owner": is_owner and not is_deleted,
|
||||
"mask": can_get and user.is_authenticated,
|
||||
"move": is_owner_or_admin and not self.ancestors_deleted_at,
|
||||
"move": is_owner_or_admin and not is_deleted,
|
||||
"partial_update": can_update,
|
||||
"restore": is_owner,
|
||||
"retrieve": can_get,
|
||||
"retrieve": retrieve,
|
||||
"media_auth": can_get,
|
||||
"link_select_options": link_select_options,
|
||||
"tree": can_get,
|
||||
"tree": retrieve,
|
||||
"update": can_update,
|
||||
"versions_destroy": is_owner_or_admin,
|
||||
"versions_list": has_access_role,
|
||||
|
||||
@@ -769,6 +769,37 @@ 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,6 +41,7 @@ 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),
|
||||
@@ -63,6 +64,7 @@ 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),
|
||||
@@ -115,6 +117,7 @@ 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),
|
||||
@@ -137,6 +140,7 @@ 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),
|
||||
@@ -208,6 +212,7 @@ 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),
|
||||
@@ -230,6 +235,7 @@ 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),
|
||||
@@ -287,6 +293,7 @@ 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),
|
||||
@@ -309,6 +316,7 @@ 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),
|
||||
@@ -393,6 +401,7 @@ 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),
|
||||
@@ -415,6 +424,7 @@ 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),
|
||||
@@ -475,6 +485,7 @@ 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),
|
||||
@@ -497,6 +508,7 @@ 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),
|
||||
@@ -609,6 +621,7 @@ 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),
|
||||
@@ -631,6 +644,7 @@ 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,6 +38,7 @@ 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),
|
||||
@@ -62,6 +63,7 @@ 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),
|
||||
@@ -84,6 +86,7 @@ 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),
|
||||
@@ -135,6 +138,7 @@ 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),
|
||||
@@ -157,6 +161,7 @@ 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),
|
||||
@@ -179,6 +184,7 @@ 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),
|
||||
@@ -251,6 +257,7 @@ 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),
|
||||
@@ -273,6 +280,7 @@ 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),
|
||||
@@ -295,6 +303,7 @@ 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),
|
||||
@@ -352,6 +361,7 @@ 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),
|
||||
@@ -374,6 +384,7 @@ 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),
|
||||
@@ -396,6 +407,7 @@ 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),
|
||||
@@ -474,6 +486,7 @@ 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),
|
||||
@@ -496,6 +509,7 @@ 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),
|
||||
@@ -518,6 +532,7 @@ 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),
|
||||
@@ -576,6 +591,7 @@ 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),
|
||||
@@ -598,6 +614,7 @@ 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),
|
||||
@@ -620,6 +637,7 @@ 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),
|
||||
@@ -724,6 +742,7 @@ 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),
|
||||
@@ -746,6 +765,7 @@ 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),
|
||||
@@ -768,6 +788,7 @@ 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,6 +65,7 @@ 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,6 +69,7 @@ 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,6 +70,7 @@ 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,
|
||||
@@ -144,6 +145,7 @@ 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,
|
||||
@@ -252,6 +254,7 @@ 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,
|
||||
@@ -333,6 +336,7 @@ 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,
|
||||
@@ -446,6 +450,7 @@ 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,
|
||||
@@ -528,6 +533,7 @@ 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",
|
||||
@@ -683,6 +689,7 @@ 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,
|
||||
@@ -749,6 +756,7 @@ 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,
|
||||
@@ -815,6 +823,7 @@ 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": 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,
|
||||
"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": True,
|
||||
"media_auth": True,
|
||||
"media_check": True,
|
||||
"mask": False,
|
||||
"media_auth": False,
|
||||
"media_check": False,
|
||||
"move": False, # Can't move a deleted document
|
||||
"partial_update": True,
|
||||
"partial_update": False,
|
||||
"restore": True,
|
||||
"retrieve": True,
|
||||
"tree": True,
|
||||
"update": True,
|
||||
"versions_destroy": True,
|
||||
"versions_list": True,
|
||||
"versions_retrieve": True,
|
||||
"update": False,
|
||||
"versions_destroy": False,
|
||||
"versions_list": False,
|
||||
"versions_retrieve": False,
|
||||
},
|
||||
"ancestors_link_reach": None,
|
||||
"ancestors_link_role": None,
|
||||
@@ -113,6 +113,7 @@ 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,
|
||||
@@ -165,10 +166,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(10):
|
||||
with django_assert_num_queries(11):
|
||||
response = client.get("/api/v1.0/documents/trashbin/")
|
||||
|
||||
with django_assert_num_queries(4):
|
||||
with django_assert_num_queries(5):
|
||||
response = client.get("/api/v1.0/documents/trashbin/")
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -207,10 +208,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(7):
|
||||
with django_assert_num_queries(8):
|
||||
response = client.get("/api/v1.0/documents/trashbin/")
|
||||
|
||||
with django_assert_num_queries(3):
|
||||
with django_assert_num_queries(4):
|
||||
response = client.get("/api/v1.0/documents/trashbin/")
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -292,3 +293,26 @@ 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()
|
||||
factories.DocumentFactory(users=[(other_user, "owner")], deleted_at=timezone.now())
|
||||
factories.DocumentFactory(users=[(other_user, "owner")], deleted_at=timezone.now())
|
||||
factories.DocumentFactory(users=[(other_user, "owner")], deleted_at=timezone.now())
|
||||
|
||||
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,6 +50,7 @@ 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,
|
||||
@@ -73,6 +74,7 @@ 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,
|
||||
@@ -96,6 +98,7 @@ 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,
|
||||
@@ -119,6 +122,7 @@ 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,
|
||||
@@ -138,6 +142,7 @@ 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,
|
||||
@@ -210,6 +215,7 @@ 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,
|
||||
@@ -233,6 +239,7 @@ 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,
|
||||
@@ -260,6 +267,7 @@ 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,
|
||||
@@ -281,6 +289,7 @@ 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,
|
||||
@@ -306,6 +315,7 @@ 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,
|
||||
@@ -327,6 +337,7 @@ 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,
|
||||
@@ -406,6 +417,7 @@ 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,
|
||||
@@ -427,6 +439,7 @@ 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,
|
||||
@@ -450,6 +463,7 @@ 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,
|
||||
@@ -469,6 +483,7 @@ 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,
|
||||
@@ -546,6 +561,7 @@ 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,
|
||||
@@ -569,6 +585,7 @@ 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,
|
||||
@@ -596,6 +613,7 @@ 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,
|
||||
@@ -617,6 +635,7 @@ 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,
|
||||
@@ -642,6 +661,7 @@ 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,
|
||||
@@ -663,6 +683,7 @@ 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,
|
||||
@@ -744,6 +765,7 @@ 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,
|
||||
@@ -765,6 +787,7 @@ 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,
|
||||
@@ -788,6 +811,7 @@ 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,
|
||||
@@ -807,6 +831,7 @@ 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,
|
||||
@@ -888,6 +913,7 @@ 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,
|
||||
@@ -911,6 +937,7 @@ 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,
|
||||
@@ -938,6 +965,7 @@ 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,
|
||||
@@ -959,6 +987,7 @@ 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,
|
||||
@@ -984,6 +1013,7 @@ 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,
|
||||
@@ -1005,6 +1035,7 @@ 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,
|
||||
@@ -1094,6 +1125,7 @@ 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,
|
||||
@@ -1115,6 +1147,7 @@ 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,
|
||||
@@ -1138,6 +1171,7 @@ 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,
|
||||
@@ -1157,6 +1191,7 @@ 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,
|
||||
@@ -1170,3 +1205,56 @@ 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")
|
||||
|
||||
@@ -42,6 +42,7 @@ 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",
|
||||
@@ -60,7 +61,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",
|
||||
"AI_FEATURE_ENABLED": False,
|
||||
"TRASHBIN_CUTOFF_DAYS": 30,
|
||||
"theme_customization": {},
|
||||
}
|
||||
policy_list = sorted(response.headers["Content-Security-Policy"].split("; "))
|
||||
|
||||
@@ -375,8 +375,42 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
|
||||
|
||||
document.soft_delete()
|
||||
document.refresh_from_db()
|
||||
expected_abilities["move"] = False
|
||||
assert document.get_abilities(user) == expected_abilities
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
@override_settings(
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -490,24 +490,12 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
THEME_CUSTOMIZATION_CACHE_TIMEOUT = values.Value(
|
||||
THEME_CUSTOMIZATION_CACHE_TIMEOUT = values.IntegerValue(
|
||||
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
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Breton\n"
|
||||
"Language: br_FR\n"
|
||||
@@ -50,27 +50,36 @@ msgstr "Kuzhet"
|
||||
msgid "Favorite"
|
||||
msgstr "Sinedoù"
|
||||
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
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:491 core/api/serializers.py:491
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
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:628 core/api/serializers.py:628
|
||||
#: 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
|
||||
msgid "Body"
|
||||
msgstr "Korf"
|
||||
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
msgid "Body type"
|
||||
msgstr "Doare korf"
|
||||
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
msgid "Format"
|
||||
msgstr "Stumm"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "eilenn {title}"
|
||||
@@ -225,8 +234,8 @@ msgstr "implijer"
|
||||
msgid "users"
|
||||
msgstr "implijerien"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
msgid "title"
|
||||
msgstr "titl"
|
||||
|
||||
@@ -242,174 +251,174 @@ msgstr "Restr"
|
||||
msgid "Documents"
|
||||
msgstr "Restroù"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
msgid "Untitled Document"
|
||||
msgstr "Restr hep titl"
|
||||
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} en deus rannet ur restr ganeoc'h!"
|
||||
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, 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:864 core/models.py:864
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#, 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:964 core/models.py:964
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Roud liamm ar restr/an implijer"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Roudoù liamm ar restr/an implijer"
|
||||
|
||||
#: build/lib/core/models.py:971 core/models.py:971
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
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:994 core/models.py:994
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorite"
|
||||
msgstr "Restr muiañ-karet"
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorites"
|
||||
msgstr "Restroù muiañ-karet"
|
||||
|
||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
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:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relation"
|
||||
msgstr "Liamm restr/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relations"
|
||||
msgstr "Liammoù restr/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
msgid "This user is already in this document."
|
||||
msgstr "An implijer-mañ a zo dija er restr-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
||||
#: core/models.py:1042 core/models.py:1367
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
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:1188 core/models.py:1188
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for access"
|
||||
msgstr "Goulenn tizhout ar restr"
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Goulennoù tizhout ar restr"
|
||||
|
||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
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:1260 core/models.py:1260
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#, 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:1264 core/models.py:1264
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#, 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:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#, 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:1282 core/models.py:1282
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "description"
|
||||
msgstr "deskrivadur"
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "code"
|
||||
msgstr "kod"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "public"
|
||||
msgstr "publik"
|
||||
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
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:1294 core/models.py:1294
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Template"
|
||||
msgstr "Patrom"
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Templates"
|
||||
msgstr "Patromoù"
|
||||
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relation"
|
||||
msgstr "Liamm patrom/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relations"
|
||||
msgstr "Liammoù patrom/implijer"
|
||||
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
msgid "This user is already in this template."
|
||||
msgstr "An implijer-mañ a zo dija er patrom-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Ar skipailh-mañ a zo dija er patrom-mañ."
|
||||
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "email address"
|
||||
msgstr "postel"
|
||||
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitation"
|
||||
msgstr "Pedadenn d'ur restr"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitations"
|
||||
msgstr "Pedadennoù d'ur restr"
|
||||
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
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:162
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo ar postel"
|
||||
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Digeriñ"
|
||||
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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:233
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: German\n"
|
||||
"Language: de_DE\n"
|
||||
@@ -50,27 +50,36 @@ msgstr ""
|
||||
msgid "Favorite"
|
||||
msgstr "Favorit"
|
||||
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Sie sind Besitzer eines neuen Dokuments:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
#: 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
|
||||
msgid "Body"
|
||||
msgstr "Inhalt"
|
||||
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
msgid "Body type"
|
||||
msgstr "Typ"
|
||||
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "Kopie von {title}"
|
||||
@@ -225,8 +234,8 @@ msgstr "Benutzer"
|
||||
msgid "users"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
msgid "title"
|
||||
msgstr "Titel"
|
||||
|
||||
@@ -242,174 +251,174 @@ msgstr "Dokument"
|
||||
msgid "Documents"
|
||||
msgstr "Dokumente"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
msgid "Untitled Document"
|
||||
msgstr "Unbenanntes Dokument"
|
||||
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
|
||||
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, 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:864 core/models.py:864
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#, 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:964 core/models.py:964
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Dokument/Benutzer Linkverfolgung"
|
||||
|
||||
#: build/lib/core/models.py:971 core/models.py:971
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
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:994 core/models.py:994
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorite"
|
||||
msgstr "Dokumentenfavorit"
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorites"
|
||||
msgstr "Dokumentfavoriten"
|
||||
|
||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
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:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relation"
|
||||
msgstr "Dokument/Benutzerbeziehung"
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relations"
|
||||
msgstr "Dokument/Benutzerbeziehungen"
|
||||
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
|
||||
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
|
||||
|
||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
||||
#: core/models.py:1042 core/models.py:1367
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
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:1188 core/models.py:1188
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "description"
|
||||
msgstr "Beschreibung"
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "code"
|
||||
msgstr "Code"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "css"
|
||||
msgstr "CSS"
|
||||
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "public"
|
||||
msgstr "öffentlich"
|
||||
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
|
||||
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Template"
|
||||
msgstr "Vorlage"
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Templates"
|
||||
msgstr "Vorlagen"
|
||||
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relation"
|
||||
msgstr "Vorlage/Benutzer-Beziehung"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relations"
|
||||
msgstr "Vorlage/Benutzerbeziehungen"
|
||||
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
|
||||
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Dieses Team ist bereits in diesem Template."
|
||||
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "email address"
|
||||
msgstr "E-Mail-Adresse"
|
||||
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitation"
|
||||
msgstr "Einladung zum Dokument"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitations"
|
||||
msgstr "Dokumenteinladungen"
|
||||
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
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:162
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo-E-Mail"
|
||||
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Öffnen"
|
||||
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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:233
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: English\n"
|
||||
"Language: en_US\n"
|
||||
@@ -50,27 +50,36 @@ msgstr ""
|
||||
msgid "Favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
#: 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
|
||||
msgid "Body"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
msgid "Body type"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
msgid "Format"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
@@ -225,8 +234,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -242,174 +251,174 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:971 core/models.py:971
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
||||
#: core/models.py:1042 core/models.py:1367
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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:233
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Spanish\n"
|
||||
"Language: es_ES\n"
|
||||
@@ -50,27 +50,36 @@ msgstr ""
|
||||
msgid "Favorite"
|
||||
msgstr "Favorito"
|
||||
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "¡Un nuevo documento se ha creado por ti!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
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:628 core/api/serializers.py:628
|
||||
#: 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
|
||||
msgid "Body"
|
||||
msgstr "Cuerpo"
|
||||
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
msgid "Body type"
|
||||
msgstr "Tipo de Cuerpo"
|
||||
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "copia de {title}"
|
||||
@@ -225,8 +234,8 @@ msgstr "usuario"
|
||||
msgid "users"
|
||||
msgstr "usuarios"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
msgid "title"
|
||||
msgstr "título"
|
||||
|
||||
@@ -242,174 +251,174 @@ msgstr "Documento"
|
||||
msgid "Documents"
|
||||
msgstr "Documentos"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
msgid "Untitled Document"
|
||||
msgstr "Documento sin título"
|
||||
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "¡{name} ha compartido un documento contigo!"
|
||||
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, 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:864 core/models.py:864
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} ha compartido un documento contigo: {title}"
|
||||
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Traza del enlace de documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Trazas del enlace de documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:971 core/models.py:971
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
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:994 core/models.py:994
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorite"
|
||||
msgstr "Documento favorito"
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorites"
|
||||
msgstr "Documentos favoritos"
|
||||
|
||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
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:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relation"
|
||||
msgstr "Relación documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relations"
|
||||
msgstr "Relaciones documento/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Este usuario ya forma parte del documento."
|
||||
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Este equipo ya forma parte del documento."
|
||||
|
||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
||||
#: core/models.py:1042 core/models.py:1367
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
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:1188 core/models.py:1188
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for access"
|
||||
msgstr "Solicitud de acceso"
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Solicitud de accesos"
|
||||
|
||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
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:1260 core/models.py:1260
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "¡{name} desea acceder a un documento!"
|
||||
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} desea acceso al siguiente documento:"
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#, 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:1282 core/models.py:1282
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "description"
|
||||
msgstr "descripción"
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "code"
|
||||
msgstr "código"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "public"
|
||||
msgstr "público"
|
||||
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
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:1294 core/models.py:1294
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Template"
|
||||
msgstr "Plantilla"
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Templates"
|
||||
msgstr "Plantillas"
|
||||
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relation"
|
||||
msgstr "Relación plantilla/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relations"
|
||||
msgstr "Relaciones plantilla/usuario"
|
||||
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Este usuario ya forma parte de la plantilla."
|
||||
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Este equipo ya se encuentra en esta plantilla."
|
||||
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "email address"
|
||||
msgstr "dirección de correo electrónico"
|
||||
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitation"
|
||||
msgstr "Invitación al documento"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitations"
|
||||
msgstr "Invitaciones a documentos"
|
||||
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
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:162
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo de correo electrónico"
|
||||
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Abrir"
|
||||
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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:233
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: French\n"
|
||||
"Language: fr_FR\n"
|
||||
@@ -50,27 +50,36 @@ msgstr "Masqué"
|
||||
msgid "Favorite"
|
||||
msgstr "Favoris"
|
||||
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Un nouveau document a été créé pour vous !"
|
||||
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
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:628 core/api/serializers.py:628
|
||||
#: 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
|
||||
msgid "Body"
|
||||
msgstr "Corps"
|
||||
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
msgid "Body type"
|
||||
msgstr "Type de corps"
|
||||
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "copie de {title}"
|
||||
@@ -225,8 +234,8 @@ msgstr "utilisateur"
|
||||
msgid "users"
|
||||
msgstr "utilisateurs"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
msgid "title"
|
||||
msgstr "titre"
|
||||
|
||||
@@ -242,174 +251,174 @@ msgstr "Document"
|
||||
msgid "Documents"
|
||||
msgstr "Documents"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
msgid "Untitled Document"
|
||||
msgstr "Document sans titre"
|
||||
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} a partagé un document avec vous!"
|
||||
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, 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:864 core/models.py:864
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#, 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:964 core/models.py:964
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Trace du lien document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Traces du lien document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:971 core/models.py:971
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
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:994 core/models.py:994
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorite"
|
||||
msgstr "Document favori"
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorites"
|
||||
msgstr "Documents favoris"
|
||||
|
||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
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:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relation"
|
||||
msgstr "Relation document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relations"
|
||||
msgstr "Relations document/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Cet utilisateur est déjà dans ce document."
|
||||
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Cette équipe est déjà dans ce document."
|
||||
|
||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
||||
#: core/models.py:1042 core/models.py:1367
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
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:1188 core/models.py:1188
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for access"
|
||||
msgstr "Demande d'accès au document"
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Demande d'accès au document"
|
||||
|
||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
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:1260 core/models.py:1260
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} souhaiterait accéder au document suivant !"
|
||||
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#, 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:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#, 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:1282 core/models.py:1282
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "description"
|
||||
msgstr "description"
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "css"
|
||||
msgstr "CSS"
|
||||
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "public"
|
||||
msgstr "public"
|
||||
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
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:1294 core/models.py:1294
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Template"
|
||||
msgstr "Modèle"
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Templates"
|
||||
msgstr "Modèles"
|
||||
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relation"
|
||||
msgstr "Relation modèle/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relations"
|
||||
msgstr "Relations modèle/utilisateur"
|
||||
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Cet utilisateur est déjà dans ce modèle."
|
||||
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Cette équipe est déjà modèle."
|
||||
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "email address"
|
||||
msgstr "adresse e-mail"
|
||||
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitation"
|
||||
msgstr "Invitation à un document"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitations"
|
||||
msgstr "Invitations à un document"
|
||||
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
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:162
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo de l'e-mail"
|
||||
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Ouvrir"
|
||||
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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:233
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Italian\n"
|
||||
"Language: it_IT\n"
|
||||
@@ -50,27 +50,36 @@ msgstr ""
|
||||
msgid "Favorite"
|
||||
msgstr "Preferiti"
|
||||
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Un nuovo documento è stato creato a tuo nome!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Sei ora proprietario di un nuovo documento:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
#: 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
|
||||
msgid "Body"
|
||||
msgstr "Corpo"
|
||||
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
msgid "Body type"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "copia di {title}"
|
||||
@@ -225,8 +234,8 @@ msgstr "utente"
|
||||
msgid "users"
|
||||
msgstr "utenti"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
msgid "title"
|
||||
msgstr "titolo"
|
||||
|
||||
@@ -242,174 +251,174 @@ msgstr "Documento"
|
||||
msgid "Documents"
|
||||
msgstr "Documenti"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
msgid "Untitled Document"
|
||||
msgstr "Documento senza titolo"
|
||||
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} ha condiviso un documento con te!"
|
||||
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, 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:864 core/models.py:864
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#, 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:964 core/models.py:964
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:971 core/models.py:971
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorite"
|
||||
msgstr "Documento preferito"
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorites"
|
||||
msgstr "Documenti preferiti"
|
||||
|
||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Questo utente è già presente in questo documento."
|
||||
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Questo team è già presente in questo documento."
|
||||
|
||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
||||
#: core/models.py:1042 core/models.py:1367
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "description"
|
||||
msgstr "descrizione"
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "public"
|
||||
msgstr "pubblico"
|
||||
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Indica se questo modello è pubblico per chiunque."
|
||||
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Template"
|
||||
msgstr "Modello"
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Templates"
|
||||
msgstr "Modelli"
|
||||
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Questo utente è già in questo modello."
|
||||
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Questo team è già in questo modello."
|
||||
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "email address"
|
||||
msgstr "indirizzo e-mail"
|
||||
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitation"
|
||||
msgstr "Invito al documento"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitations"
|
||||
msgstr "Inviti al documento"
|
||||
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
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:162
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo e-mail"
|
||||
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Apri"
|
||||
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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:233
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Dutch\n"
|
||||
"Language: nl_NL\n"
|
||||
@@ -44,33 +44,42 @@ msgstr "Ik ben Eigenaar"
|
||||
|
||||
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
|
||||
msgid "Masked"
|
||||
msgstr ""
|
||||
msgstr "Gemaskeerd"
|
||||
|
||||
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
|
||||
msgid "Favorite"
|
||||
msgstr "Favoriete"
|
||||
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Een nieuw document was gecreëerd voor u!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "U heeft eigenaarschap van een nieuw document:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
#: 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
|
||||
msgid "Body"
|
||||
msgstr "Text"
|
||||
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
msgid "Body type"
|
||||
msgstr "Text type"
|
||||
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
msgid "Format"
|
||||
msgstr "Formaat"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "kopie van {title}"
|
||||
@@ -163,7 +172,7 @@ msgstr "id"
|
||||
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Required. 255 characters or fewer. ASCII characters only."
|
||||
msgstr ""
|
||||
msgstr "Vereist. Minder dan 255 ASCII tekens."
|
||||
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "full name"
|
||||
@@ -225,8 +234,8 @@ msgstr "gebruiker"
|
||||
msgid "users"
|
||||
msgstr "gebruikers"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
msgid "title"
|
||||
msgstr "titel"
|
||||
|
||||
@@ -242,174 +251,174 @@ msgstr "Document"
|
||||
msgid "Documents"
|
||||
msgstr "Documenten"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
msgid "Untitled Document"
|
||||
msgstr "Naamloos Document"
|
||||
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} heeft een document met gedeeld!"
|
||||
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, 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:864 core/models.py:864
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#, 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:964 core/models.py:964
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Document/gebruiker url"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Document/gebruiker url"
|
||||
|
||||
#: build/lib/core/models.py:971 core/models.py:971
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Een url bestaat al voor dit document/deze gebruiker."
|
||||
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorite"
|
||||
msgstr "Document favoriet"
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorites"
|
||||
msgstr "Document favorieten"
|
||||
|
||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Dit document is al in gebruik als favoriete door dezelfde gebruiker."
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relation"
|
||||
msgstr "Document/gebruiker relatie"
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relations"
|
||||
msgstr "Document/gebruiker relaties"
|
||||
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
msgid "This user is already in this document."
|
||||
msgstr "De gebruiker is al in dit document."
|
||||
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Het team is al in dit document."
|
||||
|
||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
||||
#: core/models.py:1042 core/models.py:1367
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
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:1188 core/models.py:1188
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for access"
|
||||
msgstr "Document verzoekt om toegang"
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
msgstr "Document verzoekt om toegangen"
|
||||
|
||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
msgstr "Deze gebruiker heeft al om toegang tot dit document gevraagd."
|
||||
|
||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
msgstr "{name} wenst toegang tot een document!"
|
||||
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
msgstr "{name} wenst toegang tot het volgende document:"
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
msgstr "{name} vraagt toegang tot het document: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "description"
|
||||
msgstr "omschrijving"
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "code"
|
||||
msgstr "code"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "public"
|
||||
msgstr "publiek"
|
||||
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Of dit template als publiek is en door iedereen te gebruiken is."
|
||||
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Template"
|
||||
msgstr "Template"
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Templates"
|
||||
msgstr "Templates"
|
||||
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relation"
|
||||
msgstr "Template/gebruiker relatie"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relations"
|
||||
msgstr "Template/gebruiker relaties"
|
||||
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
msgid "This user is already in this template."
|
||||
msgstr "De gebruiker bestaat al in dit template."
|
||||
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Het team bestaat al in dit template."
|
||||
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "email address"
|
||||
msgstr "email adres"
|
||||
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitation"
|
||||
msgstr "Document uitnodiging"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitations"
|
||||
msgstr "Document uitnodigingen"
|
||||
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
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:162
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logo email"
|
||||
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Open"
|
||||
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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:233
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Portuguese\n"
|
||||
"Language: pt_PT\n"
|
||||
@@ -50,27 +50,36 @@ msgstr ""
|
||||
msgid "Favorite"
|
||||
msgstr "Favorito"
|
||||
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Um novo documento foi criado em seu nome!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
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:628 core/api/serializers.py:628
|
||||
#: 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
|
||||
msgid "Body"
|
||||
msgstr "Corpo"
|
||||
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
msgid "Body type"
|
||||
msgstr "Tipo de corpo"
|
||||
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
msgid "Format"
|
||||
msgstr "Formato"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "cópia de {title}"
|
||||
@@ -225,8 +234,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -242,174 +251,174 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:971 core/models.py:971
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
||||
#: core/models.py:1042 core/models.py:1367
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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:233
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Russian\n"
|
||||
"Language: ru_RU\n"
|
||||
@@ -50,27 +50,36 @@ msgstr "Скрытый"
|
||||
msgid "Favorite"
|
||||
msgstr "Избранное"
|
||||
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Новый документ был создан от вашего имени!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Вы назначены владельцем для нового документа:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
#: 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
|
||||
msgid "Body"
|
||||
msgstr "Текст сообщения"
|
||||
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
msgid "Body type"
|
||||
msgstr "Тип сообщения"
|
||||
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
msgid "Format"
|
||||
msgstr "Формат"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "копия {title}"
|
||||
@@ -225,8 +234,8 @@ msgstr "пользователь"
|
||||
msgid "users"
|
||||
msgstr "пользователи"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
msgid "title"
|
||||
msgstr "заголовок"
|
||||
|
||||
@@ -242,174 +251,174 @@ msgstr "Документ"
|
||||
msgid "Documents"
|
||||
msgstr "Документы"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
msgid "Untitled Document"
|
||||
msgstr "Безымянный документ"
|
||||
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} делится с вами документом!"
|
||||
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} приглашает вас присоединиться к следующему документу с ролью \"{role}\":"
|
||||
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} делится с вами документом: {title}"
|
||||
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Трассировка связи документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Трассировка связей документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:971 core/models.py:971
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Для этого документа/пользователя уже существует трассировка ссылки."
|
||||
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorite"
|
||||
msgstr "Избранный документ"
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorites"
|
||||
msgstr "Избранные документы"
|
||||
|
||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Этот документ уже помечен как избранный для этого пользователя."
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relation"
|
||||
msgstr "Отношение документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relations"
|
||||
msgstr "Отношения документ/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Этот пользователь уже имеет доступ к этому документу."
|
||||
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Эта команда уже имеет доступ к этому документу."
|
||||
|
||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
||||
#: core/models.py:1042 core/models.py:1367
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
|
||||
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for access"
|
||||
msgstr "Документ запрашивает доступ"
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Документ запрашивает доступы"
|
||||
|
||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr "Этот пользователь уже запросил доступ к этому документу."
|
||||
|
||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} хочет получить доступ к документу!"
|
||||
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} хочет получить доступ к следующему документу:"
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name} запрашивает доступ к документу: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "description"
|
||||
msgstr "описание"
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "code"
|
||||
msgstr "код"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "public"
|
||||
msgstr "доступно всем"
|
||||
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Этот шаблон доступен всем пользователям."
|
||||
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Template"
|
||||
msgstr "Шаблон"
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Templates"
|
||||
msgstr "Шаблоны"
|
||||
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relation"
|
||||
msgstr "Отношение шаблон/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relations"
|
||||
msgstr "Отношения шаблон/пользователь"
|
||||
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Этот пользователь уже указан в этом шаблоне."
|
||||
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Эта команда уже указана в этом шаблоне."
|
||||
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "email address"
|
||||
msgstr "адрес электронной почты"
|
||||
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitation"
|
||||
msgstr "Приглашение для документа"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitations"
|
||||
msgstr "Приглашения для документов"
|
||||
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Этот адрес уже связан с зарегистрированным пользователем."
|
||||
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Логотип email"
|
||||
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Открыть"
|
||||
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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:233
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Slovenian\n"
|
||||
"Language: sl_SI\n"
|
||||
@@ -50,27 +50,36 @@ msgstr ""
|
||||
msgid "Favorite"
|
||||
msgstr "Priljubljena"
|
||||
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
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:491 core/api/serializers.py:491
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
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:628 core/api/serializers.py:628
|
||||
#: 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
|
||||
msgid "Body"
|
||||
msgstr "Telo"
|
||||
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
msgid "Body type"
|
||||
msgstr "Vrsta telesa"
|
||||
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
msgid "Format"
|
||||
msgstr "Oblika"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
@@ -225,8 +234,8 @@ msgstr "uporabnik"
|
||||
msgid "users"
|
||||
msgstr "uporabniki"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
msgid "title"
|
||||
msgstr "naslov"
|
||||
|
||||
@@ -242,174 +251,174 @@ msgstr "Dokument"
|
||||
msgid "Documents"
|
||||
msgstr "Dokumenti"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
msgid "Untitled Document"
|
||||
msgstr "Dokument brez naslova"
|
||||
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} je delil dokument z vami!"
|
||||
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, 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:864 core/models.py:864
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} je delil dokument z vami: {title}"
|
||||
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Dokument/sled povezave uporabnika"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Sledi povezav dokumenta/uporabnika"
|
||||
|
||||
#: build/lib/core/models.py:971 core/models.py:971
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
|
||||
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorite"
|
||||
msgstr "Priljubljeni dokument"
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorites"
|
||||
msgstr "Priljubljeni dokumenti"
|
||||
|
||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
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:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relation"
|
||||
msgstr "Odnos dokument/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relations"
|
||||
msgstr "Odnosi dokument/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Ta uporabnik je že v tem dokumentu."
|
||||
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Ta ekipa je že v tem dokumentu."
|
||||
|
||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
||||
#: core/models.py:1042 core/models.py:1367
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
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:1188 core/models.py:1188
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "description"
|
||||
msgstr "opis"
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "code"
|
||||
msgstr "koda"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "public"
|
||||
msgstr "javno"
|
||||
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Ali je ta predloga javna za uporabo."
|
||||
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Template"
|
||||
msgstr "Predloga"
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Templates"
|
||||
msgstr "Predloge"
|
||||
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relation"
|
||||
msgstr "Odnos predloga/uporabnik"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relations"
|
||||
msgstr "Odnosi med predlogo in uporabnikom"
|
||||
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Ta uporabnik je že v tej predlogi."
|
||||
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Ta ekipa je že v tej predlogi."
|
||||
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "email address"
|
||||
msgstr "elektronski naslov"
|
||||
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitation"
|
||||
msgstr "Vabilo na dokument"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitations"
|
||||
msgstr "Vabila na dokument"
|
||||
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
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:162
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "E-pošta z logotipom"
|
||||
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Odpri"
|
||||
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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:233
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Swedish\n"
|
||||
"Language: sv_SE\n"
|
||||
@@ -50,27 +50,36 @@ msgstr ""
|
||||
msgid "Favorite"
|
||||
msgstr "Favoriter"
|
||||
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Ett nytt dokument skapades åt dig!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
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:628 core/api/serializers.py:628
|
||||
#: 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
|
||||
msgid "Body"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
msgid "Body type"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
msgid "Format"
|
||||
msgstr "Format"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
@@ -225,8 +234,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -242,174 +251,174 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:971 core/models.py:971
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
||||
#: core/models.py:1042 core/models.py:1367
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "email address"
|
||||
msgstr "e-postadress"
|
||||
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitation"
|
||||
msgstr "Bjud in dokument"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitations"
|
||||
msgstr "Inbjudningar dokument"
|
||||
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
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:162
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Logotyp e-post"
|
||||
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Öppna"
|
||||
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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:233
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Turkish\n"
|
||||
"Language: tr_TR\n"
|
||||
@@ -50,27 +50,36 @@ msgstr ""
|
||||
msgid "Favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
#: 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
|
||||
msgid "Body"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
msgid "Body type"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
msgid "Format"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr ""
|
||||
@@ -225,8 +234,8 @@ msgstr ""
|
||||
msgid "users"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
msgid "title"
|
||||
msgstr ""
|
||||
|
||||
@@ -242,174 +251,174 @@ msgstr ""
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
msgid "Untitled Document"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link trace"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link traces"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:971 core/models.py:971
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorite"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorites"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
msgid "This user is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
msgid "This team is already in this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
||||
#: core/models.py:1042 core/models.py:1367
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "code"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "css"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "public"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
msgid "This user is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
msgid "This team is already in this template."
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "email address"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitation"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitations"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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:233
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Ukrainian\n"
|
||||
"Language: uk_UA\n"
|
||||
@@ -50,27 +50,36 @@ msgstr "Приховано"
|
||||
msgid "Favorite"
|
||||
msgstr "Обране"
|
||||
|
||||
#: build/lib/core/api/serializers.py:487 core/api/serializers.py:487
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "Новий документ був створений від вашого імені!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "Ви тепер є власником нового документа:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
#: 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
|
||||
msgid "Body"
|
||||
msgstr "Вміст"
|
||||
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
msgid "Body type"
|
||||
msgstr "Тип вмісту"
|
||||
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
msgid "Format"
|
||||
msgstr "Формат"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "копія {title}"
|
||||
@@ -225,8 +234,8 @@ msgstr "користувач"
|
||||
msgid "users"
|
||||
msgstr "користувачі"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
msgid "title"
|
||||
msgstr "заголовок"
|
||||
|
||||
@@ -242,174 +251,174 @@ msgstr "Документ"
|
||||
msgid "Documents"
|
||||
msgstr "Документи"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
msgid "Untitled Document"
|
||||
msgstr "Документ без назви"
|
||||
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} ділиться з вами документом!"
|
||||
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} запрошує вас для роботи з документом із роллю \"{role}\":"
|
||||
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} ділиться з вами документом: {title}"
|
||||
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link trace"
|
||||
msgstr "Трасування посилання Документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link traces"
|
||||
msgstr "Трасування посилань Документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:971 core/models.py:971
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "Відстеження вже існуючих посилань для цього документа/користувача."
|
||||
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorite"
|
||||
msgstr "Обраний документ"
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorites"
|
||||
msgstr "Обрані документи"
|
||||
|
||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "Цей документ вже вказаний як обраний для одного користувача."
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relation"
|
||||
msgstr "Відносини документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relations"
|
||||
msgstr "Відносини документ/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
msgid "This user is already in this document."
|
||||
msgstr "Цей користувач вже має доступ до цього документу."
|
||||
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
msgid "This team is already in this document."
|
||||
msgstr "Ця команда вже має доступ до цього документа."
|
||||
|
||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
||||
#: core/models.py:1042 core/models.py:1367
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Вкажіть користувача або команду, а не обох."
|
||||
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for access"
|
||||
msgstr "Запит доступу до документа"
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for accesses"
|
||||
msgstr "Запит доступу для документа"
|
||||
|
||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr "Цей користувач вже попросив доступ до цього документа."
|
||||
|
||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr "{name} хоче отримати доступ до документа!"
|
||||
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr "{name} бажає отримати доступ до наступного документа:"
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr "{name} запитує доступ до документа: {title}"
|
||||
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "description"
|
||||
msgstr "опис"
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "code"
|
||||
msgstr "код"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "public"
|
||||
msgstr "публічне"
|
||||
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "Чи є цей шаблон публічним для будь-кого користувача."
|
||||
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Template"
|
||||
msgstr "Шаблон"
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Templates"
|
||||
msgstr "Шаблони"
|
||||
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relation"
|
||||
msgstr "Відношення шаблон/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relations"
|
||||
msgstr "Відношення шаблон/користувач"
|
||||
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
msgid "This user is already in this template."
|
||||
msgstr "Цей користувач вже має доступ до цього шаблону."
|
||||
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
msgid "This team is already in this template."
|
||||
msgstr "Ця команда вже має доступ до цього шаблону."
|
||||
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "email address"
|
||||
msgstr "електронна адреса"
|
||||
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitation"
|
||||
msgstr "Запрошення до редагування документа"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitations"
|
||||
msgstr "Запрошення до редагування документів"
|
||||
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."
|
||||
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "Логотип пошти"
|
||||
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "Відкрити"
|
||||
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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:233
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -2,8 +2,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: lasuite-docs\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-10 14:29+0000\n"
|
||||
"PO-Revision-Date: 2025-09-12 09:50\n"
|
||||
"POT-Creation-Date: 2025-10-14 07:19+0000\n"
|
||||
"PO-Revision-Date: 2025-10-14 13:09\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Chinese Simplified\n"
|
||||
"Language: zh_CN\n"
|
||||
@@ -44,33 +44,42 @@ 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:487 core/api/serializers.py:487
|
||||
#: build/lib/core/api/serializers.py:496 core/api/serializers.py:496
|
||||
msgid "A new document was created on your behalf!"
|
||||
msgstr "已为您创建了一份新文档!"
|
||||
|
||||
#: build/lib/core/api/serializers.py:491 core/api/serializers.py:491
|
||||
#: build/lib/core/api/serializers.py:500 core/api/serializers.py:500
|
||||
msgid "You have been granted ownership of a new document:"
|
||||
msgstr "您已被授予新文档的所有权:"
|
||||
|
||||
#: build/lib/core/api/serializers.py:628 core/api/serializers.py:628
|
||||
#: 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
|
||||
msgid "Body"
|
||||
msgstr "正文"
|
||||
|
||||
#: build/lib/core/api/serializers.py:631 core/api/serializers.py:631
|
||||
#: build/lib/core/api/serializers.py:696 core/api/serializers.py:696
|
||||
msgid "Body type"
|
||||
msgstr "正文类型"
|
||||
|
||||
#: build/lib/core/api/serializers.py:637 core/api/serializers.py:637
|
||||
#: build/lib/core/api/serializers.py:702 core/api/serializers.py:702
|
||||
msgid "Format"
|
||||
msgstr "格式"
|
||||
|
||||
#: build/lib/core/api/viewsets.py:965 core/api/viewsets.py:965
|
||||
#: build/lib/core/api/viewsets.py:983 core/api/viewsets.py:983
|
||||
#, python-brace-format
|
||||
msgid "copy of {title}"
|
||||
msgstr "{title} 的副本"
|
||||
@@ -163,7 +172,7 @@ msgstr "sub"
|
||||
|
||||
#: build/lib/core/models.py:142 core/models.py:142
|
||||
msgid "Required. 255 characters or fewer. ASCII characters only."
|
||||
msgstr ""
|
||||
msgstr "必填项。限255个字符以内。仅支持ASCII字符。"
|
||||
|
||||
#: build/lib/core/models.py:150 core/models.py:150
|
||||
msgid "full name"
|
||||
@@ -225,8 +234,8 @@ msgstr "用户"
|
||||
msgid "users"
|
||||
msgstr "个用户"
|
||||
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1281
|
||||
#: core/models.py:359 core/models.py:1281
|
||||
#: build/lib/core/models.py:359 build/lib/core/models.py:1282
|
||||
#: core/models.py:359 core/models.py:1282
|
||||
msgid "title"
|
||||
msgstr "标题"
|
||||
|
||||
@@ -242,174 +251,174 @@ msgstr "文档"
|
||||
msgid "Documents"
|
||||
msgstr "个文档"
|
||||
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:819 core/models.py:422
|
||||
#: core/models.py:819
|
||||
#: build/lib/core/models.py:422 build/lib/core/models.py:820 core/models.py:422
|
||||
#: core/models.py:820
|
||||
msgid "Untitled Document"
|
||||
msgstr "未命名文档"
|
||||
|
||||
#: build/lib/core/models.py:854 core/models.py:854
|
||||
#: build/lib/core/models.py:855 core/models.py:855
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you!"
|
||||
msgstr "{name} 与您共享了一个文档!"
|
||||
|
||||
#: build/lib/core/models.py:858 core/models.py:858
|
||||
#: build/lib/core/models.py:859 core/models.py:859
|
||||
#, python-brace-format
|
||||
msgid "{name} invited you with the role \"{role}\" on the following document:"
|
||||
msgstr "{name} 邀请您以“{role}”角色访问以下文档:"
|
||||
|
||||
#: build/lib/core/models.py:864 core/models.py:864
|
||||
#: build/lib/core/models.py:865 core/models.py:865
|
||||
#, python-brace-format
|
||||
msgid "{name} shared a document with you: {title}"
|
||||
msgstr "{name} 与您共享了一个文档:{title}"
|
||||
|
||||
#: build/lib/core/models.py:964 core/models.py:964
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
msgid "Document/user link trace"
|
||||
msgstr "文档/用户链接跟踪"
|
||||
|
||||
#: build/lib/core/models.py:965 core/models.py:965
|
||||
#: build/lib/core/models.py:966 core/models.py:966
|
||||
msgid "Document/user link traces"
|
||||
msgstr "个文档/用户链接跟踪"
|
||||
|
||||
#: build/lib/core/models.py:971 core/models.py:971
|
||||
#: build/lib/core/models.py:972 core/models.py:972
|
||||
msgid "A link trace already exists for this document/user."
|
||||
msgstr "此文档/用户的链接跟踪已存在。"
|
||||
|
||||
#: build/lib/core/models.py:994 core/models.py:994
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
msgid "Document favorite"
|
||||
msgstr "文档收藏"
|
||||
|
||||
#: build/lib/core/models.py:995 core/models.py:995
|
||||
#: build/lib/core/models.py:996 core/models.py:996
|
||||
msgid "Document favorites"
|
||||
msgstr "文档收藏夹"
|
||||
|
||||
#: build/lib/core/models.py:1001 core/models.py:1001
|
||||
#: build/lib/core/models.py:1002 core/models.py:1002
|
||||
msgid "This document is already targeted by a favorite relation instance for the same user."
|
||||
msgstr "该文档已被同一用户的收藏关系实例关联。"
|
||||
|
||||
#: build/lib/core/models.py:1023 core/models.py:1023
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
msgid "Document/user relation"
|
||||
msgstr "文档/用户关系"
|
||||
|
||||
#: build/lib/core/models.py:1024 core/models.py:1024
|
||||
#: build/lib/core/models.py:1025 core/models.py:1025
|
||||
msgid "Document/user relations"
|
||||
msgstr "文档/用户关系集"
|
||||
|
||||
#: build/lib/core/models.py:1030 core/models.py:1030
|
||||
#: build/lib/core/models.py:1031 core/models.py:1031
|
||||
msgid "This user is already in this document."
|
||||
msgstr "该用户已在此文档中。"
|
||||
|
||||
#: build/lib/core/models.py:1036 core/models.py:1036
|
||||
#: build/lib/core/models.py:1037 core/models.py:1037
|
||||
msgid "This team is already in this document."
|
||||
msgstr "该团队已在此文档中。"
|
||||
|
||||
#: build/lib/core/models.py:1042 build/lib/core/models.py:1367
|
||||
#: core/models.py:1042 core/models.py:1367
|
||||
#: build/lib/core/models.py:1043 build/lib/core/models.py:1368
|
||||
#: core/models.py:1043 core/models.py:1368
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "必须设置用户或团队之一,不能同时设置两者。"
|
||||
|
||||
#: build/lib/core/models.py:1188 core/models.py:1188
|
||||
msgid "Document ask for access"
|
||||
msgstr ""
|
||||
|
||||
#: build/lib/core/models.py:1189 core/models.py:1189
|
||||
msgid "Document ask for access"
|
||||
msgstr "文档需要访问权限"
|
||||
|
||||
#: build/lib/core/models.py:1190 core/models.py:1190
|
||||
msgid "Document ask for accesses"
|
||||
msgstr ""
|
||||
msgstr "文档需要访问权限"
|
||||
|
||||
#: build/lib/core/models.py:1195 core/models.py:1195
|
||||
#: build/lib/core/models.py:1196 core/models.py:1196
|
||||
msgid "This user has already asked for access to this document."
|
||||
msgstr ""
|
||||
msgstr "用户已申请该文档的访问权限。"
|
||||
|
||||
#: build/lib/core/models.py:1260 core/models.py:1260
|
||||
#: build/lib/core/models.py:1261 core/models.py:1261
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to a document!"
|
||||
msgstr ""
|
||||
msgstr "{name} 申请访问文档!"
|
||||
|
||||
#: build/lib/core/models.py:1264 core/models.py:1264
|
||||
#: build/lib/core/models.py:1265 core/models.py:1265
|
||||
#, python-brace-format
|
||||
msgid "{name} would like access to the following document:"
|
||||
msgstr ""
|
||||
msgstr "{name} 申请访问以下文档:"
|
||||
|
||||
#: build/lib/core/models.py:1270 core/models.py:1270
|
||||
#: build/lib/core/models.py:1271 core/models.py:1271
|
||||
#, python-brace-format
|
||||
msgid "{name} is asking for access to the document: {title}"
|
||||
msgstr ""
|
||||
msgstr "{name}申请文档:{title}的访问权限"
|
||||
|
||||
#: build/lib/core/models.py:1282 core/models.py:1282
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
msgid "description"
|
||||
msgstr "说明"
|
||||
|
||||
#: build/lib/core/models.py:1283 core/models.py:1283
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
msgid "code"
|
||||
msgstr "代码"
|
||||
|
||||
#: build/lib/core/models.py:1284 core/models.py:1284
|
||||
#: build/lib/core/models.py:1285 core/models.py:1285
|
||||
msgid "css"
|
||||
msgstr "css"
|
||||
|
||||
#: build/lib/core/models.py:1286 core/models.py:1286
|
||||
#: build/lib/core/models.py:1287 core/models.py:1287
|
||||
msgid "public"
|
||||
msgstr "公开"
|
||||
|
||||
#: build/lib/core/models.py:1288 core/models.py:1288
|
||||
#: build/lib/core/models.py:1289 core/models.py:1289
|
||||
msgid "Whether this template is public for anyone to use."
|
||||
msgstr "该模板是否公开供任何人使用。"
|
||||
|
||||
#: build/lib/core/models.py:1294 core/models.py:1294
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
msgid "Template"
|
||||
msgstr "模板"
|
||||
|
||||
#: build/lib/core/models.py:1295 core/models.py:1295
|
||||
#: build/lib/core/models.py:1296 core/models.py:1296
|
||||
msgid "Templates"
|
||||
msgstr "模板"
|
||||
|
||||
#: build/lib/core/models.py:1348 core/models.py:1348
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
msgid "Template/user relation"
|
||||
msgstr "模板/用户关系"
|
||||
|
||||
#: build/lib/core/models.py:1349 core/models.py:1349
|
||||
#: build/lib/core/models.py:1350 core/models.py:1350
|
||||
msgid "Template/user relations"
|
||||
msgstr "模板/用户关系集"
|
||||
|
||||
#: build/lib/core/models.py:1355 core/models.py:1355
|
||||
#: build/lib/core/models.py:1356 core/models.py:1356
|
||||
msgid "This user is already in this template."
|
||||
msgstr "该用户已在此模板中。"
|
||||
|
||||
#: build/lib/core/models.py:1361 core/models.py:1361
|
||||
#: build/lib/core/models.py:1362 core/models.py:1362
|
||||
msgid "This team is already in this template."
|
||||
msgstr "该团队已在此模板中。"
|
||||
|
||||
#: build/lib/core/models.py:1438 core/models.py:1438
|
||||
#: build/lib/core/models.py:1439 core/models.py:1439
|
||||
msgid "email address"
|
||||
msgstr "电子邮件地址"
|
||||
|
||||
#: build/lib/core/models.py:1457 core/models.py:1457
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
msgid "Document invitation"
|
||||
msgstr "文档邀请"
|
||||
|
||||
#: build/lib/core/models.py:1458 core/models.py:1458
|
||||
#: build/lib/core/models.py:1459 core/models.py:1459
|
||||
msgid "Document invitations"
|
||||
msgstr "文档邀请"
|
||||
|
||||
#: build/lib/core/models.py:1478 core/models.py:1478
|
||||
#: build/lib/core/models.py:1479 core/models.py:1479
|
||||
msgid "This email is already associated to a registered user."
|
||||
msgstr "此电子邮件已经与现有注册用户关联。"
|
||||
|
||||
#: core/templates/mail/html/template.html:162
|
||||
#: core/templates/mail/html/template.html:153
|
||||
#: core/templates/mail/text/template.txt:3
|
||||
msgid "Logo email"
|
||||
msgstr "徽标邮件"
|
||||
|
||||
#: core/templates/mail/html/template.html:209
|
||||
#: core/templates/mail/html/template.html:200
|
||||
#: core/templates/mail/text/template.txt:10
|
||||
msgid "Open"
|
||||
msgstr "打开"
|
||||
|
||||
#: core/templates/mail/html/template.html:226
|
||||
#: core/templates/mail/html/template.html:217
|
||||
#: 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:233
|
||||
#: core/templates/mail/html/template.html:224
|
||||
#: core/templates/mail/text/template.txt:16
|
||||
#, python-format
|
||||
msgid " Brought to you by %(brandname)s "
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "impress"
|
||||
version = "3.7.0"
|
||||
version = "3.8.21"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -760,7 +760,7 @@ test.describe('Doc Editor', () => {
|
||||
// Wait for the interlink to be created and rendered
|
||||
const editor = page.locator('.ProseMirror.bn-editor');
|
||||
|
||||
const interlink = editor.getByRole('link', {
|
||||
const interlink = editor.getByRole('button', {
|
||||
name: docChild2,
|
||||
});
|
||||
|
||||
@@ -850,7 +850,6 @@ test.describe('Doc Editor', () => {
|
||||
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
|
||||
|
||||
@@ -472,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('link', {
|
||||
const interlink = editor.getByRole('button', {
|
||||
name: randomDoc,
|
||||
});
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ test.describe('Document grid item options', () => {
|
||||
const row = await getGridRow(page, docTitle);
|
||||
await row.getByText(`more_horiz`).click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Delete a doc' }),
|
||||
|
||||
@@ -96,7 +96,7 @@ test.describe('Doc Header', () => {
|
||||
page.getByRole('heading', { name: 'Delete a doc' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByText(`This document and any sub-`)).toBeVisible();
|
||||
await expect(page.getByText(`This document will be`)).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
|
||||
@@ -202,9 +202,19 @@ 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',
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc, goToGridDoc, verifyDocName } from './utils-common';
|
||||
import { createDoc, verifyDocName } from './utils-common';
|
||||
import { addNewMember } from './utils-share';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -8,8 +8,14 @@ test.beforeEach(async ({ page }) => {
|
||||
});
|
||||
|
||||
test.describe('Document list members', () => {
|
||||
test('it checks a big list of members', async ({ page }) => {
|
||||
const docTitle = await goToGridDoc(page);
|
||||
test('it checks a big list of members', async ({ page, browserName }) => {
|
||||
const [docTitle] = await createDoc(
|
||||
page,
|
||||
'members-big-members-list',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
|
||||
await verifyDocName(page, docTitle);
|
||||
|
||||
// Get the current URL and extract the last part
|
||||
@@ -73,7 +79,7 @@ test.describe('Document list members', () => {
|
||||
await expect(loadMore).toBeHidden();
|
||||
});
|
||||
|
||||
test('it checks a big list of invitations', async ({ page }) => {
|
||||
test('it checks a big list of invitations', async ({ page, browserName }) => {
|
||||
await page.route(
|
||||
/.*\/documents\/.*\/invitations\/\?page=.*/,
|
||||
async (route) => {
|
||||
@@ -108,7 +114,12 @@ test.describe('Document list members', () => {
|
||||
},
|
||||
);
|
||||
|
||||
const docTitle = await goToGridDoc(page);
|
||||
const [docTitle] = await createDoc(
|
||||
page,
|
||||
'members-big-invitation-list',
|
||||
browserName,
|
||||
1,
|
||||
);
|
||||
await verifyDocName(page, docTitle);
|
||||
await page.getByRole('button', { name: 'Share' }).click();
|
||||
|
||||
|
||||
@@ -60,35 +60,30 @@ test.describe('Doc Routing', () => {
|
||||
|
||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||
|
||||
// Wait for the doc link (via its dynamic title) to be visible
|
||||
const docLink = page.getByRole('link', { name: docTitle });
|
||||
await expect(docLink).toBeVisible();
|
||||
const responsePromise = page.route(
|
||||
/.*\/documents\/.*\/$|users\/me\/$/,
|
||||
async (route) => {
|
||||
const request = route.request();
|
||||
|
||||
// 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()),
|
||||
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();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await docLink.click();
|
||||
await wait401;
|
||||
await page.getByRole('link', { name: '401-doc-parent' }).click();
|
||||
|
||||
await responsePromise;
|
||||
|
||||
await expect(page.getByText('Log in to access the document.')).toBeVisible({
|
||||
timeout: 10000,
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
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.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();
|
||||
});
|
||||
});
|
||||
@@ -32,6 +32,15 @@ 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('/');
|
||||
|
||||
@@ -98,6 +107,15 @@ 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('/');
|
||||
@@ -131,3 +149,27 @@ 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: 'Gouvernement Logo',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
const header = page.locator('header').first();
|
||||
|
||||
await expect(header.getByAltText('Gouvernement Logo')).toBeVisible();
|
||||
|
||||
await expect(header.getByAltText('Docs')).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { createDoc } from './utils-common';
|
||||
|
||||
test.describe('Left panel desktop', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -11,6 +13,53 @@ 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', () => {
|
||||
@@ -47,4 +96,12 @@ 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 { Page, expect } from '@playwright/test';
|
||||
import { Locator, Page, expect } from '@playwright/test';
|
||||
|
||||
export type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
||||
export const BROWSERS: BrowserName[] = ['chromium', 'webkit', 'firefox'];
|
||||
@@ -23,6 +23,7 @@ export const CONFIG = {
|
||||
LANGUAGE_CODE: 'en-us',
|
||||
POSTHOG_KEY: {},
|
||||
SENTRY_DSN: null,
|
||||
TRASHBIN_CUTOFF_DAYS: 30,
|
||||
theme_customization: {},
|
||||
} as const;
|
||||
|
||||
@@ -47,7 +48,7 @@ export const overrideConfig = async (
|
||||
export const keyCloakSignIn = async (
|
||||
page: Page,
|
||||
browserName: string,
|
||||
fromHome: boolean = true,
|
||||
fromHome = true,
|
||||
) => {
|
||||
if (fromHome) {
|
||||
await page.getByRole('button', { name: 'Start Writing' }).first().click();
|
||||
@@ -78,8 +79,8 @@ export const createDoc = async (
|
||||
page: Page,
|
||||
docName: string,
|
||||
browserName: string,
|
||||
length: number = 1,
|
||||
isMobile: boolean = false,
|
||||
length = 1,
|
||||
isMobile = false,
|
||||
) => {
|
||||
const randomDocs = randomName(docName, browserName, length);
|
||||
|
||||
@@ -325,3 +326,19 @@ 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();
|
||||
};
|
||||
|
||||
@@ -22,6 +22,6 @@ export const writeInEditor = async ({
|
||||
text: string;
|
||||
}) => {
|
||||
const editor = await getEditor({ page });
|
||||
editor.locator('.bn-block-outer').last().fill(text);
|
||||
await 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: string = 'user.test',
|
||||
fillText = 'user.test',
|
||||
) => {
|
||||
const responsePromiseSearchUser = page.waitForResponse(
|
||||
(response) =>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
import {
|
||||
BrowserName,
|
||||
randomName,
|
||||
updateDocTitle,
|
||||
verifyDocName,
|
||||
@@ -9,9 +10,9 @@ import {
|
||||
|
||||
export const createRootSubPage = async (
|
||||
page: Page,
|
||||
browserName: string,
|
||||
browserName: BrowserName,
|
||||
docName: string,
|
||||
isMobile: boolean = false,
|
||||
isMobile = false,
|
||||
) => {
|
||||
if (isMobile) {
|
||||
await page
|
||||
@@ -67,6 +68,49 @@ 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 navigateToTopParentFromTree = async ({ page }: { page: Page }) => {
|
||||
await page.getByRole('link', { name: /Open root document/ }).click();
|
||||
};
|
||||
|
||||
export const navigateToPageFromTree = async ({
|
||||
page,
|
||||
title,
|
||||
@@ -75,6 +119,10 @@ export const navigateToPageFromTree = async ({
|
||||
title: string;
|
||||
}) => {
|
||||
const docTree = page.getByTestId('doc-tree');
|
||||
await docTree.getByText(title).click();
|
||||
await docTree
|
||||
.getByText(title, {
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
await verifyDocName(page, title);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "app-e2e",
|
||||
"version": "3.7.0",
|
||||
"version": "3.8.21",
|
||||
"repository": "https://github.com/suitenumerique/docs",
|
||||
"author": "DINUM",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
@mf-types/
|
||||
node_modules/
|
||||
@@ -1,14 +0,0 @@
|
||||
# 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
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,489 +0,0 @@
|
||||
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;
|
||||
@@ -1,109 +0,0 @@
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
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;
|
||||
@@ -1,220 +0,0 @@
|
||||
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;
|
||||
@@ -1,163 +0,0 @@
|
||||
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;
|
||||
@@ -1,2 +0,0 @@
|
||||
// This file is just to satisfy webpack's entry point requirement
|
||||
// The actual component is exposed via Module Federation
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
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,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -1,3 +1,3 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=http://localhost:8071
|
||||
NEXT_PUBLIC_SW_DEACTIVATED=false
|
||||
NEXT_PUBLIC_PUBLISH_AS_MIT=false
|
||||
NEXT_PUBLIC_API_ORIGIN=
|
||||
NEXT_PUBLIC_SW_DEACTIVATED=
|
||||
NEXT_PUBLIC_PUBLISH_AS_MIT=true
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
NEXT_PUBLIC_API_ORIGIN=http://localhost:8071
|
||||
NEXT_PUBLIC_PUBLISH_AS_MIT=false
|
||||
NEXT_PUBLIC_SW_DEACTIVATED=true
|
||||
NEXT_PUBLIC_DEVELOP_PLUGINS=false
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
out/**/*
|
||||
public/assets/fonts/**/*
|
||||
node_modules/**/*
|
||||
@@ -1,305 +0,0 @@
|
||||
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 };
|
||||
@@ -1,15 +1,9 @@
|
||||
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} */
|
||||
@@ -27,18 +21,7 @@ const nextConfig = {
|
||||
env: {
|
||||
NEXT_PUBLIC_BUILD_ID: buildId,
|
||||
},
|
||||
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',
|
||||
],
|
||||
};
|
||||
|
||||
webpack(config, { isServer }) {
|
||||
// Grab the existing rule that handles SVG imports
|
||||
const fileLoaderRule = config.module.rules.find((rule) =>
|
||||
rule.test?.test?.('.svg'),
|
||||
@@ -84,59 +67,23 @@ const nextConfig = {
|
||||
}),
|
||||
);
|
||||
|
||||
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).*/);
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
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).*/);
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Modify the file loader rule to ignore *.svg, since we have it handled now.
|
||||
fileLoaderRule.exclude = /\.svg$/i;
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "app-impress",
|
||||
"version": "3.7.0",
|
||||
"version": "3.8.21",
|
||||
"repository": "https://github.com/suitenumerique/docs",
|
||||
"author": "DINUM",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "NEXT_PRIVATE_LOCAL_WEBPACK=true next dev",
|
||||
"build": "prettier --check . && yarn stylelint && NEXT_PRIVATE_LOCAL_WEBPACK=true next build",
|
||||
"dev": "next dev",
|
||||
"build": "prettier --check . && yarn stylelint && 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",
|
||||
@@ -31,12 +31,11 @@
|
||||
"@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.25",
|
||||
"@fontsource/material-icons": "5.2.5",
|
||||
"@gouvfr-lasuite/integration": "1.0.3",
|
||||
"@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.0",
|
||||
"@sentry/nextjs": "10.11.0",
|
||||
@@ -61,6 +60,7 @@
|
||||
"react-dom": "*",
|
||||
"react-i18next": "15.7.3",
|
||||
"react-intersection-observer": "9.16.0",
|
||||
"react-resizable-panels": "3.0.6",
|
||||
"react-select": "5.10.2",
|
||||
"styled-components": "6.1.19",
|
||||
"use-debounce": "10.0.6",
|
||||
@@ -69,8 +69,6 @@
|
||||
"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.87.4",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
@@ -97,6 +95,7 @@
|
||||
"typescript": "*",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.2.4",
|
||||
"webpack": "5.101.3",
|
||||
"workbox-webpack-plugin": "7.1.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22"
|
||||
|
||||
@@ -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: string = '1.0') =>
|
||||
export const baseApiUrl = (apiVersion = '1.0') =>
|
||||
`${backendUrl()}/api/v${apiVersion}/`;
|
||||
|
||||
@@ -36,7 +36,30 @@ 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'];
|
||||
@@ -73,6 +96,12 @@ 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,6 +24,9 @@ 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}
|
||||
@@ -37,10 +40,16 @@ const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
|
||||
color: ${props.disabled
|
||||
? 'var(--c--theme--colors--greyscale-400) !important'
|
||||
: 'inherit'};
|
||||
? `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;
|
||||
}
|
||||
${$css || ''}
|
||||
`}
|
||||
{...props}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Box, BoxType } from '.';
|
||||
|
||||
export const Card = ({
|
||||
children,
|
||||
className,
|
||||
$css,
|
||||
...props
|
||||
}: PropsWithChildren<BoxType>) => {
|
||||
@@ -14,7 +15,7 @@ export const Card = ({
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={`--docs--card ${props.className || ''}`}
|
||||
className={`--docs--card ${className || ''}`}
|
||||
$background="white"
|
||||
$radius="4px"
|
||||
$css={css`
|
||||
|
||||
@@ -4,12 +4,16 @@ import { css } from 'styled-components';
|
||||
import { Text, TextType } from '@/components';
|
||||
|
||||
type IconProps = TextType & {
|
||||
disabled?: boolean;
|
||||
iconName: string;
|
||||
variant?: 'filled' | 'outlined';
|
||||
variant?: 'filled' | 'outlined' | 'symbols-outlined';
|
||||
};
|
||||
export const Icon = ({
|
||||
className,
|
||||
iconName,
|
||||
disabled,
|
||||
variant = 'outlined',
|
||||
$variation,
|
||||
...textProps
|
||||
}: IconProps) => {
|
||||
const hasLabel = 'aria-label' in textProps || 'aria-labelledby' in textProps;
|
||||
@@ -18,12 +22,15 @@ export const Icon = ({
|
||||
|
||||
return (
|
||||
<Text
|
||||
{...textProps}
|
||||
aria-hidden={ariaHidden}
|
||||
className={clsx('--docs--icon-bg', textProps.className, {
|
||||
className={clsx('--docs--icon-bg', 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>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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,29 +15,6 @@ 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>;
|
||||
@@ -50,8 +27,6 @@ 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,8 +25,6 @@ export const TextErrors = ({
|
||||
canClose = false,
|
||||
...textProps
|
||||
}: TextErrorsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<AlertStyled
|
||||
canClose={canClose}
|
||||
@@ -34,31 +32,47 @@ export const TextErrors = ({
|
||||
icon={icon}
|
||||
className="--docs--text-errors"
|
||||
>
|
||||
<Box $direction="column" $gap="0.2rem">
|
||||
{causes &&
|
||||
causes.map((cause, i) => (
|
||||
<Text
|
||||
key={`causes-${i}`}
|
||||
$theme="danger"
|
||||
$variation="600"
|
||||
$textAlign="center"
|
||||
{...textProps}
|
||||
>
|
||||
{cause}
|
||||
</Text>
|
||||
))}
|
||||
<TextOnlyErrors
|
||||
causes={causes}
|
||||
defaultMessage={defaultMessage}
|
||||
{...textProps}
|
||||
/>
|
||||
</AlertStyled>
|
||||
);
|
||||
};
|
||||
|
||||
{!causes && (
|
||||
export const TextOnlyErrors = ({
|
||||
causes,
|
||||
defaultMessage,
|
||||
...textProps
|
||||
}: TextErrorsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box $direction="column" $gap="0.2rem">
|
||||
{causes &&
|
||||
causes.map((cause, i) => (
|
||||
<Text
|
||||
key={`causes-${i}`}
|
||||
$theme="danger"
|
||||
$variation="600"
|
||||
$textAlign="center"
|
||||
{...textProps}
|
||||
>
|
||||
{defaultMessage || t('Something bad happens, please retry.')}
|
||||
{cause}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</AlertStyled>
|
||||
))}
|
||||
|
||||
{!causes && (
|
||||
<Text
|
||||
$theme="danger"
|
||||
$variation="600"
|
||||
$textAlign="center"
|
||||
{...textProps}
|
||||
>
|
||||
{defaultMessage || t('Something bad happens, please retry.')}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { HorizontalSeparator } from '@gouvfr-lasuite/ui-kit';
|
||||
import {
|
||||
Fragment,
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
@@ -15,7 +16,7 @@ import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav';
|
||||
|
||||
export type DropdownMenuOption = {
|
||||
icon?: string;
|
||||
icon?: ReactNode;
|
||||
label: string;
|
||||
testId?: string;
|
||||
value?: string;
|
||||
@@ -220,7 +221,7 @@ export const DropdownMenu = ({
|
||||
$align="center"
|
||||
$gap={spacingsTokens['base']}
|
||||
>
|
||||
{option.icon && (
|
||||
{option.icon && typeof option.icon === 'string' && (
|
||||
<Icon
|
||||
$size="20px"
|
||||
$theme="greyscale"
|
||||
@@ -229,6 +230,9 @@ export const DropdownMenu = ({
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{option.icon &&
|
||||
typeof option.icon !== 'string' &&
|
||||
option.icon}
|
||||
<Text $variation={isDisabled ? '400' : '1000'}>
|
||||
{option.label}
|
||||
</Text>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './modal/AlertModal';
|
||||
export * from './Box';
|
||||
export * from './BoxButton';
|
||||
export * from './Card';
|
||||
@@ -9,7 +8,8 @@ export * from './Icon';
|
||||
export * from './InfiniteScroll';
|
||||
export * from './Link';
|
||||
export * from './Loading';
|
||||
export * from './modal/SideModal';
|
||||
export * from './modal';
|
||||
export * from './Overlayer';
|
||||
export * from './separators';
|
||||
export * from './Text';
|
||||
export * from './TextErrors';
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
|
||||
const ButtonCloseModal = (props: ButtonProps) => {
|
||||
export const ButtonCloseModal = (props: ButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
@@ -18,5 +18,3 @@ const ButtonCloseModal = (props: ButtonProps) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ButtonCloseModal;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './AlertModal';
|
||||
export * from './ButtonCloseModal';
|
||||
@@ -12,7 +12,6 @@ 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;
|
||||
|
||||
@@ -74,11 +73,9 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CunninghamProvider theme={theme}>
|
||||
<PluginSystemProvider>
|
||||
<ConfigProvider>
|
||||
<Auth>{children}</Auth>
|
||||
</ConfigProvider>
|
||||
</PluginSystemProvider>
|
||||
<ConfigProvider>
|
||||
<Auth>{children}</Auth>
|
||||
</ConfigProvider>
|
||||
</CunninghamProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
@@ -2,14 +2,15 @@ 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 {
|
||||
@@ -26,8 +27,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,2 +1,2 @@
|
||||
export * from './AppProvider';
|
||||
export * from './plugin';
|
||||
export * from './config';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
export * from './PluginSystemProvider';
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { PropsWithChildren, useEffect, useState } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { useConfig } from '@/core/config';
|
||||
import { Loading } from '@/components';
|
||||
import { useConfig } from '@/core';
|
||||
|
||||
import { HOME_URL } from '../conf';
|
||||
import { useAuth } from '../hooks';
|
||||
@@ -14,57 +13,65 @@ export const Auth = ({ children }: PropsWithChildren) => {
|
||||
useAuth();
|
||||
const { replace, pathname } = useRouter();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
if (isLoading && !isFetchedAfterMount) {
|
||||
return (
|
||||
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
const [isRedirecting, setIsRedirecting] = useState(false);
|
||||
|
||||
/**
|
||||
* If the user is authenticated and wanted initially to access a document,
|
||||
* we redirect to the document page.
|
||||
* If the user is authenticated and initially wanted to access a specific page, redirect him to that page now.
|
||||
*/
|
||||
if (authenticated) {
|
||||
useEffect(() => {
|
||||
if (!authenticated || isRedirecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const authUrl = getAuthUrl();
|
||||
if (authUrl) {
|
||||
void replace(authUrl);
|
||||
return (
|
||||
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
setIsRedirecting(true);
|
||||
void replace(authUrl).then(() => setIsRedirecting(false));
|
||||
}
|
||||
}
|
||||
}, [authenticated, isRedirecting, pathname, replace]);
|
||||
|
||||
/**
|
||||
* If the user is not authenticated and the path is not allowed, we redirect to the login page.
|
||||
* If the user is not authenticated and not on a allowed pages
|
||||
*/
|
||||
if (!authenticated && !pathAllowed) {
|
||||
useEffect(() => {
|
||||
if (isLoading || authenticated || pathAllowed || isRedirecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* The homepage feature is enabled, redirect them to the homepage
|
||||
*/
|
||||
if (config?.FRONTEND_HOMEPAGE_FEATURE_ENABLED) {
|
||||
void replace(HOME_URL);
|
||||
} else {
|
||||
gotoLogin();
|
||||
}
|
||||
return (
|
||||
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
if (pathname !== HOME_URL) {
|
||||
setIsRedirecting(true);
|
||||
void replace(HOME_URL).then(() => setIsRedirecting(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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" />;
|
||||
}
|
||||
|
||||
return children;
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { terminateCrispSession } from '@/services/Crisp';
|
||||
|
||||
import { LOGIN_URL, LOGOUT_URL, PATH_AUTH_LOCAL_STORAGE } from './conf';
|
||||
import {
|
||||
HOME_URL,
|
||||
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) {
|
||||
@@ -10,8 +18,15 @@ 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 !== '/') {
|
||||
if (
|
||||
window.location.pathname !== '/' &&
|
||||
window.location.pathname !== `${HOME_URL}/`
|
||||
) {
|
||||
localStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.pathname);
|
||||
}
|
||||
};
|
||||
|
||||
+3
-2
@@ -83,6 +83,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
|
||||
const isConnectedToCollabServer = provider.isSynced;
|
||||
const readOnly = !doc.abilities.partial_update || !isEditable || isLoading;
|
||||
const isDeletedDoc = !!doc.deleted_at;
|
||||
|
||||
useSaveDoc(doc.id, provider.document, !readOnly, isConnectedToCollabServer);
|
||||
const { i18n } = useTranslation();
|
||||
@@ -180,7 +181,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
<Box
|
||||
$padding={{ top: 'md' }}
|
||||
$background="white"
|
||||
$css={cssEditor(readOnly)}
|
||||
$css={cssEditor(readOnly, isDeletedDoc)}
|
||||
className="--docs--editor-container"
|
||||
>
|
||||
{errorAttachment && (
|
||||
@@ -231,7 +232,7 @@ export const BlockNoteEditorVersion = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<Box $css={cssEditor(readOnly)} className="--docs--editor-container">
|
||||
<Box $css={cssEditor(readOnly, true)} className="--docs--editor-container">
|
||||
<BlockNoteView editor={editor} editable={!readOnly} theme="light" />
|
||||
</Box>
|
||||
);
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ function isBlock(block: Block): block is Block {
|
||||
);
|
||||
}
|
||||
|
||||
const recursiveContent = (content: Block[], base: string = '') => {
|
||||
const recursiveContent = (content: Block[], base = '') => {
|
||||
let fullContent = base;
|
||||
for (const innerContent of content) {
|
||||
if (innerContent.type === 'text') {
|
||||
|
||||
+7
-1
@@ -44,7 +44,13 @@ export const PdfBlock = createReactBlockSpec(
|
||||
<Box ref={contentRef} className="bn-file-block-content-wrapper">
|
||||
<PDFBlockStyle />
|
||||
<ResizableFileBlockWrapper
|
||||
buttonIcon={<Icon iconName="upload" />}
|
||||
buttonIcon={
|
||||
<Icon
|
||||
iconName="upload"
|
||||
$size="24px"
|
||||
$css="line-height: normal;"
|
||||
/>
|
||||
}
|
||||
block={block}
|
||||
editor={editor as unknown as FileBlockEditor}
|
||||
buttonText={t('Add PDF')}
|
||||
|
||||
+11
-4
@@ -1,9 +1,10 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { createReactInlineContentSpec } from '@blocknote/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { StyledLink, Text } from '@/components';
|
||||
import { BoxButton, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import SelectedPageIcon from '@/docs/doc-editor/assets/doc-selected.svg';
|
||||
import { useDoc } from '@/docs/doc-management';
|
||||
@@ -51,10 +52,16 @@ interface LinkSelectedProps {
|
||||
}
|
||||
const LinkSelected = ({ url, title }: LinkSelectedProps) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const router = useRouter();
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
void router.push(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledLink
|
||||
href={url}
|
||||
<BoxButton
|
||||
onClick={handleClick}
|
||||
draggable="false"
|
||||
$css={css`
|
||||
display: inline;
|
||||
@@ -75,6 +82,6 @@ const LinkSelected = ({ url, title }: LinkSelectedProps) => {
|
||||
<Text $weight="500" spellCheck="false" $size="16px" $display="inline">
|
||||
{title}
|
||||
</Text>
|
||||
</StyledLink>
|
||||
</BoxButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from 'styled-components';
|
||||
|
||||
export const cssEditor = (readonly: boolean) => css`
|
||||
export const cssEditor = (readonly: boolean, isDeletedDoc: boolean) => css`
|
||||
&,
|
||||
& > .bn-container,
|
||||
& .ProseMirror {
|
||||
@@ -127,6 +127,13 @@ export const cssEditor = (readonly: boolean) => css`
|
||||
.bn-block-outer:not([data-prev-depth-changed]):before {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
${isDeletedDoc &&
|
||||
`
|
||||
.node-interlinkingLinkInline button {
|
||||
pointer-events: none;
|
||||
}
|
||||
`}
|
||||
}
|
||||
|
||||
& .bn-editor {
|
||||
|
||||
@@ -16,8 +16,7 @@ import { cloneElement, isValidElement, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Text } from '@/components';
|
||||
import ButtonCloseModal from '@/components/modal/ButtonCloseModal';
|
||||
import { Box, ButtonCloseModal, Text } from '@/components';
|
||||
import { useEditorStore } from '@/docs/doc-editor';
|
||||
import { Doc, useTrans } from '@/docs/doc-management';
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export const AlertNetwork = () => {
|
||||
$padding="xs"
|
||||
$flex={1}
|
||||
$align="center"
|
||||
$gap={spacingsTokens['3xs']}
|
||||
$gap={spacingsTokens['2xs']}
|
||||
$css={css`
|
||||
border: 1px solid var(--c--theme--colors--warning-300);
|
||||
`}
|
||||
|
||||
@@ -18,7 +18,7 @@ export const AlertPublic = ({ isPublicDoc }: { isPublicDoc: boolean }) => {
|
||||
$padding="xs"
|
||||
$flex={1}
|
||||
$align="center"
|
||||
$gap={spacingsTokens['3xs']}
|
||||
$gap={spacingsTokens['2xs']}
|
||||
$css={css`
|
||||
border: 1px solid var(--c--theme--colors--primary-300, #e3e3fd);
|
||||
`}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
|
||||
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, Icon, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import {
|
||||
Doc,
|
||||
KEY_DOC,
|
||||
KEY_LIST_DOC,
|
||||
useRestoreDoc,
|
||||
} from '@/docs/doc-management';
|
||||
import { KEY_LIST_DOC_TRASHBIN } from '@/docs/docs-grid';
|
||||
|
||||
export const AlertRestore = ({ doc }: { doc: Doc }) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToastProvider();
|
||||
const treeContext = useTreeContext<Doc>();
|
||||
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
|
||||
const { mutate: restoreDoc, error } = useRestoreDoc({
|
||||
listInvalidQueries: [KEY_LIST_DOC, KEY_LIST_DOC_TRASHBIN, KEY_DOC],
|
||||
options: {
|
||||
onSuccess: (_data) => {
|
||||
// It will force the tree to be reloaded
|
||||
treeContext?.setRoot(undefined as unknown as Doc);
|
||||
|
||||
toast(t('The document has been restored.'), VariantType.SUCCESS, {
|
||||
duration: 4000,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast(
|
||||
t('An error occurred while restoring the document: {{error}}', {
|
||||
error: error?.message,
|
||||
}),
|
||||
VariantType.ERROR,
|
||||
{
|
||||
duration: 4000,
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="--docs--alert-restore"
|
||||
aria-label={t('Alert deleted document')}
|
||||
$color={colorsTokens['danger-800']}
|
||||
$background={colorsTokens['danger-100']}
|
||||
$radius={spacingsTokens['3xs']}
|
||||
$direction="row"
|
||||
$padding="xs"
|
||||
$flex={1}
|
||||
$align="center"
|
||||
$gap={spacingsTokens['3xs']}
|
||||
$css={css`
|
||||
border: 1px solid var(--c--theme--colors--danger-300, #e3e3fd);
|
||||
`}
|
||||
$justify="space-between"
|
||||
>
|
||||
<Box $direction="row" $align="center" $gap={spacingsTokens['2xs']}>
|
||||
<Icon
|
||||
$theme="danger"
|
||||
$variation="700"
|
||||
data-testid="public-icon"
|
||||
iconName="delete"
|
||||
variant="symbols-outlined"
|
||||
/>
|
||||
<Text $theme="danger" $variation="700" $weight="500">
|
||||
{t('Document deleted')}
|
||||
</Text>
|
||||
</Box>
|
||||
<BoxButton
|
||||
onClick={() =>
|
||||
restoreDoc({
|
||||
docId: doc.id,
|
||||
})
|
||||
}
|
||||
$direction="row"
|
||||
$gap="0.2rem"
|
||||
$theme="danger"
|
||||
$variation="600"
|
||||
$align="center"
|
||||
>
|
||||
<Icon
|
||||
iconName="undo"
|
||||
$theme="danger"
|
||||
$variation="600"
|
||||
$size="18px"
|
||||
variant="symbols-outlined"
|
||||
/>
|
||||
<Text $theme="danger" $variation="600" $size="s" $css="line-height:1;">
|
||||
{t('Restore')}
|
||||
</Text>
|
||||
</BoxButton>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, Icon } from '@/components';
|
||||
import { Doc } from '@/docs/doc-management';
|
||||
|
||||
interface BoutonShareProps {
|
||||
displayNbAccess: boolean;
|
||||
doc: Doc;
|
||||
isDisabled?: boolean;
|
||||
isHidden?: boolean;
|
||||
open: () => void;
|
||||
}
|
||||
|
||||
export const BoutonShare = ({
|
||||
displayNbAccess,
|
||||
doc,
|
||||
isDisabled,
|
||||
isHidden,
|
||||
open,
|
||||
}: BoutonShareProps) => {
|
||||
const { t } = useTranslation();
|
||||
const treeContext = useTreeContext<Doc>();
|
||||
|
||||
/**
|
||||
* Following the change where there is no default owner when adding a sub-page,
|
||||
* we need to handle both the case where the doc is the root and the case of sub-pages.
|
||||
*/
|
||||
const hasAccesses = useMemo(() => {
|
||||
if (treeContext?.root?.id === doc.id) {
|
||||
return doc.nb_accesses_direct > 1 && displayNbAccess;
|
||||
}
|
||||
|
||||
return doc.nb_accesses_direct >= 1 && displayNbAccess;
|
||||
}, [doc.id, treeContext?.root, doc.nb_accesses_direct, displayNbAccess]);
|
||||
|
||||
if (isHidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasAccesses) {
|
||||
return (
|
||||
<Box
|
||||
$css={css`
|
||||
.c__button--medium {
|
||||
height: 32px;
|
||||
padding: 10px var(--c--theme--spacings--xs);
|
||||
gap: 7px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Button
|
||||
color="tertiary"
|
||||
aria-label={t('Share button')}
|
||||
icon={
|
||||
<Icon
|
||||
iconName="group"
|
||||
$theme="primary"
|
||||
$variation="800"
|
||||
variant="filled"
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
}
|
||||
onClick={open}
|
||||
size="medium"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{doc.nb_accesses_direct}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
color="tertiary-text"
|
||||
onClick={open}
|
||||
size="medium"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{t('Share')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user