feat: add i18n to admin panel (#1326)
Co-authored-by: shikhantmaungs <shinkhantmaungs@gmail.com> Co-authored-by: Thomas Ghysels <info@thomasg.be> Co-authored-by: Kokutse Djoguenou <kokutse@Kokutses-MacBook-Pro.local> Co-authored-by: Christian Gil <47041342+ChrisGV04@users.noreply.github.com> Co-authored-by: Łukasz Rabiec <lukaszrabiec@gmail.com> Co-authored-by: Jenny <jennifer.eberlei@gmail.com> Co-authored-by: Hung Vu <hunghvu2017@gmail.com> Co-authored-by: Shin Khant Maung <101539335+shinkhantmaungs@users.noreply.github.com> Co-authored-by: Carlo Brualdi <carlo.brualdi@gmail.com> Co-authored-by: Ariel Tonglet <ariel.tonglet@gmail.com> Co-authored-by: Roman Ryzhikov <general+github@ya.ru> Co-authored-by: maekoya <maekoya@stromatolite.jp> Co-authored-by: Emilia Trollros <3m1l1a@emiliatrollros.se> Co-authored-by: Kokutse J Djoguenou <90865585+Julesdj@users.noreply.github.com> Co-authored-by: Mitch Dries <mitch.dries@gmail.com> BREAKING CHANGE: If you assigned labels to collections, globals or block names, you need to update your config! Your GraphQL schema and generated Typescript interfaces may have changed. Payload no longer uses labels for code based naming. To prevent breaking changes to your GraphQL API and typescript types in your project, you can assign the below properties to match what Payload previously generated for you from labels. On Collections Use `graphQL.singularName`, `graphQL.pluralName` for GraphQL schema names. Use `typescript.interface` for typescript generation name. On Globals Use `graphQL.name` for GraphQL Schema name. Use `typescript.interface` for typescript generation name. On Blocks (within Block fields) Use `graphQL.singularName` for graphQL schema names.
This commit is contained in:
@@ -231,6 +231,29 @@ To make use of Payload SCSS variables / mixins to use directly in your own compo
|
||||
@import '~payload/scss';
|
||||
```
|
||||
|
||||
### Getting the current language
|
||||
|
||||
When developing custom components you can support multiple languages to be consistent with Payload's i18n support. The best way to do this is to add your translation resources to the [i18n configuration](https://payloadcms.com/docs/configuration/i18n) and import `useTranslation` from `react-i18next` in your components.
|
||||
|
||||
For example:
|
||||
```tsx
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const CustomComponent: React.FC = () => {
|
||||
// highlight-start
|
||||
const { t, i18n } = useTranslation('namespace1');
|
||||
// highlight-end
|
||||
|
||||
return (
|
||||
<ul>
|
||||
<li>{ t('key', { variable: 'value' }) }</li>
|
||||
<li>{ t('namespace2:key', { variable: 'value' }) }</li>
|
||||
<li>{ i18n.language }</li>
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Getting the current locale
|
||||
|
||||
In any custom component you can get the selected locale with the `useLocale` hook. Here is a simple example:
|
||||
|
||||
@@ -12,19 +12,21 @@ It's often best practice to write your Collections in separate files and then im
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
| ---------------- | -------------|
|
||||
| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
||||
| Option | Description |
|
||||
|------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Collection. |
|
||||
| **`fields`** * | Array of field types that will determine the structure and functionality of the data stored within this Collection. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. |
|
||||
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). |
|
||||
| **`hooks`** | Entry points to "tie in" to Collection actions at specific points. [More](/docs/hooks/overview#collection-hooks) |
|
||||
| **`access`** | Provide access control functions to define exactly who should be able to do what with Documents in this Collection. [More](/docs/access-control/overview/#collections) |
|
||||
| **`auth`** | Specify options if you would like this Collection to feature authentication. For more, consult the [Authentication](/docs/authentication/config) documentation. |
|
||||
| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](/docs/upload/overview) documentation. |
|
||||
| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
|
||||
| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#collection-config)|
|
||||
| **`endpoints`** | Add custom routes to the REST API. [More](/docs/rest-api/overview#custom-endpoints) |
|
||||
| **`labels`** | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. |
|
||||
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). |
|
||||
| **`hooks`** | Entry points to "tie in" to Collection actions at specific points. [More](/docs/hooks/overview#collection-hooks) |
|
||||
| **`access`** | Provide access control functions to define exactly who should be able to do what with Documents in this Collection. [More](/docs/access-control/overview/#collections) |
|
||||
| **`auth`** | Specify options if you would like this Collection to feature authentication. For more, consult the [Authentication](/docs/authentication/config) documentation. |
|
||||
| **`upload`** | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](/docs/upload/overview) documentation. |
|
||||
| **`timestamps`** | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. |
|
||||
| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#collection-config) |
|
||||
| **`endpoints`** | Add custom routes to the REST API. [More](/docs/rest-api/overview#custom-endpoints) |
|
||||
| **`graphQL`** | An object with `singularName` and `pluralName` strings used in schema generation. Auto-generated from slug if not defined. |
|
||||
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
|
||||
*\* An asterisk denotes that a property is required.*
|
||||
|
||||
|
||||
@@ -12,17 +12,19 @@ As with Collection configs, it's often best practice to write your Globals in se
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
| ---------------- | -------------|
|
||||
| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Global. |
|
||||
| **`fields`** * | Array of field types that will determine the structure and functionality of the data stored within this Global. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. |
|
||||
| **`label`** | Singular label for use in identifying this Global throughout Payload. Auto-generated from slug if not defined. |
|
||||
| **`description`**| Text or React component to display below the Global header to give editors more information. |
|
||||
| **`admin`** | Admin-specific configuration. See below for [more detail](/docs/configuration/globals#admin-options). |
|
||||
| **`hooks`** | Entry points to "tie in" to collection actions at specific points. [More](/docs/hooks/overview#global-hooks) |
|
||||
| **`access`** | Provide access control functions to define exactly who should be able to do what with this Global. [More](/docs/access-control/overview/#globals) |
|
||||
| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#globals-config)|
|
||||
| **`endpoints`** | Add custom routes to the REST API. [More](/docs/rest-api/overview#custom-endpoints)|
|
||||
| Option | Description |
|
||||
|--------------------| -------------|
|
||||
| **`slug`** * | Unique, URL-friendly string that will act as an identifier for this Global. |
|
||||
| **`fields`** * | Array of field types that will determine the structure and functionality of the data stored within this Global. [Click here](/docs/fields/overview) for a full list of field types as well as how to configure them. |
|
||||
| **`label`** | Singular label for use in identifying this Global throughout Payload. Auto-generated from slug if not defined. |
|
||||
| **`description`** | Text or React component to display below the Global header to give editors more information. |
|
||||
| **`admin`** | Admin-specific configuration. See below for [more detail](/docs/configuration/globals#admin-options). |
|
||||
| **`hooks`** | Entry points to "tie in" to collection actions at specific points. [More](/docs/hooks/overview#global-hooks) |
|
||||
| **`access`** | Provide access control functions to define exactly who should be able to do what with this Global. [More](/docs/access-control/overview/#globals) |
|
||||
| **`versions`** | Set to true to enable default options, or configure with object properties. [More](/docs/versions/overview#globals-config)|
|
||||
| **`endpoints`** | Add custom routes to the REST API. [More](/docs/rest-api/overview#custom-endpoints)|
|
||||
| **`graphQL.name`** | Text used in schema generation. Auto-generated from slug if not defined. |
|
||||
| **`typescript`** | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
|
||||
|
||||
*\* An asterisk denotes that a property is required.*
|
||||
|
||||
|
||||
100
docs/configuration/i18n.mdx
Normal file
100
docs/configuration/i18n.mdx
Normal file
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: I18n
|
||||
label: I18n
|
||||
order: 40
|
||||
desc: Manage and customize internationalization support in your CMS editor experience
|
||||
keywords: internationalization, i18n, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, express
|
||||
---
|
||||
|
||||
Not only does Payload support managing localized content, it also has internationalization support so that admin users can work in their preferred language. Payload's i18n support is built on top of [i18next](https://www.i18next.com). It comes included by default and can be extended in your config.
|
||||
|
||||
While Payload's built-in features come translated, you may want to also translate parts of your project's configuration too. This is possible in places like collections and globals labels and groups, field labels, descriptions and input placeholder text. The admin UI will display all the correct translations you provide based on the user's language.
|
||||
|
||||
Here is an example of a simple collection supporting both English and Spanish editors:
|
||||
```ts
|
||||
const Articles: CollectionConfig = {
|
||||
slug: 'articles',
|
||||
labels: {
|
||||
singular: {
|
||||
en: 'Article', es: 'Artículo',
|
||||
},
|
||||
plural: {
|
||||
en: 'Articles', es: 'Artículo',
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
group: { en: 'Content', es: 'Contenido' },
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
label: {
|
||||
en: 'Title', es: 'Título',
|
||||
},
|
||||
admin: {
|
||||
placeholder: { en: 'Enter title', es: 'Introduce el título' }
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
type: 'radio',
|
||||
options: [{
|
||||
value: 'news',
|
||||
label: { en: 'News', es: 'Noticias' },
|
||||
}, // etc...
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### Admin UI
|
||||
|
||||
The Payload admin panel reads the language settings of a user's browser and display all text in that language, or will fall back to English if the user's language is not yet supported.
|
||||
After a user logs in, they can change their language selection in the `/account` view.
|
||||
|
||||
<Banner>
|
||||
<strong>Note:</strong><br/>
|
||||
If there is a language that Payload does not yet support, we accept code <a href="https://github.com/payloadcms/payload/blob/master/contributing.md">contributions</a>.
|
||||
</Banner>
|
||||
|
||||
### Node Express
|
||||
|
||||
Payload's backend uses express middleware to set the language on incoming requests before they are handled. This allows backend validation to return error messages in the user's own language or system generated emails to be sent using the correct translation. You can make HTTP requests with the `accept-language` header and Payload will use that language.
|
||||
|
||||
Anywhere in your Payload app that you have access to the `req` object, you can access i18next's extensive internationalization features assigned to `req.i18n`. To access text translations you can use `req.t('namespace:key')`.
|
||||
|
||||
Read the i18next [API documentation](https://www.i18next.com/overview/api) to learn more.
|
||||
|
||||
### Configuration Options
|
||||
|
||||
In your Payload config, you can add translations and customize the settings in `i18n`. Payload will use your custom options and merge it with the default, allowing you to override the settings Payload provides.
|
||||
|
||||
**Example Payload config extending i18n:**
|
||||
|
||||
```ts
|
||||
import { buildConfig } from 'payload/config'
|
||||
|
||||
export default buildConfig({
|
||||
//...
|
||||
i18n: {
|
||||
fallbackLng: 'en', // default
|
||||
debug: false, // default
|
||||
resources: {
|
||||
en: {
|
||||
custom: { // namespace can be anything you want
|
||||
key1: 'Translation with {{variable}}', // translation
|
||||
},
|
||||
// override existing translation keys
|
||||
general: {
|
||||
dashboard: 'Home',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
//...
|
||||
});
|
||||
```
|
||||
|
||||
See the i18next [configuration options](https://www.i18next.com/overview/configuration-options) to learn more.
|
||||
@@ -72,7 +72,7 @@ const ExampleCollection: CollectionConfig = {
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
type: 'upload',
|
||||
|
||||
@@ -22,21 +22,21 @@ keywords: blocks, fields, config, configuration, documentation, Content Manageme
|
||||
|
||||
### Field config
|
||||
|
||||
| Option | Description |
|
||||
| ---------------- | ----------- |
|
||||
| **`name`** * | To be used as the property name when stored and retrieved from the database. |
|
||||
| **`label`** | Used as a heading in the Admin panel and to name the generated GraphQL type. |
|
||||
| **`blocks`** * | Array of [block configs](/docs/fields/blocks#block-configs) to be made available to this field. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide field-level hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
|
||||
| **`access`** | Provide field-level access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API response or the Admin panel. |
|
||||
| **`defaultValue`** | Provide an array of block data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this field will be kept, so there is no need to specify each nested field as `localized`. || **`required`** | Require this field to have a value. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`labels`** | Customize the block row labels appearing in the Admin dashboard. |
|
||||
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
|
||||
| Option | Description |
|
||||
|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **`name`** * | To be used as the property name when stored and retrieved from the database. |
|
||||
| **`label`** | Used as a heading in the Admin panel. |
|
||||
| **`blocks`** * | Array of [block configs](/docs/fields/blocks#block-configs) to be made available to this field. |
|
||||
| **`validate`** | Provide a custom validation function that will be executed on both the Admin panel and the backend. [More](/docs/fields/overview#validation) |
|
||||
| **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/authentication/config), include its data in the user JWT. |
|
||||
| **`hooks`** | Provide field-level hooks to control logic for this field. [More](/docs/fields/overview#field-level-hooks) |
|
||||
| **`access`** | Provide field-level access control to denote what users can see and do with this field's data. [More](/docs/fields/overview#field-level-access-control) |
|
||||
| **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API response or the Admin panel. |
|
||||
| **`defaultValue`** | Provide an array of block data to be used for this field's default value. [More](/docs/fields/overview#default-values) |
|
||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. If enabled, a separate, localized set of all data within this field will be kept, so there is no need to specify each nested field as `localized`. || **`required`** | Require this field to have a value. |
|
||||
| **`unique`** | Enforce that each entry in the Collection has a unique value for this field. |
|
||||
| **`labels`** | Customize the block row labels appearing in the Admin dashboard. |
|
||||
| **`admin`** | Admin-specific configuration. See below for [more detail](#admin-config). |
|
||||
|
||||
*\* An asterisk denotes that a property is required.*
|
||||
|
||||
@@ -57,13 +57,14 @@ Blocks are defined as separate configs of their own.
|
||||
Best practice is to define each block config in its own file, and then import them into your Blocks field as necessary. This way each block config can be easily shared between fields. For instance, using the "layout builder" example, you might want to feature a few of the same blocks in a Post collection as well as a Page collection. Abstracting into their own files trivializes their reusability.
|
||||
</Banner>
|
||||
|
||||
| Option | Description |
|
||||
| ---------------- | ----------- |
|
||||
| **`slug`** * | Identifier for this block type. Will be saved on each block as the `blockType` property. |
|
||||
| **`fields`** * | Array of fields to be stored in this block. |
|
||||
| **`labels`** | Customize the block labels that appear in the Admin dashboard. Also used to name corresponding GraphQL schema types. Auto-generated from slug if not defined. |
|
||||
| **`imageURL`** | Provide a custom image thumbnail to help editors identify this block in the Admin UI. |
|
||||
| **`imageAltText`** | Customize this block's image thumbnail alt text. |
|
||||
| Option | Description |
|
||||
|----------------------------|---------------------------------------------------------------------------------------------------------|
|
||||
| **`slug`** * | Identifier for this block type. Will be saved on each block as the `blockType` property. |
|
||||
| **`fields`** * | Array of fields to be stored in this block. |
|
||||
| **`labels`** | Customize the block labels that appear in the Admin dashboard. Auto-generated from slug if not defined. |
|
||||
| **`imageURL`** | Provide a custom image thumbnail to help editors identify this block in the Admin UI. |
|
||||
| **`imageAltText`** | Customize this block's image thumbnail alt text. |
|
||||
| **`graphQL.singularName`** | Text to use for the GraphQL schema name. Auto-generated from slug if not defined |
|
||||
|
||||
#### Auto-generated data per block
|
||||
|
||||
|
||||
@@ -72,14 +72,15 @@ There are two arguments available to custom validation functions.
|
||||
1. The value which is currently assigned to the field
|
||||
2. An optional object with dynamic properties for more complex validation having the following:
|
||||
|
||||
| Property | Description |
|
||||
| ------------- | -------------|
|
||||
| `data` | An object of the full collection or global document |
|
||||
| `siblingData` | An object of the document data limited to fields within the same parent to the field |
|
||||
| `operation` | Will be "create" or "update" depending on the UI action or API call |
|
||||
| `id` | The value of the collection `id`, will be `undefined` on create request |
|
||||
| `user` | The currently authenticated user object |
|
||||
| `payload` | If the `validate` function is being executed on the server, Payload will be exposed for easily running local operations. |
|
||||
| Property | Description |
|
||||
|---------------|--------------------------------------------------------------------------------------------------------------------------|
|
||||
| `data` | An object of the full collection or global document. |
|
||||
| `siblingData` | An object of the document data limited to fields within the same parent to the field. |
|
||||
| `operation` | Will be "create" or "update" depending on the UI action or API call. |
|
||||
| `id` | The value of the collection `id`, will be `undefined` on create request. |
|
||||
| `t` | The function for translating text, [more](/docs/configuration/i18n). |
|
||||
| `user` | The currently authenticated user object. |
|
||||
| `payload` | If the `validate` function is being executed on the server, Payload will be exposed for easily running local operations. |
|
||||
|
||||
Example:
|
||||
```ts
|
||||
|
||||
@@ -35,10 +35,6 @@ import { CollectionConfig } from 'payload/types';
|
||||
const PublicUser: CollectionConfig = {
|
||||
slug: 'public-users',
|
||||
auth: true, // Auth is enabled
|
||||
labels: {
|
||||
singular: 'Public User',
|
||||
plural: 'Public Users',
|
||||
},
|
||||
fields: [
|
||||
...
|
||||
],
|
||||
|
||||
@@ -124,6 +124,9 @@
|
||||
"graphql-type-json": "^0.3.1",
|
||||
"html-webpack-plugin": "^5.0.0-alpha.14",
|
||||
"http-status": "^1.4.2",
|
||||
"i18next": "^22.0.1",
|
||||
"i18next-browser-languagedetector": "^6.1.8",
|
||||
"i18next-http-middleware": "^3.2.1",
|
||||
"is-hotkey": "^0.2.0",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
@@ -167,6 +170,7 @@
|
||||
"react-diff-viewer": "^3.1.1",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-i18next": "^11.18.6",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-router-navigation-prompt": "^1.9.6",
|
||||
"react-select": "^3.0.8",
|
||||
@@ -237,6 +241,7 @@
|
||||
"@types/passport-jwt": "^3.0.3",
|
||||
"@types/passport-local": "^1.0.33",
|
||||
"@types/pino": "^6.3.4",
|
||||
"@types/pino-std-serializers": "^4.0.0",
|
||||
"@types/pluralize": "^0.0.29",
|
||||
"@types/prismjs": "^1.16.2",
|
||||
"@types/prop-types": "^15.7.3",
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import qs from 'qs';
|
||||
|
||||
type GetOptions = RequestInit & {
|
||||
params?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export const requests = {
|
||||
get: (url: string, params: unknown = {}): Promise<Response> => {
|
||||
const query = qs.stringify(params, { addQueryPrefix: true });
|
||||
return fetch(`${url}${query}`, { credentials: 'include' });
|
||||
get: (url: string, options: GetOptions = { headers: {} }): Promise<Response> => {
|
||||
let query = '';
|
||||
if (options.params) {
|
||||
query = qs.stringify(options.params, { addQueryPrefix: true });
|
||||
}
|
||||
return fetch(`${url}${query}`, {
|
||||
credentials: 'include',
|
||||
headers: options.headers,
|
||||
});
|
||||
},
|
||||
|
||||
post: (url: string, options: RequestInit = { headers: {} }): Promise<Response> => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { Suspense, lazy, useState, useEffect } from 'react';
|
||||
import {
|
||||
Route, Switch, withRouter, Redirect,
|
||||
} from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from './utilities/Auth';
|
||||
import { useConfig } from './utilities/Config';
|
||||
import List from './views/collections/List';
|
||||
@@ -29,6 +30,7 @@ const Account = lazy(() => import('./views/Account'));
|
||||
const Routes = () => {
|
||||
const [initialized, setInitialized] = useState(null);
|
||||
const { user, permissions, refreshCookie } = useAuth();
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const canAccessAdmin = permissions?.canAccessAdmin;
|
||||
|
||||
@@ -54,7 +56,11 @@ const Routes = () => {
|
||||
const { slug } = userCollection;
|
||||
|
||||
if (!userCollection.auth.disableLocalStrategy) {
|
||||
requests.get(`${routes.api}/${slug}/init`).then((res) => res.json().then((data) => {
|
||||
requests.get(`${routes.api}/${slug}/init`, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
}).then((res) => res.json().then((data) => {
|
||||
if (data && 'initialized' in data) {
|
||||
setInitialized(data.initialized);
|
||||
}
|
||||
@@ -62,7 +68,7 @@ const Routes = () => {
|
||||
} else {
|
||||
setInitialized(true);
|
||||
}
|
||||
}, [routes, userCollection]);
|
||||
}, [i18n.language, routes, userCollection]);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Popup from '../Popup';
|
||||
import More from '../../icons/More';
|
||||
import Chevron from '../../icons/Chevron';
|
||||
@@ -19,6 +20,7 @@ export const ArrayAction: React.FC<Props> = ({
|
||||
duplicateRow,
|
||||
removeRow,
|
||||
}) => {
|
||||
const { t } = useTranslation('general');
|
||||
return (
|
||||
<Popup
|
||||
horizontalAlign="center"
|
||||
@@ -38,7 +40,7 @@ export const ArrayAction: React.FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<Chevron />
|
||||
Move Up
|
||||
{t('moveUp')}
|
||||
</button>
|
||||
)}
|
||||
{index < rowCount - 1 && (
|
||||
@@ -51,7 +53,7 @@ export const ArrayAction: React.FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<Chevron />
|
||||
Move Down
|
||||
{t('moveDown')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
@@ -63,7 +65,7 @@ export const ArrayAction: React.FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<Plus />
|
||||
Add Below
|
||||
{t('addBelow')}
|
||||
</button>
|
||||
<button
|
||||
className={`${baseClass}__action ${baseClass}__duplicate`}
|
||||
@@ -74,7 +76,7 @@ export const ArrayAction: React.FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<Copy />
|
||||
Duplicate
|
||||
{t('duplicate')}
|
||||
</button>
|
||||
<button
|
||||
className={`${baseClass}__action ${baseClass}__remove`}
|
||||
@@ -85,7 +87,7 @@ export const ArrayAction: React.FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<X />
|
||||
Remove
|
||||
{t('remove')}
|
||||
</button>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { formatDistance } from 'date-fns';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import { useFormModified, useAllFormFields } from '../../forms/Form/context';
|
||||
import { useLocale } from '../../utilities/Locale';
|
||||
@@ -21,6 +21,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
|
||||
const modified = useFormModified();
|
||||
const locale = useLocale();
|
||||
const { replace } = useHistory();
|
||||
const { t, i18n } = useTranslation('version');
|
||||
|
||||
let interval = 800;
|
||||
if (collection?.versions.drafts && collection.versions?.drafts?.autosave) interval = collection.versions.drafts.autosave.interval;
|
||||
@@ -42,6 +43,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
@@ -54,9 +56,9 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
|
||||
},
|
||||
});
|
||||
} else {
|
||||
toast.error('There was a problem while autosaving this document.');
|
||||
toast.error(t('error:autosaving'));
|
||||
}
|
||||
}, [collection, serverURL, api, admin, locale, replace]);
|
||||
}, [i18n, serverURL, api, collection, locale, replace, admin, t]);
|
||||
|
||||
useEffect(() => {
|
||||
// If no ID, but this is used for a collection doc,
|
||||
@@ -98,6 +100,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
@@ -114,7 +117,7 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
|
||||
};
|
||||
|
||||
autosave();
|
||||
}, [debouncedFields, modified, serverURL, api, collection, global, id, getVersions, locale]);
|
||||
}, [i18n, debouncedFields, modified, serverURL, api, collection, global, id, getVersions, locale]);
|
||||
|
||||
useEffect(() => {
|
||||
if (versions?.docs?.[0]) {
|
||||
@@ -126,12 +129,12 @@ const Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdated
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{saving && 'Saving...'}
|
||||
{saving && t('saving')}
|
||||
{(!saving && lastSaved) && (
|
||||
<React.Fragment>
|
||||
Last saved
|
||||
{formatDistance(new Date(), new Date(lastSaved))}
|
||||
ago
|
||||
{t('lastSavedAgo', {
|
||||
distance: Math.round((Number(new Date(lastSaved)) - Number(new Date())) / 1000 / 60),
|
||||
})}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import AnimateHeight from 'react-animate-height';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Props } from './types';
|
||||
import { CollapsibleProvider, useCollapsible } from './provider';
|
||||
import Chevron from '../../icons/Chevron';
|
||||
@@ -22,6 +23,7 @@ export const Collapsible: React.FC<Props> = ({
|
||||
const [collapsedLocal, setCollapsedLocal] = useState(Boolean(initCollapsed));
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const isNested = useCollapsible();
|
||||
const { t } = useTranslation('fields');
|
||||
|
||||
const collapsed = typeof collapsedFromProps === 'boolean' ? collapsedFromProps : collapsedLocal;
|
||||
|
||||
@@ -61,7 +63,7 @@ export const Collapsible: React.FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
Toggle block
|
||||
{t('toggleBlock')}
|
||||
</span>
|
||||
</button>
|
||||
{header && (
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import flattenTopLevelFields from '../../../../utilities/flattenTopLevelFields';
|
||||
import Pill from '../Pill';
|
||||
import Plus from '../../icons/Plus';
|
||||
import X from '../../icons/X';
|
||||
import { Props } from './types';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -17,6 +19,7 @@ const ColumnSelector: React.FC<Props> = (props) => {
|
||||
} = props;
|
||||
|
||||
const [fields] = useState(() => flattenTopLevelFields(collection.fields, true));
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
@@ -42,7 +45,7 @@ const ColumnSelector: React.FC<Props> = (props) => {
|
||||
isEnabled && `${baseClass}__column--active`,
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
{field.label || field.name}
|
||||
{getTranslation(field.label || field.name, i18n)}
|
||||
</Pill>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Copy from '../../icons/Copy';
|
||||
import Tooltip from '../Tooltip';
|
||||
import { Props } from './types';
|
||||
@@ -9,12 +10,13 @@ const baseClass = 'copy-to-clipboard';
|
||||
|
||||
const CopyToClipboard: React.FC<Props> = ({
|
||||
value,
|
||||
defaultMessage = 'copy',
|
||||
successMessage = 'copied',
|
||||
defaultMessage,
|
||||
successMessage,
|
||||
}) => {
|
||||
const ref = useRef(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const { t } = useTranslation('general');
|
||||
|
||||
useEffect(() => {
|
||||
if (copied && !hovered) {
|
||||
@@ -49,8 +51,8 @@ const CopyToClipboard: React.FC<Props> = ({
|
||||
>
|
||||
<Copy />
|
||||
<Tooltip>
|
||||
{copied && successMessage}
|
||||
{!copied && defaultMessage}
|
||||
{copied && (successMessage ?? t('copied'))}
|
||||
{!copied && (defaultMessage ?? t('copy'))}
|
||||
</Tooltip>
|
||||
<textarea
|
||||
readOnly
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useCallback } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import Button from '../Button';
|
||||
import MinimalTemplate from '../../templates/Minimal';
|
||||
@@ -9,6 +10,7 @@ import { useForm } from '../../forms/Form/context';
|
||||
import useTitle from '../../../hooks/useTitle';
|
||||
import { requests } from '../../../api';
|
||||
import { Props } from './types';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -35,14 +37,15 @@ const DeleteDocument: React.FC<Props> = (props) => {
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const { toggleModal } = useModal();
|
||||
const history = useHistory();
|
||||
const { t, i18n } = useTranslation('general');
|
||||
const title = useTitle(useAsTitle) || id;
|
||||
const titleToRender = titleFromProps || title;
|
||||
|
||||
const modalSlug = `delete-${id}`;
|
||||
|
||||
const addDefaultError = useCallback(() => {
|
||||
toast.error(`There was an error while deleting ${title}. Please check your connection and try again.`);
|
||||
}, [title]);
|
||||
toast.error(t('error:deletingError', { title }));
|
||||
}, [t, title]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
setDeleting(true);
|
||||
@@ -50,13 +53,14 @@ const DeleteDocument: React.FC<Props> = (props) => {
|
||||
requests.delete(`${serverURL}${api}/${slug}/${id}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
}).then(async (res) => {
|
||||
try {
|
||||
const json = await res.json();
|
||||
if (res.status < 400) {
|
||||
toggleModal(modalSlug);
|
||||
toast.success(`${singular} "${title}" successfully deleted.`);
|
||||
toast.success(t('titleDeleted', { label: getTranslation(singular, i18n), title }));
|
||||
return history.push(`${admin}/collections/${slug}`);
|
||||
}
|
||||
|
||||
@@ -72,7 +76,7 @@ const DeleteDocument: React.FC<Props> = (props) => {
|
||||
return addDefaultError();
|
||||
}
|
||||
});
|
||||
}, [addDefaultError, toggleModal, modalSlug, history, id, singular, slug, title, admin, api, serverURL, setModified]);
|
||||
}, [setModified, serverURL, api, slug, id, toggleModal, modalSlug, t, singular, i18n, title, history, admin, addDefaultError]);
|
||||
|
||||
if (id) {
|
||||
return (
|
||||
@@ -87,24 +91,25 @@ const DeleteDocument: React.FC<Props> = (props) => {
|
||||
toggleModal(modalSlug);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
{t('delete')}
|
||||
</button>
|
||||
<Modal
|
||||
slug={modalSlug}
|
||||
className={baseClass}
|
||||
>
|
||||
<MinimalTemplate className={`${baseClass}__template`}>
|
||||
<h1>Confirm deletion</h1>
|
||||
<h1>{t('confirmDeletion')}</h1>
|
||||
<p>
|
||||
You are about to delete the
|
||||
{' '}
|
||||
{singular}
|
||||
{' '}
|
||||
"
|
||||
<strong>
|
||||
{titleToRender}
|
||||
</strong>
|
||||
". Are you sure?
|
||||
<Trans
|
||||
i18nKey="aboutToDelete"
|
||||
values={{ label: singular, title: titleToRender }}
|
||||
t={t}
|
||||
>
|
||||
aboutToDelete
|
||||
<strong>
|
||||
{titleToRender}
|
||||
</strong>
|
||||
</Trans>
|
||||
</p>
|
||||
<Button
|
||||
id="confirm-cancel"
|
||||
@@ -112,13 +117,13 @@ const DeleteDocument: React.FC<Props> = (props) => {
|
||||
type="button"
|
||||
onClick={deleting ? undefined : () => toggleModal(modalSlug)}
|
||||
>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={deleting ? undefined : handleDelete}
|
||||
id="confirm-delete"
|
||||
>
|
||||
{deleting ? 'Deleting...' : 'Confirm'}
|
||||
{deleting ? t('deleting') : t('confirm')}
|
||||
</Button>
|
||||
</MinimalTemplate>
|
||||
</Modal>
|
||||
|
||||
@@ -2,12 +2,14 @@ import React, { useCallback, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import { Props } from './types';
|
||||
import Button from '../Button';
|
||||
import { requests } from '../../../api';
|
||||
import { useForm, useFormModified } from '../../forms/Form/context';
|
||||
import MinimalTemplate from '../../templates/Minimal';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -21,6 +23,7 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
|
||||
const { serverURL, routes: { api }, localization } = useConfig();
|
||||
const { routes: { admin } } = useConfig();
|
||||
const [hasClicked, setHasClicked] = useState<boolean>(false);
|
||||
const { t, i18n } = useTranslation('general');
|
||||
|
||||
const modalSlug = `duplicate-${id}`;
|
||||
|
||||
@@ -34,8 +37,13 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
|
||||
|
||||
const create = async (locale = ''): Promise<string | null> => {
|
||||
const response = await requests.get(`${serverURL}${api}/${slug}/${id}`, {
|
||||
locale,
|
||||
depth: 0,
|
||||
params: {
|
||||
locale,
|
||||
depth: 0,
|
||||
},
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
});
|
||||
let data = await response.json();
|
||||
|
||||
@@ -49,6 +57,7 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
|
||||
const result = await requests.post(`${serverURL}${api}/${slug}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
@@ -70,8 +79,13 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
|
||||
.forEach(async (locale) => {
|
||||
if (!abort) {
|
||||
const res = await requests.get(`${serverURL}${api}/${slug}/${id}`, {
|
||||
locale,
|
||||
depth: 0,
|
||||
params: {
|
||||
locale,
|
||||
depth: 0,
|
||||
},
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
});
|
||||
let localizedDoc = await res.json();
|
||||
|
||||
@@ -85,6 +99,7 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
|
||||
const patchResult = await requests.patch(`${serverURL}${api}/${slug}/${duplicateID}?locale=${locale}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
body: JSON.stringify(localizedDoc),
|
||||
});
|
||||
@@ -97,13 +112,17 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
|
||||
});
|
||||
if (abort) {
|
||||
// delete the duplicate doc to prevent incomplete
|
||||
await requests.delete(`${serverURL}${api}/${slug}/${id}`);
|
||||
await requests.delete(`${serverURL}${api}/${slug}/${id}`, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
duplicateID = await create();
|
||||
}
|
||||
|
||||
toast.success(`${collection.labels.singular} successfully duplicated.`,
|
||||
toast.success(t('successfullyDuplicated', { label: getTranslation(collection.labels.singular, i18n) }),
|
||||
{ autoClose: 3000 });
|
||||
|
||||
setModified(false);
|
||||
@@ -113,7 +132,7 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
|
||||
pathname: `${admin}/collections/${slug}/${duplicateID}`,
|
||||
});
|
||||
}, 10);
|
||||
}, [modified, localization, collection, setModified, toggleModal, modalSlug, serverURL, api, slug, id, push, admin]);
|
||||
}, [modified, localization, t, i18n, collection, setModified, toggleModal, modalSlug, serverURL, api, slug, id, push, admin]);
|
||||
|
||||
const confirm = useCallback(async () => {
|
||||
setHasClicked(false);
|
||||
@@ -128,7 +147,7 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
|
||||
className={baseClass}
|
||||
onClick={() => handleClick(false)}
|
||||
>
|
||||
Duplicate
|
||||
{t('duplicate')}
|
||||
</Button>
|
||||
{modified && hasClicked && (
|
||||
<Modal
|
||||
@@ -136,9 +155,9 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
|
||||
className={`${baseClass}__modal`}
|
||||
>
|
||||
<MinimalTemplate className={`${baseClass}__modal-template`}>
|
||||
<h1>Confirm duplicate</h1>
|
||||
<h1>{t('confirmDuplication')}</h1>
|
||||
<p>
|
||||
You have unsaved changes. Would you like to continue to duplicate?
|
||||
{t('unsavedChangesDuplicate')}
|
||||
</p>
|
||||
<Button
|
||||
id="confirm-cancel"
|
||||
@@ -146,13 +165,13 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
|
||||
type="button"
|
||||
onClick={() => toggleModal(modalSlug)}
|
||||
>
|
||||
Cancel
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={confirm}
|
||||
id="confirm-duplicate"
|
||||
>
|
||||
Duplicate without saving changes
|
||||
{t('duplicateWithoutSaving')}
|
||||
</Button>
|
||||
</MinimalTemplate>
|
||||
</Modal>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import AnimateHeight from 'react-animate-height';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Thumbnail from '../Thumbnail';
|
||||
import Button from '../Button';
|
||||
import Meta from './Meta';
|
||||
import { Props } from './types';
|
||||
|
||||
import Chevron from '../../icons/Chevron';
|
||||
import { Props } from './types';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -35,6 +35,7 @@ const FileDetails: React.FC<Props> = (props) => {
|
||||
} = doc;
|
||||
|
||||
const [moreInfoOpen, setMoreInfoOpen] = useState(false);
|
||||
const { t } = useTranslation('upload');
|
||||
|
||||
const hasSizes = sizes && Object.keys(sizes)?.length > 0;
|
||||
|
||||
@@ -63,13 +64,13 @@ const FileDetails: React.FC<Props> = (props) => {
|
||||
>
|
||||
{!moreInfoOpen && (
|
||||
<React.Fragment>
|
||||
More info
|
||||
{t('moreInfo')}
|
||||
<Chevron />
|
||||
</React.Fragment>
|
||||
)}
|
||||
{moreInfoOpen && (
|
||||
<React.Fragment>
|
||||
Less info
|
||||
{t('lessInfo')}
|
||||
<Chevron />
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import Button from '../Button';
|
||||
import MinimalTemplate from '../../templates/Minimal';
|
||||
import { Props } from './types';
|
||||
@@ -18,13 +19,14 @@ const GenerateConfirmation: React.FC<Props> = (props) => {
|
||||
|
||||
const { id } = useDocumentInfo();
|
||||
const { toggleModal } = useModal();
|
||||
const { t } = useTranslation('authentication');
|
||||
|
||||
const modalSlug = `generate-confirmation-${id}`;
|
||||
|
||||
const handleGenerate = () => {
|
||||
setKey();
|
||||
toggleModal(modalSlug);
|
||||
toast.success('New API Key Generated.', { autoClose: 3000 });
|
||||
toast.success(t('newAPIKeyGenerated'), { autoClose: 3000 });
|
||||
highlightField(true);
|
||||
};
|
||||
|
||||
@@ -37,22 +39,22 @@ const GenerateConfirmation: React.FC<Props> = (props) => {
|
||||
toggleModal(modalSlug);
|
||||
}}
|
||||
>
|
||||
Generate new API key
|
||||
{t('generateNewAPIKey')}
|
||||
</Button>
|
||||
<Modal
|
||||
slug={modalSlug}
|
||||
className={baseClass}
|
||||
>
|
||||
<MinimalTemplate className={`${baseClass}__template`}>
|
||||
<h1>Confirm Generation</h1>
|
||||
<h1>{t('confirmGeneration')}</h1>
|
||||
<p>
|
||||
Generating a new API key will
|
||||
{' '}
|
||||
<strong>invalidate</strong>
|
||||
{' '}
|
||||
the previous key.
|
||||
{' '}
|
||||
Are you sure you wish to continue?
|
||||
<Trans
|
||||
i18nKey="generatingNewAPIKeyWillInvalidate"
|
||||
t={t}
|
||||
>
|
||||
generatingNewAPIKeyWillInvalidate
|
||||
<strong>invalidate</strong>
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<Button
|
||||
@@ -62,12 +64,12 @@ const GenerateConfirmation: React.FC<Props> = (props) => {
|
||||
toggleModal(modalSlug);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
{t('general:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleGenerate}
|
||||
>
|
||||
Generate
|
||||
{t('generate')}
|
||||
</Button>
|
||||
</MinimalTemplate>
|
||||
</Modal>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import AnimateHeight from 'react-animate-height';
|
||||
import { FieldAffectingData, fieldAffectsData } from '../../../../fields/config/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { fieldAffectsData } from '../../../../fields/config/types';
|
||||
import SearchFilter from '../SearchFilter';
|
||||
import ColumnSelector from '../ColumnSelector';
|
||||
import WhereBuilder from '../WhereBuilder';
|
||||
@@ -10,6 +11,7 @@ import { Props } from './types';
|
||||
import { useSearchParams } from '../../utilities/SearchParams';
|
||||
import validateWhereQuery from '../WhereBuilder/validateWhereQuery';
|
||||
import { getTextFieldsToBeSearched } from './getTextFieldsToBeSearched';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -40,6 +42,7 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
const [titleField] = useState(() => fields.find((field) => fieldAffectsData(field) && field.name === useAsTitle));
|
||||
const [textFieldsToBeSearched] = useState(getTextFieldsToBeSearched(listSearchableFields, fields));
|
||||
const [visibleDrawer, setVisibleDrawer] = useState<'where' | 'sort' | 'columns'>(shouldInitializeWhereOpened ? 'where' : undefined);
|
||||
const { t, i18n } = useTranslation('general');
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
@@ -48,7 +51,7 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
fieldName={titleField && fieldAffectsData(titleField) ? titleField.name : undefined}
|
||||
handleChange={handleWhereChange}
|
||||
modifySearchQuery={modifySearchQuery}
|
||||
fieldLabel={titleField && fieldAffectsData(titleField) && titleField.label ? titleField.label : undefined}
|
||||
fieldLabel={(titleField && fieldAffectsData(titleField) && getTranslation(titleField.label || titleField.name, i18n)) ?? undefined}
|
||||
listSearchableFields={textFieldsToBeSearched}
|
||||
/>
|
||||
<div className={`${baseClass}__buttons`}>
|
||||
@@ -61,7 +64,7 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
icon="chevron"
|
||||
iconStyle="none"
|
||||
>
|
||||
Columns
|
||||
{t('columns')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
@@ -71,7 +74,7 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
icon="chevron"
|
||||
iconStyle="none"
|
||||
>
|
||||
Filters
|
||||
{t('filters')}
|
||||
</Button>
|
||||
{enableSort && (
|
||||
<Button
|
||||
@@ -81,7 +84,7 @@ const ListControls: React.FC<Props> = (props) => {
|
||||
icon="chevron"
|
||||
iconStyle="none"
|
||||
>
|
||||
Sort
|
||||
{t('sort')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import qs from 'qs';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import { useLocale } from '../../utilities/Locale';
|
||||
import { useSearchParams } from '../../utilities/SearchParams';
|
||||
@@ -15,6 +16,7 @@ const Localizer: React.FC<Props> = () => {
|
||||
const { localization } = useConfig();
|
||||
const locale = useLocale();
|
||||
const searchParams = useSearchParams();
|
||||
const { t } = useTranslation('general');
|
||||
|
||||
if (localization) {
|
||||
const { locales } = localization;
|
||||
@@ -26,7 +28,7 @@ const Localizer: React.FC<Props> = () => {
|
||||
button={locale}
|
||||
render={({ close }) => (
|
||||
<div>
|
||||
<span>Locales</span>
|
||||
<span>{t('locales')}</span>
|
||||
<ul>
|
||||
{locales.map((localeOption) => {
|
||||
const baseLocaleClass = `${baseClass}__locale`;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { NavLink, Link, useHistory } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import { useAuth } from '../../utilities/Auth';
|
||||
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
|
||||
@@ -10,10 +11,11 @@ import Icon from '../../graphics/Icon';
|
||||
import Account from '../../graphics/Account';
|
||||
import Localizer from '../Localizer';
|
||||
import NavGroup from '../NavGroup';
|
||||
import Logout from '../Logout';
|
||||
import { groupNavItems, Group, EntityToGroup, EntityType } from '../../../utilities/groupNavItems';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
import Logout from '../Logout';
|
||||
|
||||
const baseClass = 'nav';
|
||||
|
||||
@@ -22,6 +24,7 @@ const DefaultNav = () => {
|
||||
const [menuActive, setMenuActive] = useState(false);
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
const history = useHistory();
|
||||
const { i18n } = useTranslation('general');
|
||||
const {
|
||||
collections,
|
||||
globals,
|
||||
@@ -31,7 +34,7 @@ const DefaultNav = () => {
|
||||
admin: {
|
||||
components: {
|
||||
beforeNavLinks,
|
||||
afterNavLinks
|
||||
afterNavLinks,
|
||||
},
|
||||
},
|
||||
} = useConfig();
|
||||
@@ -60,8 +63,8 @@ const DefaultNav = () => {
|
||||
|
||||
return entityToGroup;
|
||||
}),
|
||||
], permissions));
|
||||
}, [collections, globals, permissions]);
|
||||
], permissions, i18n));
|
||||
}, [collections, globals, permissions, i18n, i18n.language]);
|
||||
|
||||
useEffect(() => history.listen(() => {
|
||||
setMenuActive(false);
|
||||
@@ -102,13 +105,13 @@ const DefaultNav = () => {
|
||||
|
||||
if (type === EntityType.collection) {
|
||||
href = `${admin}/collections/${entity.slug}`;
|
||||
entityLabel = entity.labels.plural;
|
||||
entityLabel = getTranslation(entity.labels.plural, i18n);
|
||||
id = `nav-${entity.slug}`;
|
||||
}
|
||||
|
||||
if (type === EntityType.global) {
|
||||
href = `${admin}/globals/${entity.slug}`;
|
||||
entityLabel = entity.label;
|
||||
entityLabel = getTranslation(entity.label, i18n);
|
||||
id = `nav-global-${entity.slug}`;
|
||||
}
|
||||
|
||||
@@ -137,7 +140,7 @@ const DefaultNav = () => {
|
||||
>
|
||||
<Account />
|
||||
</Link>
|
||||
<Logout/>
|
||||
<Logout />
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import qs from 'qs';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams } from '../../utilities/SearchParams';
|
||||
import Popup from '../Popup';
|
||||
import Chevron from '../../icons/Chevron';
|
||||
@@ -22,6 +23,7 @@ type Props = {
|
||||
const PerPage: React.FC<Props> = ({ limits = defaultLimits, limit, handleChange, modifySearchParams = true }) => {
|
||||
const params = useSearchParams();
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation('general');
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
@@ -29,9 +31,7 @@ const PerPage: React.FC<Props> = ({ limits = defaultLimits, limit, handleChange,
|
||||
horizontalAlign="right"
|
||||
button={(
|
||||
<strong>
|
||||
Per Page:
|
||||
{' '}
|
||||
{limit}
|
||||
{t('perPage', { limit })}
|
||||
<Chevron />
|
||||
</strong>
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,6 @@ import PopupButton from './PopupButton';
|
||||
import useIntersect from '../../../hooks/useIntersect';
|
||||
|
||||
import './index.scss';
|
||||
import useThrottledEffect from '../../../hooks/useThrottledEffect';
|
||||
|
||||
const baseClass = 'popup';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../utilities/Auth';
|
||||
import Button from '../Button';
|
||||
import { Props } from './types';
|
||||
@@ -18,6 +19,7 @@ const PreviewButton: React.FC<Props> = (props) => {
|
||||
|
||||
const locale = useLocale();
|
||||
const { token } = useAuth();
|
||||
const { t } = useTranslation('version');
|
||||
|
||||
useEffect(() => {
|
||||
if (generatePreviewURL && typeof generatePreviewURL === 'function') {
|
||||
@@ -44,7 +46,7 @@ const PreviewButton: React.FC<Props> = (props) => {
|
||||
url={url}
|
||||
newTab
|
||||
>
|
||||
Preview
|
||||
{t('preview')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FormSubmit from '../../forms/Submit';
|
||||
import { Props } from './types';
|
||||
import { useDocumentInfo } from '../../utilities/DocumentInfo';
|
||||
@@ -8,6 +9,7 @@ const Publish: React.FC<Props> = () => {
|
||||
const { unpublishedVersions, publishedDoc } = useDocumentInfo();
|
||||
const { submit } = useForm();
|
||||
const modified = useFormModified();
|
||||
const { t } = useTranslation('version');
|
||||
|
||||
const hasNewerVersions = unpublishedVersions?.totalDocs > 0;
|
||||
const canPublish = modified || hasNewerVersions || !publishedDoc;
|
||||
@@ -26,7 +28,7 @@ const Publish: React.FC<Props> = () => {
|
||||
onClick={publish}
|
||||
disabled={!canPublish}
|
||||
>
|
||||
Publish changes
|
||||
{t('publishChanges')}
|
||||
</FormSubmit>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,9 +12,11 @@ import {
|
||||
SortEndHandler,
|
||||
SortableHandle,
|
||||
} from 'react-sortable-hoc';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { arrayMove } from '../../../../utilities/arrayMove';
|
||||
import { Props, Value } from './types';
|
||||
import Chevron from '../../icons/Chevron';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -64,6 +66,8 @@ const ReactSelect: React.FC<Props> = (props) => {
|
||||
filterOption = undefined,
|
||||
} = props;
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const classes = [
|
||||
className,
|
||||
'react-select',
|
||||
@@ -92,7 +96,7 @@ const ReactSelect: React.FC<Props> = (props) => {
|
||||
// small fix for https://github.com/clauderic/react-sortable-hoc/pull/352:
|
||||
getHelperDimensions={({ node }) => node.getBoundingClientRect()}
|
||||
// react-select props:
|
||||
placeholder={placeholder}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
{...props}
|
||||
value={value as Value[]}
|
||||
onChange={onChange}
|
||||
@@ -117,7 +121,7 @@ const ReactSelect: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<Select
|
||||
placeholder={placeholder}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
captureMenuScroll
|
||||
{...props}
|
||||
value={value}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import FormSubmit from '../../forms/Submit';
|
||||
import { useForm, useFormModified } from '../../forms/Form/context';
|
||||
@@ -15,6 +16,7 @@ const SaveDraft: React.FC = () => {
|
||||
const { collection, global, id } = useDocumentInfo();
|
||||
const modified = useFormModified();
|
||||
const locale = useLocale();
|
||||
const { t } = useTranslation('version');
|
||||
|
||||
const canSaveDraft = modified;
|
||||
|
||||
@@ -50,7 +52,7 @@ const SaveDraft: React.FC = () => {
|
||||
onClick={saveDraft}
|
||||
disabled={!canSaveDraft}
|
||||
>
|
||||
Save draft
|
||||
{t('saveDraft')}
|
||||
</FormSubmit>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import queryString from 'qs';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Props } from './types';
|
||||
import Search from '../../icons/Search';
|
||||
import useDebounce from '../../../hooks/useDebounce';
|
||||
import { useSearchParams } from '../../utilities/SearchParams';
|
||||
import { Where, WhereField } from '../../../../types';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -22,11 +24,12 @@ const SearchFilter: React.FC<Props> = (props) => {
|
||||
|
||||
const params = useSearchParams();
|
||||
const history = useHistory();
|
||||
const { t, i18n } = useTranslation('general');
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [previousSearch, setPreviousSearch] = useState('');
|
||||
|
||||
const placeholder = useRef(`Search by ${fieldLabel}`);
|
||||
const placeholder = useRef(t('searchBy', { label: getTranslation(fieldLabel, i18n) }));
|
||||
|
||||
const debouncedSearch = useDebounce(search, 300);
|
||||
|
||||
@@ -78,12 +81,12 @@ const SearchFilter: React.FC<Props> = (props) => {
|
||||
if (listSearchableFields?.length > 0) {
|
||||
placeholder.current = listSearchableFields.reduce<string>((prev, curr, i) => {
|
||||
if (i === listSearchableFields.length - 1) {
|
||||
return `${prev} or ${curr.label || curr.name}`;
|
||||
return `${prev} ${t('or')} ${getTranslation(curr.label || curr.name, i18n)}`;
|
||||
}
|
||||
return `${prev}, ${curr.label || curr.name}`;
|
||||
return `${prev}, ${getTranslation(curr.label || curr.name, i18n)}`;
|
||||
}, placeholder.current);
|
||||
}
|
||||
}, [listSearchableFields]);
|
||||
}, [t, listSearchableFields, i18n]);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import queryString from 'qs';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Props } from './types';
|
||||
import Chevron from '../../icons/Chevron';
|
||||
import Button from '../Button';
|
||||
import { useSearchParams } from '../../utilities/SearchParams';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
import { useSearchParams } from '../../utilities/SearchParams';
|
||||
|
||||
const baseClass = 'sort-column';
|
||||
|
||||
@@ -16,6 +18,7 @@ const SortColumn: React.FC<Props> = (props) => {
|
||||
} = props;
|
||||
const params = useSearchParams();
|
||||
const history = useHistory();
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const { sort } = params;
|
||||
|
||||
@@ -39,7 +42,7 @@ const SortColumn: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<span className={`${baseClass}__label`}>{label}</span>
|
||||
<span className={`${baseClass}__label`}>{getTranslation(label, i18n)}</span>
|
||||
{!disable && (
|
||||
<span className={`${baseClass}__buttons`}>
|
||||
<Button
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export type Props = {
|
||||
label: string,
|
||||
label: Record<string, string> | string,
|
||||
name: string,
|
||||
disable?: boolean,
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import queryString from 'qs';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Props } from './types';
|
||||
import ReactSelect from '../ReactSelect';
|
||||
import sortableFieldTypes from '../../../../fields/sortableFieldTypes';
|
||||
import { useSearchParams } from '../../utilities/SearchParams';
|
||||
import { fieldAffectsData } from '../../../../fields/config/types';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -22,19 +24,20 @@ const SortComplex: React.FC<Props> = (props) => {
|
||||
|
||||
const history = useHistory();
|
||||
const params = useSearchParams();
|
||||
const { t, i18n } = useTranslation('general');
|
||||
|
||||
const [sortFields] = useState(() => collection.fields.reduce((fields, field) => {
|
||||
if (fieldAffectsData(field) && sortableFieldTypes.indexOf(field.type) > -1) {
|
||||
return [
|
||||
...fields,
|
||||
{ label: field.label, value: field.name },
|
||||
{ label: getTranslation(field.label || field.name, i18n), value: field.name },
|
||||
];
|
||||
}
|
||||
return fields;
|
||||
}, []));
|
||||
|
||||
const [sortField, setSortField] = useState(sortFields[0]);
|
||||
const [sortOrder, setSortOrder] = useState({ label: 'Descending', value: '-' });
|
||||
const [sortOrder, setSortOrder] = useState({ label: t('descending'), value: '-' });
|
||||
|
||||
useEffect(() => {
|
||||
if (sortField?.value) {
|
||||
@@ -59,7 +62,7 @@ const SortComplex: React.FC<Props> = (props) => {
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<div className={`${baseClass}__select`}>
|
||||
<div className={`${baseClass}__label`}>
|
||||
Column to Sort
|
||||
{t('columnToSort')}
|
||||
</div>
|
||||
<ReactSelect
|
||||
value={sortField}
|
||||
@@ -69,7 +72,7 @@ const SortComplex: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
<div className={`${baseClass}__select`}>
|
||||
<div className={`${baseClass}__label`}>
|
||||
Order
|
||||
{t('order')}
|
||||
</div>
|
||||
<ReactSelect
|
||||
value={sortOrder}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import { Props } from './types';
|
||||
import { useDocumentInfo } from '../../utilities/DocumentInfo';
|
||||
@@ -16,12 +17,23 @@ import './index.scss';
|
||||
const baseClass = 'status';
|
||||
|
||||
const Status: React.FC<Props> = () => {
|
||||
const { publishedDoc, unpublishedVersions, collection, global, id, getVersions } = useDocumentInfo();
|
||||
const {
|
||||
publishedDoc,
|
||||
unpublishedVersions,
|
||||
collection,
|
||||
global,
|
||||
id,
|
||||
getVersions,
|
||||
} = useDocumentInfo();
|
||||
const { toggleModal } = useModal();
|
||||
const { serverURL, routes: { api } } = useConfig();
|
||||
const {
|
||||
serverURL,
|
||||
routes: { api },
|
||||
} = useConfig();
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const { reset: resetForm } = useForm();
|
||||
const locale = useLocale();
|
||||
const { t, i18n } = useTranslation('version');
|
||||
|
||||
const unPublishModalSlug = `confirm-un-publish-${id}`;
|
||||
const revertModalSlug = `confirm-revert-${id}`;
|
||||
@@ -29,11 +41,11 @@ const Status: React.FC<Props> = () => {
|
||||
let statusToRender;
|
||||
|
||||
if (unpublishedVersions?.docs?.length > 0 && publishedDoc) {
|
||||
statusToRender = 'Changed';
|
||||
statusToRender = t('changed');
|
||||
} else if (!publishedDoc) {
|
||||
statusToRender = 'Draft';
|
||||
statusToRender = t('draft');
|
||||
} else if (publishedDoc && unpublishedVersions?.docs?.length <= 1) {
|
||||
statusToRender = 'Published';
|
||||
statusToRender = t('published');
|
||||
}
|
||||
|
||||
const performAction = useCallback(async (action: 'revert' | 'unpublish') => {
|
||||
@@ -65,6 +77,7 @@ const Status: React.FC<Props> = () => {
|
||||
const res = await requests[method](url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
@@ -88,7 +101,7 @@ const Status: React.FC<Props> = () => {
|
||||
toast.success(json.message);
|
||||
getVersions();
|
||||
} else {
|
||||
toast.error('There was a problem while un-publishing this document.');
|
||||
toast.error(t('unPublishingDocument'));
|
||||
}
|
||||
|
||||
setProcessing(false);
|
||||
@@ -99,7 +112,7 @@ const Status: React.FC<Props> = () => {
|
||||
if (action === 'unpublish') {
|
||||
toggleModal(unPublishModalSlug);
|
||||
}
|
||||
}, [collection, global, publishedDoc, serverURL, api, id, locale, resetForm, getVersions, toggleModal, revertModalSlug, unPublishModalSlug]);
|
||||
}, [collection, global, publishedDoc, serverURL, api, id, i18n, locale, resetForm, getVersions, t, toggleModal, revertModalSlug, unPublishModalSlug]);
|
||||
|
||||
if (statusToRender) {
|
||||
return (
|
||||
@@ -114,26 +127,26 @@ const Status: React.FC<Props> = () => {
|
||||
className={`${baseClass}__action`}
|
||||
buttonStyle="none"
|
||||
>
|
||||
Unpublish
|
||||
{t('unpublish')}
|
||||
</Button>
|
||||
<Modal
|
||||
slug={unPublishModalSlug}
|
||||
className={`${baseClass}__modal`}
|
||||
>
|
||||
<MinimalTemplate className={`${baseClass}__modal-template`}>
|
||||
<h1>Confirm unpublish</h1>
|
||||
<p>You are about to unpublish this document. Are you sure?</p>
|
||||
<h1>{t('confirmUnpublish')}</h1>
|
||||
<p>{t('aboutToUnpublish')}</p>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
type="button"
|
||||
onClick={processing ? undefined : () => toggleModal(unPublishModalSlug)}
|
||||
>
|
||||
Cancel
|
||||
{t('general:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={processing ? undefined : () => performAction('unpublish')}
|
||||
>
|
||||
{processing ? 'Unpublishing...' : 'Confirm'}
|
||||
{t(processing ? 'unpublishing' : 'general:confirm')}
|
||||
</Button>
|
||||
</MinimalTemplate>
|
||||
</Modal>
|
||||
@@ -147,26 +160,26 @@ const Status: React.FC<Props> = () => {
|
||||
className={`${baseClass}__action`}
|
||||
buttonStyle="none"
|
||||
>
|
||||
Revert to published
|
||||
{t('revertToPublished')}
|
||||
</Button>
|
||||
<Modal
|
||||
slug={revertModalSlug}
|
||||
className={`${baseClass}__modal`}
|
||||
>
|
||||
<MinimalTemplate className={`${baseClass}__modal-template`}>
|
||||
<h1>Confirm revert to saved</h1>
|
||||
<p>You are about to revert this document's changes to its published state. Are you sure?</p>
|
||||
<h1>{t('confirmRevertToSaved')}</h1>
|
||||
<p>{t('aboutToRevertToPublished')}</p>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
type="button"
|
||||
onClick={processing ? undefined : () => toggleModal(revertModalSlug)}
|
||||
>
|
||||
Cancel
|
||||
{t('general:published')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={processing ? undefined : () => performAction('revert')}
|
||||
>
|
||||
{processing ? 'Reverting...' : 'Confirm'}
|
||||
{t(processing ? 'reverting' : 'general:confirm')}
|
||||
</Button>
|
||||
</MinimalTemplate>
|
||||
</Modal>
|
||||
|
||||
@@ -2,8 +2,10 @@ import React, {
|
||||
useState, createContext, useContext,
|
||||
} from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Chevron from '../../icons/Chevron';
|
||||
import { Context as ContextType } from './types';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -26,7 +28,8 @@ const StepNavProvider: React.FC<{children?: React.ReactNode}> = ({ children }) =
|
||||
const useStepNav = (): ContextType => useContext(Context);
|
||||
|
||||
const StepNav: React.FC = () => {
|
||||
const dashboardLabel = <span>Dashboard</span>;
|
||||
const { t, i18n } = useTranslation();
|
||||
const dashboardLabel = <span>{t('general:dashboard')}</span>;
|
||||
const { stepNav } = useStepNav();
|
||||
|
||||
return (
|
||||
@@ -40,7 +43,7 @@ const StepNav: React.FC = () => {
|
||||
)
|
||||
: dashboardLabel}
|
||||
{stepNav.map((item, i) => {
|
||||
const StepLabel = <span key={i}>{item.label}</span>;
|
||||
const StepLabel = <span key={i}>{getTranslation(item.label, i18n)}</span>;
|
||||
|
||||
const Step = stepNav.length === i + 1
|
||||
? StepLabel
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export type StepNavItem = {
|
||||
label: string
|
||||
label: Record<string, string> | string
|
||||
url?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Props } from './types';
|
||||
import Thumbnail from '../Thumbnail';
|
||||
|
||||
@@ -15,6 +16,8 @@ const UploadCard: React.FC<Props> = (props) => {
|
||||
collection,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation('general');
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
className,
|
||||
@@ -32,7 +35,7 @@ const UploadCard: React.FC<Props> = (props) => {
|
||||
collection={collection}
|
||||
/>
|
||||
<div className={`${baseClass}__filename`}>
|
||||
{typeof doc?.filename === 'string' ? doc?.filename : '[Untitled]'}
|
||||
{typeof doc?.filename === 'string' ? doc?.filename : `[${t('untitled')}]`}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import Button from '../Button';
|
||||
import { Props } from './types';
|
||||
@@ -14,6 +15,7 @@ const baseClass = 'versions-count';
|
||||
const VersionsCount: React.FC<Props> = ({ collection, global, id }) => {
|
||||
const { routes: { admin } } = useConfig();
|
||||
const { versions, publishedDoc, unpublishedVersions } = useDocumentInfo();
|
||||
const { t } = useTranslation('version');
|
||||
|
||||
// Doc status could come from three places:
|
||||
// 1. the newest unpublished version (a draft)
|
||||
@@ -44,7 +46,7 @@ const VersionsCount: React.FC<Props> = ({ collection, global, id }) => {
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
{versionCount === 0 && 'No versions found'}
|
||||
{versionCount === 0 && t('versionCount_none')}
|
||||
{versionCount > 0 && (
|
||||
<Button
|
||||
className={`${baseClass}__button`}
|
||||
@@ -52,12 +54,7 @@ const VersionsCount: React.FC<Props> = ({ collection, global, id }) => {
|
||||
el="link"
|
||||
to={versionsURL}
|
||||
>
|
||||
{versionCount}
|
||||
{' '}
|
||||
version
|
||||
{versionCount > 1 && 's'}
|
||||
{' '}
|
||||
found
|
||||
{t('versionCount', { count: versionCount })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Props, isComponent } from './types';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
import './index.scss';
|
||||
|
||||
const ViewDescription: React.FC<Props> = (props) => {
|
||||
const { i18n } = useTranslation();
|
||||
const {
|
||||
description,
|
||||
} = props;
|
||||
@@ -17,7 +20,7 @@ const ViewDescription: React.FC<Props> = (props) => {
|
||||
<div
|
||||
className="view-description"
|
||||
>
|
||||
{typeof description === 'function' ? description() : description}
|
||||
{typeof description === 'function' ? description() : getTranslation(description, i18n) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export type DescriptionFunction = () => string
|
||||
|
||||
export type DescriptionComponent = React.ComponentType<any>
|
||||
|
||||
type Description = string | DescriptionFunction | DescriptionComponent
|
||||
type Description = Record<string, string> | string | DescriptionFunction | DescriptionComponent
|
||||
|
||||
export type Props = {
|
||||
description?: Description
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Props } from './types';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'condition-value-number';
|
||||
|
||||
const NumberField: React.FC<Props> = ({ onChange, value }) => (
|
||||
<input
|
||||
placeholder="Enter a value"
|
||||
className={baseClass}
|
||||
type="number"
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
const NumberField: React.FC<Props> = ({ onChange, value }) => {
|
||||
const { t } = useTranslation('general');
|
||||
return (
|
||||
<input
|
||||
placeholder={t('enterAValue')}
|
||||
className={baseClass}
|
||||
type="number"
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NumberField;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useReducer, useState, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../../../utilities/Config';
|
||||
import { Props, Option, ValueWithRelation, GetResults } from './types';
|
||||
import optionsReducer from './optionsReducer';
|
||||
@@ -32,11 +33,12 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
const [errorLoading, setErrorLoading] = useState('');
|
||||
const [hasLoadedFirstOptions, setHasLoadedFirstOptions] = useState(false);
|
||||
const debouncedSearch = useDebounce(search, 300);
|
||||
const { t, i18n } = useTranslation('general');
|
||||
|
||||
const addOptions = useCallback((data, relation) => {
|
||||
const collection = collections.find((coll) => coll.slug === relation);
|
||||
dispatchOptions({ type: 'ADD', data, relation, hasMultipleRelations, collection });
|
||||
}, [collections, hasMultipleRelations]);
|
||||
dispatchOptions({ type: 'ADD', data, relation, hasMultipleRelations, collection, i18n });
|
||||
}, [collections, hasMultipleRelations, i18n]);
|
||||
|
||||
const getResults = useCallback<GetResults>(async ({
|
||||
lastFullyLoadedRelation: lastFullyLoadedRelationArg,
|
||||
@@ -60,10 +62,15 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
const fieldToSearch = collection?.admin?.useAsTitle || 'id';
|
||||
const searchParam = searchArg ? `&where[${fieldToSearch}][like]=${searchArg}` : '';
|
||||
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPageToUse}&depth=0${searchParam}`, { credentials: 'include' });
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?limit=${maxResultsPerRequest}&page=${lastLoadedPageToUse}&depth=0${searchParam}`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data: PaginatedDocs<any> = await response.json();
|
||||
const data: PaginatedDocs = await response.json();
|
||||
if (data.docs.length > 0) {
|
||||
resultsFetched += data.docs.length;
|
||||
addOptions(data, relation);
|
||||
@@ -80,12 +87,12 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setErrorLoading('An error has occurred.');
|
||||
setErrorLoading(t('errors:unspecific'));
|
||||
}
|
||||
}
|
||||
}, Promise.resolve());
|
||||
}
|
||||
}, [addOptions, api, collections, serverURL, errorLoading, relationTo]);
|
||||
}, [i18n, relationTo, errorLoading, collections, serverURL, api, addOptions, t]);
|
||||
|
||||
const findOptionsByValue = useCallback((): Option | Option[] => {
|
||||
if (value) {
|
||||
@@ -152,16 +159,21 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
|
||||
const addOptionByID = useCallback(async (id, relation) => {
|
||||
if (!errorLoading && id !== 'null') {
|
||||
const response = await fetch(`${serverURL}${api}/${relation}/${id}?depth=0`, { credentials: 'include' });
|
||||
const response = await fetch(`${serverURL}${api}/${relation}/${id}?depth=0`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
addOptions({ docs: [data] }, relation);
|
||||
} else {
|
||||
console.error(`There was a problem loading the document with ID of ${id}.`);
|
||||
console.error(t('error:loadingDocument', { id }));
|
||||
}
|
||||
}
|
||||
}, [addOptions, api, errorLoading, serverURL]);
|
||||
}, [i18n, addOptions, api, errorLoading, serverURL, t]);
|
||||
|
||||
// ///////////////////////////
|
||||
// Get results when search input changes
|
||||
@@ -171,13 +183,14 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
dispatchOptions({
|
||||
type: 'CLEAR',
|
||||
required: true,
|
||||
i18n,
|
||||
});
|
||||
|
||||
setHasLoadedFirstOptions(true);
|
||||
setLastLoadedPage(1);
|
||||
setLastFullyLoadedRelation(-1);
|
||||
getResults({ search: debouncedSearch });
|
||||
}, [getResults, debouncedSearch, relationTo]);
|
||||
}, [getResults, debouncedSearch, relationTo, i18n]);
|
||||
|
||||
// ///////////////////////////
|
||||
// Format options once first options have been retrieved
|
||||
@@ -224,7 +237,7 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
<div className={classes}>
|
||||
{!errorLoading && (
|
||||
<ReactSelect
|
||||
placeholder="Select a value"
|
||||
placeholder={t('selectValue')}
|
||||
onInputChange={handleInputChange}
|
||||
onChange={(selected) => {
|
||||
if (hasMany) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Option, Action } from './types';
|
||||
import { getTranslation } from '../../../../../../utilities/getTranslation';
|
||||
|
||||
const reduceToIDs = (options) => options.reduce((ids, option) => {
|
||||
if (option.options) {
|
||||
@@ -17,11 +18,11 @@ const reduceToIDs = (options) => options.reduce((ids, option) => {
|
||||
const optionsReducer = (state: Option[], action: Action): Option[] => {
|
||||
switch (action.type) {
|
||||
case 'CLEAR': {
|
||||
return action.required ? [] : [{ value: 'null', label: 'None' }];
|
||||
return action.required ? [] : [{ value: 'null', label: action.i18n.t('general:none') }];
|
||||
}
|
||||
|
||||
case 'ADD': {
|
||||
const { hasMultipleRelations, collection, relation, data } = action;
|
||||
const { hasMultipleRelations, collection, relation, data, i18n } = action;
|
||||
|
||||
const labelKey = collection.admin.useAsTitle || 'id';
|
||||
|
||||
@@ -47,7 +48,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
|
||||
}
|
||||
|
||||
const newOptions = [...state];
|
||||
const optionsToAddTo = newOptions.find((optionGroup) => optionGroup.label === collection.labels.plural);
|
||||
const optionsToAddTo = newOptions.find((optionGroup) => optionGroup.label === getTranslation(collection.labels.plural, i18n));
|
||||
|
||||
const newSubOptions = data.docs.reduce((docs, doc) => {
|
||||
if (loadedIDs.indexOf(doc.id) === -1) {
|
||||
@@ -73,7 +74,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
|
||||
];
|
||||
} else {
|
||||
newOptions.push({
|
||||
label: collection.labels.plural,
|
||||
label: getTranslation(collection.labels.plural, i18n),
|
||||
options: newSubOptions,
|
||||
value: undefined,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import i18n from 'i18next';
|
||||
import { RelationshipField } from '../../../../../../fields/config/types';
|
||||
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
|
||||
import { PaginatedDocs } from '../../../../../../mongoose/types';
|
||||
@@ -17,6 +18,7 @@ export type Option = {
|
||||
type CLEAR = {
|
||||
type: 'CLEAR'
|
||||
required: boolean
|
||||
i18n: typeof i18n
|
||||
}
|
||||
|
||||
type ADD = {
|
||||
@@ -25,6 +27,7 @@ type ADD = {
|
||||
relation: string
|
||||
hasMultipleRelations: boolean
|
||||
collection: SanitizedCollectionConfig
|
||||
i18n: typeof i18n
|
||||
}
|
||||
|
||||
export type Action = CLEAR | ADD
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useReducer } from 'react';
|
||||
import queryString from 'qs';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Props } from './types';
|
||||
import useThrottledEffect from '../../../hooks/useThrottledEffect';
|
||||
import Button from '../Button';
|
||||
@@ -11,15 +12,16 @@ import flattenTopLevelFields from '../../../../utilities/flattenTopLevelFields';
|
||||
import { useSearchParams } from '../../utilities/SearchParams';
|
||||
import validateWhereQuery from './validateWhereQuery';
|
||||
import { Where } from '../../../../types';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'where-builder';
|
||||
|
||||
const reduceFields = (fields) => flattenTopLevelFields(fields).reduce((reduced, field) => {
|
||||
const reduceFields = (fields, i18n) => flattenTopLevelFields(fields).reduce((reduced, field) => {
|
||||
if (typeof fieldTypes[field.type] === 'object') {
|
||||
const formattedField = {
|
||||
label: field.label,
|
||||
label: getTranslation(field.label || field.name, i18n),
|
||||
value: field.name,
|
||||
...fieldTypes[field.type],
|
||||
props: {
|
||||
@@ -50,6 +52,7 @@ const WhereBuilder: React.FC<Props> = (props) => {
|
||||
|
||||
const history = useHistory();
|
||||
const params = useSearchParams();
|
||||
const { t, i18n } = useTranslation('general');
|
||||
|
||||
const [conditions, dispatchConditions] = useReducer(reducer, params.where, (whereFromSearch) => {
|
||||
if (modifySearchQuery && validateWhereQuery(whereFromSearch)) {
|
||||
@@ -59,7 +62,7 @@ const WhereBuilder: React.FC<Props> = (props) => {
|
||||
return [];
|
||||
});
|
||||
|
||||
const [reducedFields] = useState(() => reduceFields(collection.fields));
|
||||
const [reducedFields] = useState(() => reduceFields(collection.fields, i18n));
|
||||
|
||||
useThrottledEffect(() => {
|
||||
const currentParams = queryString.parse(history.location.search, { ignoreQueryPrefix: true, depth: 10 }) as { where: Where };
|
||||
@@ -104,18 +107,14 @@ const WhereBuilder: React.FC<Props> = (props) => {
|
||||
{conditions.length > 0 && (
|
||||
<React.Fragment>
|
||||
<div className={`${baseClass}__label`}>
|
||||
Filter
|
||||
{' '}
|
||||
{plural}
|
||||
{' '}
|
||||
where
|
||||
{t('filterWhere', { label: getTranslation(plural, i18n) }) }
|
||||
</div>
|
||||
<ul className={`${baseClass}__or-filters`}>
|
||||
{conditions.map((or, orIndex) => (
|
||||
<li key={orIndex}>
|
||||
{orIndex !== 0 && (
|
||||
<div className={`${baseClass}__label`}>
|
||||
Or
|
||||
{t('or')}
|
||||
</div>
|
||||
)}
|
||||
<ul className={`${baseClass}__and-filters`}>
|
||||
@@ -123,7 +122,7 @@ const WhereBuilder: React.FC<Props> = (props) => {
|
||||
<li key={andIndex}>
|
||||
{andIndex !== 0 && (
|
||||
<div className={`${baseClass}__label`}>
|
||||
And
|
||||
{t('and')}
|
||||
</div>
|
||||
)}
|
||||
<Condition
|
||||
@@ -148,13 +147,13 @@ const WhereBuilder: React.FC<Props> = (props) => {
|
||||
iconStyle="with-border"
|
||||
onClick={() => dispatchConditions({ type: 'add', field: reducedFields[0].value })}
|
||||
>
|
||||
Or
|
||||
{t('or')}
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{conditions.length === 0 && (
|
||||
<div className={`${baseClass}__no-filters`}>
|
||||
<div className={`${baseClass}__label`}>No filters set</div>
|
||||
<div className={`${baseClass}__label`}>{t('noFiltersSet')}</div>
|
||||
<Button
|
||||
className={`${baseClass}__add-first-filter`}
|
||||
icon="plus"
|
||||
@@ -163,7 +162,7 @@ const WhereBuilder: React.FC<Props> = (props) => {
|
||||
iconStyle="with-border"
|
||||
onClick={() => dispatchConditions({ type: 'add', field: reducedFields[0].value })}
|
||||
>
|
||||
Add filter
|
||||
{t('addFilter')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -9,7 +9,7 @@ const baseClass = 'field-error';
|
||||
const Error: React.FC<Props> = (props) => {
|
||||
const {
|
||||
showError = false,
|
||||
message = 'Please complete this field.',
|
||||
message,
|
||||
} = props;
|
||||
|
||||
if (showError) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type Props = {
|
||||
showError?: boolean
|
||||
message?: string
|
||||
message: string
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Props, isComponent } from './types';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'field-description';
|
||||
@@ -11,6 +13,7 @@ const FieldDescription: React.FC<Props> = (props) => {
|
||||
value,
|
||||
} = props;
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
if (isComponent(description)) {
|
||||
const Description = description;
|
||||
@@ -25,7 +28,7 @@ const FieldDescription: React.FC<Props> = (props) => {
|
||||
className,
|
||||
].filter(Boolean).join(' ')}
|
||||
>
|
||||
{typeof description === 'function' ? description({ value }) : description}
|
||||
{typeof description === 'function' ? description({ value }) : getTranslation(description, i18n)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export type DescriptionFunction = (value?: unknown) => string
|
||||
|
||||
export type DescriptionComponent = React.ComponentType<{ value: unknown }>
|
||||
|
||||
export type Description = string | DescriptionFunction | DescriptionComponent
|
||||
export type Description = Record<string, string> | string | DescriptionFunction | DescriptionComponent
|
||||
|
||||
export type Props = {
|
||||
description?: Description
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import ObjectID from 'bson-objectid';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { User } from '../../../../../auth';
|
||||
import {
|
||||
NonPresentationalField,
|
||||
@@ -23,6 +24,7 @@ type Args = {
|
||||
operation: 'create' | 'update'
|
||||
data: Data
|
||||
fullData: Data
|
||||
t: TFunction
|
||||
}
|
||||
|
||||
export const addFieldStatePromise = async ({
|
||||
@@ -37,6 +39,7 @@ export const addFieldStatePromise = async ({
|
||||
fieldPromises,
|
||||
id,
|
||||
operation,
|
||||
t,
|
||||
}: Args): Promise<void> => {
|
||||
if (fieldAffectsData(field)) {
|
||||
const fieldState: Field = {
|
||||
@@ -63,6 +66,7 @@ export const addFieldStatePromise = async ({
|
||||
siblingData: data,
|
||||
id,
|
||||
operation,
|
||||
t,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -97,6 +101,7 @@ export const addFieldStatePromise = async ({
|
||||
id,
|
||||
locale,
|
||||
operation,
|
||||
t,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -152,6 +157,7 @@ export const addFieldStatePromise = async ({
|
||||
operation,
|
||||
fieldPromises,
|
||||
id,
|
||||
t,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -183,6 +189,7 @@ export const addFieldStatePromise = async ({
|
||||
path: `${path}${field.name}.`,
|
||||
locale,
|
||||
user,
|
||||
t,
|
||||
});
|
||||
|
||||
break;
|
||||
@@ -212,6 +219,7 @@ export const addFieldStatePromise = async ({
|
||||
id,
|
||||
locale,
|
||||
operation,
|
||||
t,
|
||||
});
|
||||
} else if (field.type === 'tabs') {
|
||||
field.tabs.forEach((tab) => {
|
||||
@@ -227,6 +235,7 @@ export const addFieldStatePromise = async ({
|
||||
id,
|
||||
locale,
|
||||
operation,
|
||||
t,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { TFunction } from 'i18next';
|
||||
import { User } from '../../../../../auth';
|
||||
import { Field as FieldSchema } from '../../../../../fields/config/types';
|
||||
import { Fields, Data } from '../types';
|
||||
@@ -11,6 +12,7 @@ type Args = {
|
||||
id?: string | number,
|
||||
operation?: 'create' | 'update'
|
||||
locale: string
|
||||
t: TFunction
|
||||
}
|
||||
|
||||
const buildStateFromSchema = async (args: Args): Promise<Fields> => {
|
||||
@@ -21,6 +23,7 @@ const buildStateFromSchema = async (args: Args): Promise<Fields> => {
|
||||
id,
|
||||
operation,
|
||||
locale,
|
||||
t,
|
||||
} = args;
|
||||
|
||||
if (fieldSchema) {
|
||||
@@ -39,6 +42,7 @@ const buildStateFromSchema = async (args: Args): Promise<Fields> => {
|
||||
data: fullData,
|
||||
fullData,
|
||||
parentPassesCondition: true,
|
||||
t,
|
||||
});
|
||||
|
||||
await Promise.all(fieldPromises);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { TFunction } from 'i18next';
|
||||
import { User } from '../../../../../auth';
|
||||
import {
|
||||
Field as FieldSchema,
|
||||
@@ -18,6 +19,7 @@ type Args = {
|
||||
fieldPromises: Promise<void>[]
|
||||
id: string | number
|
||||
operation: 'create' | 'update'
|
||||
t: TFunction
|
||||
}
|
||||
|
||||
export const iterateFields = ({
|
||||
@@ -32,6 +34,7 @@ export const iterateFields = ({
|
||||
fieldPromises,
|
||||
id,
|
||||
state,
|
||||
t,
|
||||
}: Args): void => {
|
||||
fields.forEach((field) => {
|
||||
const initialData = data;
|
||||
@@ -51,6 +54,7 @@ export const iterateFields = ({
|
||||
field,
|
||||
passesCondition,
|
||||
data,
|
||||
t,
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import isDeepEqual from 'deep-equal';
|
||||
import { serialize } from 'object-to-formdata';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../utilities/Auth';
|
||||
import { useLocale } from '../../utilities/Locale';
|
||||
import { useDocumentInfo } from '../../utilities/DocumentInfo';
|
||||
@@ -46,6 +47,7 @@ const Form: React.FC<Props> = (props) => {
|
||||
|
||||
const history = useHistory();
|
||||
const locale = useLocale();
|
||||
const { t, i18n } = useTranslation('general');
|
||||
const { refreshCookie, user } = useAuth();
|
||||
const { id } = useDocumentInfo();
|
||||
const operation = useOperation();
|
||||
@@ -90,6 +92,7 @@ const Form: React.FC<Props> = (props) => {
|
||||
user,
|
||||
id,
|
||||
operation,
|
||||
t,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -110,7 +113,7 @@ const Form: React.FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}, [contextRef, id, user, operation, dispatchFields]);
|
||||
}, [contextRef, id, user, operation, t, dispatchFields]);
|
||||
|
||||
const submit = useCallback(async (options: SubmitOptions = {}, e): Promise<void> => {
|
||||
const {
|
||||
@@ -142,7 +145,7 @@ const Form: React.FC<Props> = (props) => {
|
||||
|
||||
// If not valid, prevent submission
|
||||
if (!isValid) {
|
||||
toast.error('Please correct invalid fields.');
|
||||
toast.error(t('error:correctInvalidFields'));
|
||||
setProcessing(false);
|
||||
|
||||
return;
|
||||
@@ -164,6 +167,9 @@ const Form: React.FC<Props> = (props) => {
|
||||
try {
|
||||
const res = await requests[methodToUse.toLowerCase()](actionToUse, {
|
||||
body: formData,
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
});
|
||||
|
||||
setModified(false);
|
||||
@@ -206,7 +212,7 @@ const Form: React.FC<Props> = (props) => {
|
||||
|
||||
history.push(destination);
|
||||
} else if (!disableSuccessStatus) {
|
||||
toast.success(json.message || 'Submission successful.', { autoClose: 3000 });
|
||||
toast.success(json.message || t('submissionSuccessful'), { autoClose: 3000 });
|
||||
}
|
||||
} else {
|
||||
contextRef.current = { ...contextRef.current }; // triggers rerender of all components that subscribe to form
|
||||
@@ -262,13 +268,13 @@ const Form: React.FC<Props> = (props) => {
|
||||
});
|
||||
|
||||
nonFieldErrors.forEach((err) => {
|
||||
toast.error(err.message || 'An unknown error occurred.');
|
||||
toast.error(err.message || t('error:unknown'));
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const message = errorMessages[res.status] || 'An unknown error occurred.';
|
||||
const message = errorMessages[res.status] || t('error:unknown');
|
||||
|
||||
toast.error(message);
|
||||
}
|
||||
@@ -291,6 +297,8 @@ const Form: React.FC<Props> = (props) => {
|
||||
onSubmit,
|
||||
onSuccess,
|
||||
redirect,
|
||||
t,
|
||||
i18n,
|
||||
waitForAutocomplete,
|
||||
]);
|
||||
|
||||
@@ -325,10 +333,10 @@ const Form: React.FC<Props> = (props) => {
|
||||
}, [contextRef]);
|
||||
|
||||
const reset = useCallback(async (fieldSchema: Field[], data: unknown) => {
|
||||
const state = await buildStateFromSchema({ fieldSchema, data, user, id, operation, locale });
|
||||
const state = await buildStateFromSchema({ fieldSchema, data, user, id, operation, locale, t });
|
||||
contextRef.current = { ...initContextState } as FormContextType;
|
||||
dispatchFields({ type: 'REPLACE_STATE', state });
|
||||
}, [id, user, operation, locale, dispatchFields]);
|
||||
}, [id, user, operation, locale, t, dispatchFields]);
|
||||
|
||||
contextRef.current.submit = submit;
|
||||
contextRef.current.getFields = getFields;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Props } from './types';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const Label: React.FC<Props> = (props) => {
|
||||
const {
|
||||
label, required = false, htmlFor,
|
||||
} = props;
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
if (label) {
|
||||
return (
|
||||
@@ -13,7 +17,7 @@ const Label: React.FC<Props> = (props) => {
|
||||
htmlFor={htmlFor}
|
||||
className="field-label"
|
||||
>
|
||||
{label}
|
||||
{ getTranslation(label, i18n) }
|
||||
{required && <span className="required">*</span>}
|
||||
</label>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export type Props = {
|
||||
label?: string | false | JSX.Element
|
||||
label?: Record<string, string> | string | false | JSX.Element
|
||||
required?: boolean
|
||||
htmlFor?: string
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
|
||||
import useIntersect from '../../../hooks/useIntersect';
|
||||
import { Props } from './types';
|
||||
import { fieldAffectsData, fieldIsPresentationalOnly } from '../../../../fields/config/types';
|
||||
import { useOperation } from '../../utilities/OperationProvider';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
const baseClass = 'render-fields';
|
||||
|
||||
@@ -23,6 +25,7 @@ const RenderFields: React.FC<Props> = (props) => {
|
||||
indexPath: incomingIndexPath,
|
||||
} = props;
|
||||
|
||||
const { t, i18n } = useTranslation('general');
|
||||
const [hasRendered, setHasRendered] = useState(Boolean(forceRender));
|
||||
const [intersectionRef, entry] = useIntersect(intersectionObserverOptions);
|
||||
const operation = useOperation();
|
||||
@@ -107,7 +110,7 @@ const RenderFields: React.FC<Props> = (props) => {
|
||||
className="missing-field"
|
||||
key={fieldIndex}
|
||||
>
|
||||
{`No matched field found for "${field.label}"`}
|
||||
{t('error:noMatchedField', { label: fieldAffectsData(field) ? getTranslation(field.label || field.name, i18n) : field.path })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { isComponent, Props } from './types';
|
||||
import { useWatchForm } from '../Form/context';
|
||||
import { getTranslation } from '../../../../utilities/getTranslation';
|
||||
|
||||
const baseClass = 'row-label';
|
||||
|
||||
@@ -27,6 +29,7 @@ const RowLabelContent: React.FC<Omit<Props, 'className'>> = (props) => {
|
||||
rowNumber,
|
||||
} = props;
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
const { getDataByPath, getSiblingData } = useWatchForm();
|
||||
const collapsibleData = getSiblingData(path);
|
||||
const arrayData = getDataByPath(path);
|
||||
@@ -49,7 +52,7 @@ const RowLabelContent: React.FC<Omit<Props, 'className'>> = (props) => {
|
||||
data,
|
||||
path,
|
||||
index: rowNumber,
|
||||
}) : label}
|
||||
}) : getTranslation(label, i18n)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ export type RowLabelFunction = (args: RowLabelArgs) => string
|
||||
|
||||
export type RowLabelComponent = React.ComponentType<RowLabelArgs>
|
||||
|
||||
export type RowLabel = string | RowLabelFunction | RowLabelComponent
|
||||
export type RowLabel = string | Record<string, string> | RowLabelFunction | RowLabelComponent
|
||||
|
||||
export function isComponent(label: RowLabel): label is RowLabelComponent {
|
||||
return React.isValidElement(label);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useCallback, useEffect, useReducer } from 'react';
|
||||
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../../utilities/Auth';
|
||||
import withCondition from '../../withCondition';
|
||||
import Button from '../../../elements/Button';
|
||||
@@ -25,6 +26,7 @@ import HiddenInput from '../HiddenInput';
|
||||
import { RowLabel } from '../../RowLabel';
|
||||
|
||||
import './index.scss';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
|
||||
const baseClass = 'array-field';
|
||||
|
||||
@@ -52,14 +54,6 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
|
||||
const path = pathFromProps || name;
|
||||
|
||||
// Handle labeling for Arrays, Global Arrays, and Blocks
|
||||
const getLabels = (p: Props) => {
|
||||
if (p?.labels) return p.labels;
|
||||
if (p?.label) return { singular: p.label, plural: undefined };
|
||||
return { singular: 'Row', plural: 'Rows' };
|
||||
};
|
||||
|
||||
const labels = getLabels(props);
|
||||
// eslint-disable-next-line react/destructuring-assignment
|
||||
const label = props?.label ?? props?.labels?.singular;
|
||||
|
||||
@@ -74,6 +68,16 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
const { id } = useDocumentInfo();
|
||||
const locale = useLocale();
|
||||
const operation = useOperation();
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
|
||||
// Handle labeling for Arrays, Global Arrays, and Blocks
|
||||
const getLabels = (p: Props) => {
|
||||
if (p?.labels) return p.labels;
|
||||
if (p?.label) return { singular: p.label, plural: undefined };
|
||||
return { singular: t('row'), plural: t('rows') };
|
||||
};
|
||||
|
||||
const labels = getLabels(props);
|
||||
|
||||
const { dispatchFields, setModified } = formContext;
|
||||
|
||||
@@ -93,7 +97,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
});
|
||||
|
||||
const addRow = useCallback(async (rowIndex: number) => {
|
||||
const subFieldState = await buildStateFromSchema({ fieldSchema: fields, operation, id, user, locale });
|
||||
const subFieldState = await buildStateFromSchema({ fieldSchema: fields, operation, id, user, locale, t });
|
||||
dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path });
|
||||
dispatchRows({ type: 'ADD', rowIndex });
|
||||
setModified(true);
|
||||
@@ -101,7 +105,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
setTimeout(() => {
|
||||
scrollToID(`${path}-row-${rowIndex + 1}`);
|
||||
}, 0);
|
||||
}, [dispatchRows, dispatchFields, fields, path, operation, id, user, locale, setModified]);
|
||||
}, [dispatchRows, dispatchFields, fields, path, operation, id, user, locale, setModified, t]);
|
||||
|
||||
const duplicateRow = useCallback(async (rowIndex: number) => {
|
||||
dispatchFields({ type: 'DUPLICATE_ROW', rowIndex, path });
|
||||
@@ -220,7 +224,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<h3>{label}</h3>
|
||||
<h3>{getTranslation(label || name, i18n)}</h3>
|
||||
<ul className={`${baseClass}__header-actions`}>
|
||||
<li>
|
||||
<button
|
||||
@@ -228,7 +232,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
onClick={() => toggleCollapseAll(true)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
Collapse All
|
||||
{t('collapseAll')}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
@@ -237,12 +241,13 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
onClick={() => toggleCollapseAll(false)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
Show All
|
||||
{t('showAll')}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<FieldDescription
|
||||
className={`field-description-${path.replace(/\./gi, '__')}`}
|
||||
value={value}
|
||||
description={description}
|
||||
/>
|
||||
@@ -319,19 +324,18 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
})}
|
||||
{(rows.length < minRows || (required && rows.length === 0)) && (
|
||||
<Banner type="error">
|
||||
This field requires at least
|
||||
{' '}
|
||||
{minRows
|
||||
? `${minRows} ${labels.plural}`
|
||||
: `1 ${labels.singular}`}
|
||||
{t('validation:requiresAtLeast', {
|
||||
count: minRows,
|
||||
label: getTranslation(minRows
|
||||
? labels.plural
|
||||
: labels.singular,
|
||||
i18n) || t(minRows > 1 ? 'general:row' : 'general:rows'),
|
||||
})}
|
||||
</Banner>
|
||||
)}
|
||||
{(rows.length === 0 && readOnly) && (
|
||||
<Banner>
|
||||
This field has no
|
||||
{' '}
|
||||
{labels.plural}
|
||||
.
|
||||
{t('validation:fieldHasNo', { label: getTranslation(labels.plural, i18n) })}
|
||||
</Banner>
|
||||
)}
|
||||
{provided.placeholder}
|
||||
@@ -347,7 +351,7 @@ const ArrayFieldType: React.FC<Props> = (props) => {
|
||||
iconStyle="with-border"
|
||||
iconPosition="left"
|
||||
>
|
||||
{`Add ${labels.singular}`}
|
||||
{t('addLabel', { label: getTranslation(labels.singular, i18n) })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SearchIcon from '../../../../../graphics/Search';
|
||||
|
||||
import './index.scss';
|
||||
@@ -7,6 +8,7 @@ const baseClass = 'block-search';
|
||||
|
||||
const BlockSearch: React.FC<{ setSearchTerm: (term: string) => void }> = (props) => {
|
||||
const { setSearchTerm } = props;
|
||||
const { t } = useTranslation('fields');
|
||||
|
||||
const handleChange = (e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
@@ -16,7 +18,7 @@ const BlockSearch: React.FC<{ setSearchTerm: (term: string) => void }> = (props)
|
||||
<div className={baseClass}>
|
||||
<input
|
||||
className={`${baseClass}__input`}
|
||||
placeholder="Search for a block"
|
||||
placeholder={t('searchForBlock')}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<SearchIcon />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getTranslation } from '../../../../../../../utilities/getTranslation';
|
||||
import DefaultBlockImage from '../../../../../graphics/DefaultBlockImage';
|
||||
import { Props } from './types';
|
||||
|
||||
@@ -12,6 +13,8 @@ const BlockSelection: React.FC<Props> = (props) => {
|
||||
addRow, addRowIndex, block, close,
|
||||
} = props;
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const {
|
||||
labels, slug, imageURL, imageAltText,
|
||||
} = block;
|
||||
@@ -38,7 +41,7 @@ const BlockSelection: React.FC<Props> = (props) => {
|
||||
)
|
||||
: <DefaultBlockImage />}
|
||||
</div>
|
||||
<div className={`${baseClass}__label`}>{labels.singular}</div>
|
||||
<div className={`${baseClass}__label`}>{getTranslation(labels.singular, i18n)}</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useField from '../../../useField';
|
||||
import { Props } from './types';
|
||||
|
||||
@@ -10,6 +11,7 @@ const SectionTitle: React.FC<Props> = (props) => {
|
||||
const { path, readOnly } = props;
|
||||
|
||||
const { value, setValue } = useField({ path });
|
||||
const { t } = useTranslation('general');
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
@@ -24,7 +26,7 @@ const SectionTitle: React.FC<Props> = (props) => {
|
||||
className={`${baseClass}__input`}
|
||||
id={path}
|
||||
value={value as string || ''}
|
||||
placeholder="Untitled"
|
||||
placeholder={t('untitled')}
|
||||
type="text"
|
||||
name={path}
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useEffect, useReducer, useState } from 'react';
|
||||
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../../utilities/Auth';
|
||||
import { usePreferences } from '../../../utilities/Preferences';
|
||||
import { useLocale } from '../../../utilities/Locale';
|
||||
@@ -27,23 +28,23 @@ import SectionTitle from './SectionTitle';
|
||||
import Pill from '../../../elements/Pill';
|
||||
import { scrollToID } from '../../../../utilities/scrollToID';
|
||||
import HiddenInput from '../HiddenInput';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'blocks-field';
|
||||
|
||||
const labelDefaults = {
|
||||
singular: 'Block',
|
||||
plural: 'Blocks',
|
||||
};
|
||||
|
||||
const BlocksField: React.FC<Props> = (props) => {
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
const {
|
||||
label,
|
||||
name,
|
||||
path: pathFromProps,
|
||||
blocks,
|
||||
labels = labelDefaults,
|
||||
labels = {
|
||||
singular: t('block'),
|
||||
plural: t('blocks'),
|
||||
},
|
||||
fieldTypes,
|
||||
maxRows,
|
||||
minRows,
|
||||
@@ -98,7 +99,7 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
|
||||
const addRow = useCallback(async (rowIndex: number, blockType: string) => {
|
||||
const block = blocks.find((potentialBlock) => potentialBlock.slug === blockType);
|
||||
const subFieldState = await buildStateFromSchema({ fieldSchema: block.fields, operation, id, user, locale });
|
||||
const subFieldState = await buildStateFromSchema({ fieldSchema: block.fields, operation, id, user, locale, t });
|
||||
dispatchFields({ type: 'ADD_ROW', rowIndex, subFieldState, path, blockType });
|
||||
dispatchRows({ type: 'ADD', rowIndex, blockType });
|
||||
setModified(true);
|
||||
@@ -106,7 +107,7 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
setTimeout(() => {
|
||||
scrollToID(`${path}-row-${rowIndex + 1}`);
|
||||
}, 0);
|
||||
}, [path, blocks, dispatchFields, operation, id, user, locale, setModified]);
|
||||
}, [blocks, operation, id, user, locale, t, dispatchFields, path, setModified]);
|
||||
|
||||
const duplicateRow = useCallback(async (rowIndex: number, blockType: string) => {
|
||||
dispatchFields({ type: 'DUPLICATE_ROW', rowIndex, path });
|
||||
@@ -223,7 +224,7 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
</div>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<div className={`${baseClass}__header-wrap`}>
|
||||
<h3>{label}</h3>
|
||||
<h3>{getTranslation(label || name, i18n)}</h3>
|
||||
<ul className={`${baseClass}__header-actions`}>
|
||||
<li>
|
||||
<button
|
||||
@@ -231,7 +232,7 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
onClick={() => toggleCollapseAll(true)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
Collapse All
|
||||
{t('collapseAll')}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
@@ -240,7 +241,7 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
onClick={() => toggleCollapseAll(false)}
|
||||
className={`${baseClass}__header-action`}
|
||||
>
|
||||
Show All
|
||||
{t('showAll')}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -295,7 +296,7 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
pillStyle="white"
|
||||
className={`${baseClass}__block-pill ${baseClass}__block-pill-${blockType}`}
|
||||
>
|
||||
{blockToRender.labels.singular}
|
||||
{getTranslation(blockToRender.labels.singular, i18n)}
|
||||
</Pill>
|
||||
<SectionTitle
|
||||
path={`${path}.${i}.blockName`}
|
||||
@@ -360,17 +361,15 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
})}
|
||||
{(rows.length < minRows || (required && rows.length === 0)) && (
|
||||
<Banner type="error">
|
||||
This field requires at least
|
||||
{' '}
|
||||
{`${minRows || 1} ${minRows === 1 || typeof minRows === 'undefined' ? labels.singular : labels.plural}`}
|
||||
{t('requiresAtLeast', {
|
||||
count: minRows,
|
||||
label: getTranslation(minRows === 1 || typeof minRows === 'undefined' ? labels.singular : labels.plural, i18n),
|
||||
})}
|
||||
</Banner>
|
||||
)}
|
||||
{(rows.length === 0 && readOnly) && (
|
||||
<Banner>
|
||||
This field has no
|
||||
{' '}
|
||||
{labels.plural}
|
||||
.
|
||||
{t('validation:fieldHasNo', { label: getTranslation(labels.plural, i18n) })}
|
||||
</Banner>
|
||||
)}
|
||||
{provided.placeholder}
|
||||
@@ -391,7 +390,7 @@ const BlocksField: React.FC<Props> = (props) => {
|
||||
iconPosition="left"
|
||||
iconStyle="with-border"
|
||||
>
|
||||
{`Add ${labels.singular}`}
|
||||
{t('addLabel', { label: getTranslation(labels.singular, i18n) })}
|
||||
</Button>
|
||||
)}
|
||||
render={({ close }) => (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useField from '../../useField';
|
||||
import withCondition from '../../withCondition';
|
||||
import Error from '../../Error';
|
||||
@@ -6,6 +7,7 @@ import { checkbox } from '../../../../../fields/validations';
|
||||
import Check from '../../../icons/Check';
|
||||
import FieldDescription from '../../FieldDescription';
|
||||
import { Props } from './types';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -30,6 +32,8 @@ const Checkbox: React.FC<Props> = (props) => {
|
||||
} = {},
|
||||
} = props;
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const path = pathFromProps || name;
|
||||
|
||||
const memoizedValidate = useCallback((value, options) => {
|
||||
@@ -87,7 +91,7 @@ const Checkbox: React.FC<Props> = (props) => {
|
||||
<Check />
|
||||
</span>
|
||||
<span className={`${baseClass}__label`}>
|
||||
{label}
|
||||
{getTranslation(label || name, i18n)}
|
||||
</span>
|
||||
</button>
|
||||
<FieldDescription
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useField from '../../useField';
|
||||
import Label from '../../Label';
|
||||
import Error from '../../Error';
|
||||
import { useFormFields } from '../../Form/context';
|
||||
import { Field } from '../../Form/types';
|
||||
|
||||
import './index.scss';
|
||||
import { Field } from '../../Form/types';
|
||||
|
||||
const ConfirmPassword: React.FC = () => {
|
||||
const password = useFormFields<Field>(([fields]) => fields.password);
|
||||
const { t } = useTranslation('fields');
|
||||
|
||||
const validate = useCallback((value: string) => {
|
||||
if (!value) {
|
||||
return 'This field is required';
|
||||
return t('validation:required');
|
||||
}
|
||||
|
||||
if (value === password?.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return 'Passwords do not match.';
|
||||
}, [password]);
|
||||
return t('passwordsDoNotMatch');
|
||||
}, [password, t]);
|
||||
|
||||
const {
|
||||
value,
|
||||
@@ -47,7 +49,7 @@ const ConfirmPassword: React.FC = () => {
|
||||
/>
|
||||
<Label
|
||||
htmlFor="field-confirm-password"
|
||||
label="Confirm Password"
|
||||
label={t('authentication:confirmPassword')}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DatePicker from '../../../elements/DatePicker';
|
||||
import withCondition from '../../withCondition';
|
||||
import useField from '../../useField';
|
||||
@@ -8,6 +9,7 @@ import Error from '../../Error';
|
||||
import FieldDescription from '../../FieldDescription';
|
||||
import { date as dateValidation } from '../../../../../fields/validations';
|
||||
import { Props } from './types';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -32,6 +34,8 @@ const DateTime: React.FC<Props> = (props) => {
|
||||
} = {},
|
||||
} = props;
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const path = pathFromProps || name;
|
||||
|
||||
const memoizedValidate = useCallback((value, options) => {
|
||||
@@ -82,7 +86,7 @@ const DateTime: React.FC<Props> = (props) => {
|
||||
>
|
||||
<DatePicker
|
||||
{...date}
|
||||
placeholder={placeholder}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
readOnly={readOnly}
|
||||
onChange={readOnly ? undefined : setValue}
|
||||
value={value as Date}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import withCondition from '../../withCondition';
|
||||
import useField from '../../useField';
|
||||
import Label from '../../Label';
|
||||
@@ -6,6 +7,7 @@ import Error from '../../Error';
|
||||
import FieldDescription from '../../FieldDescription';
|
||||
import { email } from '../../../../../fields/validations';
|
||||
import { Props } from './types';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -28,6 +30,8 @@ const Email: React.FC<Props> = (props) => {
|
||||
label,
|
||||
} = props;
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const path = pathFromProps || name;
|
||||
|
||||
const memoizedValidate = useCallback((value, options) => {
|
||||
@@ -77,7 +81,7 @@ const Email: React.FC<Props> = (props) => {
|
||||
value={value as string || ''}
|
||||
onChange={setValue}
|
||||
disabled={Boolean(readOnly)}
|
||||
placeholder={placeholder}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
type="email"
|
||||
name={path}
|
||||
autoComplete={autoComplete}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import RenderFields from '../../RenderFields';
|
||||
import withCondition from '../../withCondition';
|
||||
import FieldDescription from '../../FieldDescription';
|
||||
import { Props } from './types';
|
||||
import { fieldAffectsData } from '../../../../../fields/config/types';
|
||||
import { useCollapsible } from '../../../elements/Collapsible/provider';
|
||||
|
||||
import './index.scss';
|
||||
import { GroupProvider, useGroup } from './provider';
|
||||
import { useTabs } from '../Tabs/provider';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const baseClass = 'group-field';
|
||||
|
||||
@@ -34,6 +36,7 @@ const Group: React.FC<Props> = (props) => {
|
||||
const isWithinCollapsible = useCollapsible();
|
||||
const isWithinGroup = useGroup();
|
||||
const isWithinTab = useTabs();
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const path = pathFromProps || name;
|
||||
|
||||
@@ -59,9 +62,10 @@ const Group: React.FC<Props> = (props) => {
|
||||
{(label || description) && (
|
||||
<header className={`${baseClass}__header`}>
|
||||
{label && (
|
||||
<h3 className={`${baseClass}__title`}>{label}</h3>
|
||||
<h3 className={`${baseClass}__title`}>{getTranslation(label, i18n)}</h3>
|
||||
)}
|
||||
<FieldDescription
|
||||
className={`field-description-${path.replace(/\./gi, '__')}`}
|
||||
value={null}
|
||||
description={description}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useField from '../../useField';
|
||||
import Label from '../../Label';
|
||||
import Error from '../../Error';
|
||||
@@ -6,6 +7,7 @@ import FieldDescription from '../../FieldDescription';
|
||||
import withCondition from '../../withCondition';
|
||||
import { number } from '../../../../../fields/validations';
|
||||
import { Props } from './types';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -30,6 +32,8 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
} = {},
|
||||
} = props;
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const path = pathFromProps || name;
|
||||
|
||||
const memoizedValidate = useCallback((value, options) => {
|
||||
@@ -87,7 +91,7 @@ const NumberField: React.FC<Props> = (props) => {
|
||||
value={typeof value === 'number' ? value : ''}
|
||||
onChange={handleChange}
|
||||
disabled={readOnly}
|
||||
placeholder={placeholder}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
type="number"
|
||||
name={path}
|
||||
step={step}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useField from '../../useField';
|
||||
import Label from '../../Label';
|
||||
import Error from '../../Error';
|
||||
@@ -6,6 +7,7 @@ import FieldDescription from '../../FieldDescription';
|
||||
import withCondition from '../../withCondition';
|
||||
import { point } from '../../../../../fields/validations';
|
||||
import { Props } from './types';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -32,6 +34,8 @@ const PointField: React.FC<Props> = (props) => {
|
||||
|
||||
const path = pathFromProps || name;
|
||||
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
|
||||
const memoizedValidate = useCallback((value, options) => {
|
||||
return validate(value, { ...options, required });
|
||||
}, [validate, required]);
|
||||
@@ -81,7 +85,7 @@ const PointField: React.FC<Props> = (props) => {
|
||||
<li>
|
||||
<Label
|
||||
htmlFor={`field-longitude-${path.replace(/\./gi, '__')}`}
|
||||
label={`${label} - Longitude`}
|
||||
label={`${getTranslation(label || name, i18n)} - ${t('longitude')}`}
|
||||
required={required}
|
||||
/>
|
||||
<input
|
||||
@@ -89,7 +93,7 @@ const PointField: React.FC<Props> = (props) => {
|
||||
value={(value && typeof value[0] === 'number') ? value[0] : ''}
|
||||
onChange={(e) => handleChange(e, 0)}
|
||||
disabled={readOnly}
|
||||
placeholder={placeholder}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
type="number"
|
||||
name={`${path}.longitude`}
|
||||
step={step}
|
||||
@@ -98,7 +102,7 @@ const PointField: React.FC<Props> = (props) => {
|
||||
<li>
|
||||
<Label
|
||||
htmlFor={`field-latitude-${path.replace(/\./gi, '__')}`}
|
||||
label={`${label} - Latitude`}
|
||||
label={`${getTranslation(label || name, i18n)} - ${t('latitude')}`}
|
||||
required={required}
|
||||
/>
|
||||
<input
|
||||
@@ -106,7 +110,7 @@ const PointField: React.FC<Props> = (props) => {
|
||||
value={(value && typeof value[1] === 'number') ? value[1] : ''}
|
||||
onChange={(e) => handleChange(e, 1)}
|
||||
disabled={readOnly}
|
||||
placeholder={placeholder}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
type="number"
|
||||
name={`${path}.latitude`}
|
||||
step={step}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Props } from './types';
|
||||
import { getTranslation } from '../../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -7,6 +9,7 @@ const baseClass = 'radio-input';
|
||||
|
||||
const RadioInput: React.FC<Props> = (props) => {
|
||||
const { isSelected, option, onChange, path } = props;
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
@@ -27,7 +30,7 @@ const RadioInput: React.FC<Props> = (props) => {
|
||||
onChange={() => (typeof onChange === 'function' ? onChange(option.value) : null)}
|
||||
/>
|
||||
<span className={`${baseClass}__styled-radio`} />
|
||||
<span className={`${baseClass}__label`}>{option.label}</span>
|
||||
<span className={`${baseClass}__label`}>{getTranslation(option.label, i18n)}</span>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { OnChange } from '../types';
|
||||
export type Props = {
|
||||
isSelected: boolean
|
||||
option: {
|
||||
label: string
|
||||
label: Record<string, string> | string
|
||||
value: string
|
||||
}
|
||||
onChange: OnChange
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { useWindowInfo } from '@faceless-ui/window-info';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '../../../../../elements/Button';
|
||||
import { Props } from './types';
|
||||
import { useAuth } from '../../../../../utilities/Auth';
|
||||
@@ -12,6 +13,7 @@ import X from '../../../../../icons/X';
|
||||
import { Fields } from '../../../../Form/types';
|
||||
import buildStateFromSchema from '../../../../Form/buildStateFromSchema';
|
||||
import { EditDepthContext, useEditDepth } from '../../../../../utilities/EditDepth';
|
||||
import { getTranslation } from '../../../../../../../utilities/getTranslation';
|
||||
import { DocumentInfoProvider } from '../../../../../utilities/DocumentInfo';
|
||||
|
||||
import './index.scss';
|
||||
@@ -27,17 +29,18 @@ export const AddNewRelationModal: React.FC<Props> = ({ modalCollection, onSave,
|
||||
const [initialState, setInitialState] = useState<Fields>();
|
||||
const [isAnimated, setIsAnimated] = useState(false);
|
||||
const editDepth = useEditDepth();
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
|
||||
const modalAction = `${serverURL}${api}/${modalCollection.slug}?locale=${locale}&depth=0&fallback-locale=null`;
|
||||
|
||||
useEffect(() => {
|
||||
const buildState = async () => {
|
||||
const state = await buildStateFromSchema({ fieldSchema: modalCollection.fields, data: {}, user, operation: 'create', locale });
|
||||
const state = await buildStateFromSchema({ fieldSchema: modalCollection.fields, data: {}, user, operation: 'create', locale, t });
|
||||
setInitialState(state);
|
||||
};
|
||||
|
||||
buildState();
|
||||
}, [modalCollection, locale, user]);
|
||||
}, [modalCollection, locale, user, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsAnimated(true);
|
||||
@@ -87,9 +90,7 @@ export const AddNewRelationModal: React.FC<Props> = ({ modalCollection, onSave,
|
||||
customHeader: (
|
||||
<div className={`${baseClass}__header`}>
|
||||
<h2>
|
||||
Add new
|
||||
{' '}
|
||||
{modalCollection.labels.singular}
|
||||
{t('addNewLabel', { label: getTranslation(modalCollection.labels.singular, i18n) })}
|
||||
</h2>
|
||||
<Button
|
||||
buttonStyle="none"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '../../../../elements/Button';
|
||||
import { Props } from './types';
|
||||
import { SanitizedCollectionConfig } from '../../../../../../collections/config/types';
|
||||
@@ -9,6 +10,7 @@ import { useAuth } from '../../../../utilities/Auth';
|
||||
import { AddNewRelationModal } from './Modal';
|
||||
import { useEditDepth } from '../../../../utilities/EditDepth';
|
||||
import Plus from '../../../../icons/Plus';
|
||||
import { getTranslation } from '../../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -22,6 +24,7 @@ export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, val
|
||||
const [modalCollection, setModalCollection] = useState<SanitizedCollectionConfig>();
|
||||
const [popupOpen, setPopupOpen] = useState(false);
|
||||
const editDepth = useEditDepth();
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
|
||||
const modalSlug = `${path}-add-modal-depth-${editDepth}`;
|
||||
|
||||
@@ -44,6 +47,7 @@ export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, val
|
||||
json.doc,
|
||||
],
|
||||
sort: true,
|
||||
i18n,
|
||||
});
|
||||
|
||||
if (hasMany) {
|
||||
@@ -54,7 +58,7 @@ export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, val
|
||||
|
||||
setModalCollection(undefined);
|
||||
toggleModal(modalSlug);
|
||||
}, [relationTo, modalCollection, hasMany, toggleModal, modalSlug, setValue, value, dispatchOptions]);
|
||||
}, [relationTo, modalCollection, dispatchOptions, i18n, hasMany, toggleModal, modalSlug, setValue, value]);
|
||||
|
||||
const onPopopToggle = useCallback((state) => {
|
||||
setPopupOpen(state);
|
||||
@@ -86,7 +90,7 @@ export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, val
|
||||
className={`${baseClass}__add-button`}
|
||||
onClick={() => openModal(relatedCollections[0])}
|
||||
buttonStyle="none"
|
||||
tooltip={`Add new ${relatedCollections[0].labels.singular}`}
|
||||
tooltip={t('addNewLabel', { label: relatedCollections[0].labels.singular })}
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
@@ -100,7 +104,7 @@ export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, val
|
||||
<Button
|
||||
className={`${baseClass}__add-button`}
|
||||
buttonStyle="none"
|
||||
tooltip={popupOpen ? undefined : 'Add new'}
|
||||
tooltip={popupOpen ? undefined : t('addNew')}
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
@@ -116,7 +120,7 @@ export const AddNewRelation: React.FC<Props> = ({ path, hasMany, relationTo, val
|
||||
type="button"
|
||||
onClick={() => { closePopup(); openModal(relatedCollection); }}
|
||||
>
|
||||
{relatedCollection.labels.singular}
|
||||
{getTranslation(relatedCollection.labels.singular, i18n)}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, {
|
||||
} from 'react';
|
||||
import equal from 'deep-equal';
|
||||
import qs from 'qs';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../../utilities/Config';
|
||||
import { useAuth } from '../../../utilities/Auth';
|
||||
import withCondition from '../../withCondition';
|
||||
@@ -62,12 +63,13 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
collections,
|
||||
} = useConfig();
|
||||
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
const { id } = useDocumentInfo();
|
||||
const { user, permissions } = useAuth();
|
||||
const [fields] = useAllFormFields();
|
||||
const formProcessing = useFormProcessing();
|
||||
const hasMultipleRelations = Array.isArray(relationTo);
|
||||
const [options, dispatchOptions] = useReducer(optionsReducer, required || hasMany ? [] : [{ value: null, label: 'None' }]);
|
||||
const [options, dispatchOptions] = useReducer(optionsReducer, required || hasMany ? [] : [{ value: null, label: t('general:none') }]);
|
||||
const [lastFullyLoadedRelation, setLastFullyLoadedRelation] = useState(-1);
|
||||
const [lastLoadedPage, setLastLoadedPage] = useState(1);
|
||||
const [errorLoading, setErrorLoading] = useState('');
|
||||
@@ -159,13 +161,18 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
query.where.and.push(optionFilters[relation]);
|
||||
}
|
||||
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, { credentials: 'include' });
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data: PaginatedDocs<unknown> = await response.json();
|
||||
if (data.docs.length > 0) {
|
||||
resultsFetched += data.docs.length;
|
||||
dispatchOptions({ type: 'ADD', docs: data.docs, hasMultipleRelations, collection, sort });
|
||||
dispatchOptions({ type: 'ADD', docs: data.docs, hasMultipleRelations, collection, sort, i18n });
|
||||
setLastLoadedPage(data.page);
|
||||
|
||||
if (!data.nextPage) {
|
||||
@@ -181,9 +188,9 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
} else if (response.status === 403) {
|
||||
setLastFullyLoadedRelation(relations.indexOf(relation));
|
||||
lastLoadedPageToUse = 1;
|
||||
dispatchOptions({ type: 'ADD', docs: [], hasMultipleRelations, collection, sort, ids: relationMap[relation] });
|
||||
dispatchOptions({ type: 'ADD', docs: [], hasMultipleRelations, collection, sort, ids: relationMap[relation], i18n });
|
||||
} else {
|
||||
setErrorLoading('An error has occurred.');
|
||||
setErrorLoading(t('error:unspecific'));
|
||||
}
|
||||
}
|
||||
}, Promise.resolve());
|
||||
@@ -198,6 +205,8 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
serverURL,
|
||||
api,
|
||||
hasMultipleRelations,
|
||||
t,
|
||||
i18n,
|
||||
]);
|
||||
|
||||
const findOptionsByValue = useCallback((): Option | Option[] => {
|
||||
@@ -295,13 +304,18 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
};
|
||||
|
||||
if (!errorLoading) {
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, { credentials: 'include' });
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
});
|
||||
const collection = collections.find((coll) => coll.slug === relation);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
dispatchOptions({ type: 'ADD', docs: data.docs, hasMultipleRelations, collection, sort: true, ids });
|
||||
dispatchOptions({ type: 'ADD', docs: data.docs, hasMultipleRelations, collection, sort: true, ids, i18n });
|
||||
} else if (response.status === 403) {
|
||||
dispatchOptions({ type: 'ADD', docs: [], hasMultipleRelations, collection, sort: true, ids });
|
||||
dispatchOptions({ type: 'ADD', docs: [], hasMultipleRelations, collection, sort: true, ids, i18n });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,7 +323,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
|
||||
setHasLoadedValueOptions(true);
|
||||
}
|
||||
}, [hasMany, hasMultipleRelations, relationTo, initialValue, hasLoadedValueOptions, errorLoading, collections, api, serverURL]);
|
||||
}, [hasMany, hasMultipleRelations, relationTo, initialValue, hasLoadedValueOptions, errorLoading, collections, api, serverURL, i18n]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!filterOptions) return;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Option, Action } from './types';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
|
||||
const reduceToIDs = (options) => options.reduce((ids, option) => {
|
||||
if (option.options) {
|
||||
@@ -29,7 +30,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
|
||||
}
|
||||
|
||||
case 'ADD': {
|
||||
const { hasMultipleRelations, collection, docs, sort, ids = [] } = action;
|
||||
const { hasMultipleRelations, collection, docs, sort, ids = [], i18n } = action;
|
||||
const relation = collection.slug;
|
||||
|
||||
const labelKey = collection.admin.useAsTitle || 'id';
|
||||
@@ -45,7 +46,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
|
||||
return [
|
||||
...docOptions,
|
||||
{
|
||||
label: doc[labelKey] || `Untitled - ID: ${doc.id}`,
|
||||
label: doc[labelKey] || `${i18n.t('general:untitled')} - ID: ${doc.id}`,
|
||||
value: doc.id,
|
||||
},
|
||||
];
|
||||
@@ -57,7 +58,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
|
||||
ids.forEach((id) => {
|
||||
if (!loadedIDs.includes(id)) {
|
||||
options.push({
|
||||
label: labelKey === 'id' ? id : `Untitled - ID: ${id}`,
|
||||
label: labelKey === 'id' ? id : `${i18n.t('general:untitled')} - ID: ${id}`,
|
||||
value: id,
|
||||
});
|
||||
}
|
||||
@@ -76,7 +77,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
|
||||
return [
|
||||
...docSubOptions,
|
||||
{
|
||||
label: doc[labelKey] || `Untitled - ID: ${doc.id}`,
|
||||
label: doc[labelKey] || `${i18n.t('general:untitled')} - ID: ${doc.id}`,
|
||||
relationTo: relation,
|
||||
value: doc.id,
|
||||
},
|
||||
@@ -89,7 +90,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
|
||||
ids.forEach((id) => {
|
||||
if (!loadedIDs.includes(id)) {
|
||||
newSubOptions.push({
|
||||
label: labelKey === 'id' ? id : `Untitled - ID: ${id}`,
|
||||
label: labelKey === 'id' ? id : `${i18n.t('general:untitled')} - ID: ${id}`,
|
||||
value: id,
|
||||
});
|
||||
}
|
||||
@@ -104,7 +105,7 @@ const optionsReducer = (state: Option[], action: Action): Option[] => {
|
||||
optionsToAddTo.options = sort ? sortOptions(subOptions) : subOptions;
|
||||
} else {
|
||||
newOptions.push({
|
||||
label: collection.labels.plural,
|
||||
label: getTranslation(collection.labels.plural, i18n),
|
||||
options: sort ? sortOptions(newSubOptions) : newSubOptions,
|
||||
value: undefined,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import i18n from 'i18next';
|
||||
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
|
||||
import { RelationshipField } from '../../../../../fields/config/types';
|
||||
|
||||
@@ -23,6 +24,7 @@ type ADD = {
|
||||
collection: SanitizedCollectionConfig
|
||||
sort?: boolean
|
||||
ids?: unknown[]
|
||||
i18n: typeof i18n
|
||||
}
|
||||
|
||||
export type Action = CLEAR | ADD
|
||||
|
||||
@@ -3,6 +3,7 @@ import isHotkey from 'is-hotkey';
|
||||
import { createEditor, Transforms, Node, Element as SlateElement, Text, BaseEditor } from 'slate';
|
||||
import { ReactEditor, Editable, withReact, Slate } from 'slate-react';
|
||||
import { HistoryEditor, withHistory } from 'slate-history';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { richText } from '../../../../../fields/validations';
|
||||
import useField from '../../useField';
|
||||
import withCondition from '../../withCondition';
|
||||
@@ -21,6 +22,7 @@ import { RichTextElement, RichTextLeaf } from '../../../../../fields/config/type
|
||||
import listTypes from './elements/listTypes';
|
||||
import mergeCustomFunctions from './mergeCustomFunctions';
|
||||
import withEnterBreakOut from './plugins/withEnterBreakOut';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -65,6 +67,7 @@ const RichText: React.FC<Props> = (props) => {
|
||||
|
||||
const path = pathFromProps || name;
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [enabledElements, setEnabledElements] = useState({});
|
||||
const [enabledLeaves, setEnabledLeaves] = useState({});
|
||||
@@ -308,7 +311,7 @@ const RichText: React.FC<Props> = (props) => {
|
||||
className={`${baseClass}__input`}
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
placeholder={placeholder}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
spellCheck
|
||||
readOnly={readOnly}
|
||||
onKeyDown={(event) => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { Fragment, useState } from 'react';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import { Transforms, Editor, Range } from 'slate';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ElementButton from '../Button';
|
||||
import { unwrapLink } from './utilities';
|
||||
import LinkIcon from '../../../../../icons/Link';
|
||||
@@ -22,6 +23,7 @@ export const LinkButton = ({ fieldProps }) => {
|
||||
|
||||
const modalSlug = `${baseModalSlug}-${fieldProps.path}`;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const config = useConfig();
|
||||
const editor = useSlate();
|
||||
const { user } = useAuth();
|
||||
@@ -71,7 +73,7 @@ export const LinkButton = ({ fieldProps }) => {
|
||||
text: editor.selection ? Editor.string(editor, editor.selection) : '',
|
||||
};
|
||||
|
||||
const state = await buildStateFromSchema({ fieldSchema, data, user, operation: 'create', locale });
|
||||
const state = await buildStateFromSchema({ fieldSchema, data, user, operation: 'create', locale, t });
|
||||
setInitialState(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import { Transforms, Node, Editor } from 'slate';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { unwrapLink } from './utilities';
|
||||
import Popup from '../../../../../elements/Popup';
|
||||
import { EditModal } from './Modal';
|
||||
@@ -30,6 +31,7 @@ export const LinkElement = ({ attributes, children, element, editorRef, fieldPro
|
||||
const config = useConfig();
|
||||
const { user } = useAuth();
|
||||
const locale = useLocale();
|
||||
const { t } = useTranslation('fields');
|
||||
const { openModal, toggleModal } = useModal();
|
||||
const [renderModal, setRenderModal] = useState(false);
|
||||
const [renderPopup, setRenderPopup] = useState(false);
|
||||
@@ -77,12 +79,12 @@ export const LinkElement = ({ attributes, children, element, editorRef, fieldPro
|
||||
fields: deepCopyObject(element.fields),
|
||||
};
|
||||
|
||||
const state = await buildStateFromSchema({ fieldSchema, data, user, operation: 'update', locale });
|
||||
const state = await buildStateFromSchema({ fieldSchema, data, user, operation: 'update', locale, t });
|
||||
setInitialState(state);
|
||||
};
|
||||
|
||||
awaitInitialState();
|
||||
}, [renderModal, element, fieldSchema, user, locale]);
|
||||
}, [renderModal, element, fieldSchema, user, locale, t]);
|
||||
|
||||
return (
|
||||
<span
|
||||
@@ -146,17 +148,20 @@ export const LinkElement = ({ attributes, children, element, editorRef, fieldPro
|
||||
render={() => (
|
||||
<div className={`${baseClass}__popup`}>
|
||||
{element.linkType === 'internal' && element.doc?.relationTo && element.doc?.value && (
|
||||
<Fragment>
|
||||
Linked to
|
||||
<Trans
|
||||
i18nKey="linkedTo"
|
||||
values={{ label: config.collections.find(({ slug }) => slug === element.doc.relationTo)?.labels?.singular }}
|
||||
>
|
||||
linkedTo
|
||||
<a
|
||||
className={`${baseClass}__link-label`}
|
||||
href={`${config.routes.admin}/collections/${element.doc.relationTo}/${element.doc.value}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{config.collections.find(({ slug }) => slug === element.doc.relationTo)?.labels?.singular}
|
||||
label
|
||||
</a>
|
||||
</Fragment>
|
||||
</Trans>
|
||||
)}
|
||||
{(element.linkType === 'custom' || !element.linkType) && (
|
||||
<a
|
||||
@@ -179,7 +184,7 @@ export const LinkElement = ({ attributes, children, element, editorRef, fieldPro
|
||||
openModal(modalSlug);
|
||||
setRenderModal(true);
|
||||
}}
|
||||
tooltip="Edit"
|
||||
tooltip={t('general:edit')}
|
||||
/>
|
||||
<Button
|
||||
className={`${baseClass}__link-close`}
|
||||
@@ -190,7 +195,7 @@ export const LinkElement = ({ attributes, children, element, editorRef, fieldPro
|
||||
e.preventDefault();
|
||||
unwrapLink(editor);
|
||||
}}
|
||||
tooltip="Remove"
|
||||
tooltip={t('general:remove')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { Fragment, useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../../../../../../utilities/Config';
|
||||
import { useAuth } from '../../../../../../../utilities/Auth';
|
||||
import { useFormFields } from '../../../../../../Form/context';
|
||||
@@ -22,6 +23,7 @@ const createOptions = (collections, permissions) => collections.reduce((options,
|
||||
const RelationshipFields = () => {
|
||||
const { collections } = useConfig();
|
||||
const { permissions } = useAuth();
|
||||
const { t } = useTranslation('fields');
|
||||
|
||||
const [options, setOptions] = useState(() => createOptions(collections, permissions));
|
||||
const relationTo = useFormFields<string>(([fields]) => fields.relationTo?.value as string);
|
||||
@@ -34,13 +36,13 @@ const RelationshipFields = () => {
|
||||
<Fragment>
|
||||
<Select
|
||||
required
|
||||
label="Relation To"
|
||||
label={t('relationTo')}
|
||||
name="relationTo"
|
||||
options={options}
|
||||
/>
|
||||
{relationTo && (
|
||||
<Relationship
|
||||
label="Related Document"
|
||||
label={t('relatedDocument')}
|
||||
name="value"
|
||||
relationTo={relationTo}
|
||||
required
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../../../../../utilities/Config';
|
||||
import ElementButton from '../../Button';
|
||||
import RelationshipIcon from '../../../../../../icons/Relationship';
|
||||
@@ -42,20 +43,25 @@ const RelationshipButton: React.FC<{ path: string }> = ({ path }) => {
|
||||
const { serverURL, routes: { api }, collections } = useConfig();
|
||||
const [renderModal, setRenderModal] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
const [hasEnabledCollections] = useState(() => collections.find(({ admin: { enableRichTextRelationship } }) => enableRichTextRelationship));
|
||||
const modalSlug = `${path}-add-relationship`;
|
||||
|
||||
const handleAddRelationship = useCallback(async (_, { relationTo, value }) => {
|
||||
setLoading(true);
|
||||
|
||||
const res = await requests.get(`${serverURL}${api}/${relationTo}/${value}?depth=0`);
|
||||
const res = await requests.get(`${serverURL}${api}/${relationTo}/${value}?depth=0`, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
insertRelationship(editor, { value: { id: json.id }, relationTo });
|
||||
toggleModal(modalSlug);
|
||||
setRenderModal(false);
|
||||
setLoading(false);
|
||||
}, [editor, toggleModal, modalSlug, api, serverURL]);
|
||||
}, [i18n.language, editor, toggleModal, modalSlug, api, serverURL]);
|
||||
|
||||
useEffect(() => {
|
||||
if (renderModal) {
|
||||
@@ -81,7 +87,7 @@ const RelationshipButton: React.FC<{ path: string }> = ({ path }) => {
|
||||
>
|
||||
<MinimalTemplate className={`${baseClass}__modal-template`}>
|
||||
<header className={`${baseClass}__header`}>
|
||||
<h3>Add Relationship</h3>
|
||||
<h3>{t('addRelationship')}</h3>
|
||||
<Button
|
||||
buttonStyle="none"
|
||||
onClick={() => {
|
||||
@@ -99,7 +105,7 @@ const RelationshipButton: React.FC<{ path: string }> = ({ path }) => {
|
||||
>
|
||||
<Fields />
|
||||
<Submit>
|
||||
Add relationship
|
||||
{t('addRelationship')}
|
||||
</Submit>
|
||||
</Form>
|
||||
</MinimalTemplate>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useFocused, useSelected } from 'slate-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../../../../../utilities/Config';
|
||||
import RelationshipIcon from '../../../../../../icons/Relationship';
|
||||
import usePayloadAPI from '../../../../../../../hooks/usePayloadAPI';
|
||||
@@ -19,6 +20,7 @@ const Element = (props) => {
|
||||
const [relatedCollection] = useState(() => collections.find((coll) => coll.slug === relationTo));
|
||||
const selected = useSelected();
|
||||
const focused = useFocused();
|
||||
const { t } = useTranslation('fields');
|
||||
|
||||
const [{ data }] = usePayloadAPI(
|
||||
`${serverURL}${api}/${relatedCollection.slug}/${value?.id}`,
|
||||
@@ -37,9 +39,7 @@ const Element = (props) => {
|
||||
<RelationshipIcon />
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<div className={`${baseClass}__label`}>
|
||||
{relatedCollection.labels.singular}
|
||||
{' '}
|
||||
Relationship
|
||||
{t('labelRelationship', { label: relatedCollection.labels.singular })}
|
||||
</div>
|
||||
<h5>{data[relatedCollection?.admin?.useAsTitle || 'id']}</h5>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { Fragment, useEffect, useState } from 'react';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { Transforms } from 'slate';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../../../../../utilities/Config';
|
||||
import ElementButton from '../../Button';
|
||||
import UploadIcon from '../../../../../../icons/Upload';
|
||||
@@ -17,6 +17,7 @@ import Button from '../../../../../../elements/Button';
|
||||
import { SanitizedCollectionConfig } from '../../../../../../../../collections/config/types';
|
||||
import PerPage from '../../../../../../elements/PerPage';
|
||||
import { injectVoidElement } from '../../injectVoid';
|
||||
import { getTranslation } from '../../../../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
import '../addSwapModals.scss';
|
||||
@@ -42,6 +43,7 @@ const insertUpload = (editor, { value, relationTo }) => {
|
||||
};
|
||||
|
||||
const UploadButton: React.FC<{ path: string }> = ({ path }) => {
|
||||
const { t, i18n } = useTranslation('upload');
|
||||
const { toggleModal, isModalOpen } = useModal();
|
||||
const editor = useSlate();
|
||||
const { serverURL, routes: { api }, collections } = useConfig();
|
||||
@@ -50,14 +52,13 @@ const UploadButton: React.FC<{ path: string }> = ({ path }) => {
|
||||
const [modalCollectionOption, setModalCollectionOption] = useState<{ label: string, value: string }>(() => {
|
||||
const firstAvailableCollection = collections.find(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship));
|
||||
if (firstAvailableCollection) {
|
||||
return { label: firstAvailableCollection.labels.singular, value: firstAvailableCollection.slug };
|
||||
return { label: getTranslation(firstAvailableCollection.labels.singular, i18n), value: firstAvailableCollection.slug };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
const [modalCollection, setModalCollection] = useState<SanitizedCollectionConfig>(() => collections.find(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship)));
|
||||
|
||||
const [fields, setFields] = useState(() => (modalCollection ? formatFields(modalCollection) : undefined));
|
||||
const [fields, setFields] = useState(() => (modalCollection ? formatFields(modalCollection, t) : undefined));
|
||||
const [limit, setLimit] = useState<number>();
|
||||
const [sort, setSort] = useState(null);
|
||||
const [where, setWhere] = useState(null);
|
||||
@@ -73,9 +74,9 @@ const UploadButton: React.FC<{ path: string }> = ({ path }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (modalCollection) {
|
||||
setFields(formatFields(modalCollection));
|
||||
setFields(formatFields(modalCollection, t));
|
||||
}
|
||||
}, [modalCollection]);
|
||||
}, [modalCollection, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (renderModal) {
|
||||
@@ -127,9 +128,7 @@ const UploadButton: React.FC<{ path: string }> = ({ path }) => {
|
||||
<MinimalTemplate width="wide">
|
||||
<header className={`${baseModalClass}__header`}>
|
||||
<h1>
|
||||
Add
|
||||
{' '}
|
||||
{modalCollection.labels.singular}
|
||||
{t('addLabel', { label: getTranslation(modalCollection.labels.singular, i18n) })}
|
||||
</h1>
|
||||
<Button
|
||||
icon="x"
|
||||
@@ -144,12 +143,12 @@ const UploadButton: React.FC<{ path: string }> = ({ path }) => {
|
||||
</header>
|
||||
{moreThanOneAvailableCollection && (
|
||||
<div className={`${baseModalClass}__select-collection-wrap`}>
|
||||
<Label label="Select a Collection to Browse" />
|
||||
<Label label={t('selectCollectionToBrowse')} />
|
||||
<ReactSelect
|
||||
className={`${baseClass}__select-collection`}
|
||||
value={modalCollectionOption}
|
||||
onChange={setModalCollectionOption}
|
||||
options={availableCollections.map((coll) => ({ label: coll.labels.singular, value: coll.slug }))}
|
||||
options={availableCollections.map((coll) => ({ label: getTranslation(coll.labels.singular, i18n), value: coll.slug }))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -198,7 +197,7 @@ const UploadButton: React.FC<{ path: string }> = ({ path }) => {
|
||||
-
|
||||
{data.totalPages > 1 ? data.limit : data.totalDocs}
|
||||
{' '}
|
||||
of
|
||||
{t('general:of')}
|
||||
{' '}
|
||||
{data.totalDocs}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Transforms, Element } from 'slate';
|
||||
import { ReactEditor, useSlateStatic } from 'slate-react';
|
||||
import { Modal } from '@faceless-ui/modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../../../../../../utilities/Auth';
|
||||
import { SanitizedCollectionConfig } from '../../../../../../../../../collections/config/types';
|
||||
import buildStateFromSchema from '../../../../../../Form/buildStateFromSchema';
|
||||
@@ -13,6 +14,7 @@ import Form from '../../../../../../Form';
|
||||
import Submit from '../../../../../../Submit';
|
||||
import { Field } from '../../../../../../../../../fields/config/types';
|
||||
import { useLocale } from '../../../../../../../utilities/Locale';
|
||||
import { getTranslation } from '../../../../../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -32,6 +34,7 @@ export const EditModal: React.FC<Props> = ({ slug, closeModal, relatedCollection
|
||||
const [initialState, setInitialState] = useState({});
|
||||
const { user } = useAuth();
|
||||
const locale = useLocale();
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
|
||||
const handleUpdateEditData = useCallback((_, data) => {
|
||||
const newNode = {
|
||||
@@ -50,12 +53,12 @@ export const EditModal: React.FC<Props> = ({ slug, closeModal, relatedCollection
|
||||
|
||||
useEffect(() => {
|
||||
const awaitInitialState = async () => {
|
||||
const state = await buildStateFromSchema({ fieldSchema, data: { ...element?.fields || {} }, user, operation: 'update', locale });
|
||||
const state = await buildStateFromSchema({ fieldSchema, data: { ...element?.fields || {} }, user, operation: 'update', locale, t });
|
||||
setInitialState(state);
|
||||
};
|
||||
|
||||
awaitInitialState();
|
||||
}, [fieldSchema, element.fields, user, locale]);
|
||||
}, [fieldSchema, element.fields, user, locale, t]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -65,11 +68,7 @@ export const EditModal: React.FC<Props> = ({ slug, closeModal, relatedCollection
|
||||
<MinimalTemplate width="wide">
|
||||
<header className={`${baseClass}__header`}>
|
||||
<h1>
|
||||
Edit
|
||||
{' '}
|
||||
{relatedCollectionConfig.labels.singular}
|
||||
{' '}
|
||||
data
|
||||
{ t('editLabelData', { label: getTranslation(relatedCollectionConfig.labels.singular, i18n) }) }
|
||||
</h1>
|
||||
<Button
|
||||
icon="x"
|
||||
@@ -90,7 +89,7 @@ export const EditModal: React.FC<Props> = ({ slug, closeModal, relatedCollection
|
||||
fieldSchema={fieldSchema}
|
||||
/>
|
||||
<Submit>
|
||||
Save changes
|
||||
{t('saveChanges')}
|
||||
</Submit>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from 'react';
|
||||
import { Modal } from '@faceless-ui/modal';
|
||||
import { Element, Transforms } from 'slate';
|
||||
import { ReactEditor, useSlateStatic } from 'slate-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../../../../../../utilities/Config';
|
||||
import { SanitizedCollectionConfig } from '../../../../../../../../../collections/config/types';
|
||||
import usePayloadAPI from '../../../../../../../../hooks/usePayloadAPI';
|
||||
@@ -14,6 +15,7 @@ import UploadGallery from '../../../../../../../elements/UploadGallery';
|
||||
import Paginator from '../../../../../../../elements/Paginator';
|
||||
import PerPage from '../../../../../../../elements/PerPage';
|
||||
import formatFields from '../../../../../../../views/collections/List/formatFields';
|
||||
import { getTranslation } from '../../../../../../../../../utilities/getTranslation';
|
||||
|
||||
import '../../addSwapModals.scss';
|
||||
|
||||
@@ -29,11 +31,12 @@ type Props = {
|
||||
export const SwapUploadModal: React.FC<Props> = ({ closeModal, element, setRelatedCollectionConfig, relatedCollectionConfig, slug }) => {
|
||||
const { collections, serverURL, routes: { api } } = useConfig();
|
||||
const editor = useSlateStatic();
|
||||
const { t, i18n } = useTranslation('upload');
|
||||
|
||||
const [modalCollection, setModalCollection] = React.useState(relatedCollectionConfig);
|
||||
const [modalCollectionOption, setModalCollectionOption] = React.useState<{ label: string, value: string }>({ label: relatedCollectionConfig.labels.singular, value: relatedCollectionConfig.slug });
|
||||
const [modalCollectionOption, setModalCollectionOption] = React.useState<{ label: string, value: string }>({ label: getTranslation(relatedCollectionConfig.labels.singular, i18n), value: relatedCollectionConfig.slug });
|
||||
const [availableCollections] = React.useState(() => collections.filter(({ admin: { enableRichTextRelationship }, upload }) => (Boolean(upload) && enableRichTextRelationship)));
|
||||
const [fields, setFields] = React.useState(() => formatFields(modalCollection));
|
||||
const [fields, setFields] = React.useState(() => formatFields(modalCollection, t));
|
||||
|
||||
const [limit, setLimit] = React.useState<number>();
|
||||
const [sort, setSort] = React.useState(null);
|
||||
@@ -82,9 +85,9 @@ export const SwapUploadModal: React.FC<Props> = ({ closeModal, element, setRelat
|
||||
}, [setParams, page, sort, where, limit]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setFields(formatFields(modalCollection));
|
||||
setFields(formatFields(modalCollection, t));
|
||||
setLimit(modalCollection.admin.pagination.defaultLimit);
|
||||
}, [modalCollection]);
|
||||
}, [modalCollection, t]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setModalCollection(collections.find(({ slug: collectionSlug }) => modalCollectionOption.value === collectionSlug));
|
||||
@@ -98,9 +101,7 @@ export const SwapUploadModal: React.FC<Props> = ({ closeModal, element, setRelat
|
||||
<MinimalTemplate width="wide">
|
||||
<header className={`${baseClass}__header`}>
|
||||
<h1>
|
||||
Choose
|
||||
{' '}
|
||||
{modalCollection.labels.singular}
|
||||
{t('chooseLabel', { label: getTranslation(modalCollection.labels.singular, i18n) })}
|
||||
</h1>
|
||||
<Button
|
||||
icon="x"
|
||||
@@ -113,12 +114,12 @@ export const SwapUploadModal: React.FC<Props> = ({ closeModal, element, setRelat
|
||||
{
|
||||
moreThanOneAvailableCollection && (
|
||||
<div className={`${baseClass}__select-collection-wrap`}>
|
||||
<Label label="Select a Collection to Browse" />
|
||||
<Label label={t('selectCollectionToBrowse')} />
|
||||
<ReactSelect
|
||||
className={`${baseClass}__select-collection`}
|
||||
value={modalCollectionOption}
|
||||
onChange={setModalCollectionOption}
|
||||
options={availableCollections.map((coll) => ({ label: coll.labels.singular, value: coll.slug }))}
|
||||
options={availableCollections.map((coll) => ({ label: getTranslation(coll.labels.singular, i18n), value: coll.slug }))}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -165,7 +166,7 @@ export const SwapUploadModal: React.FC<Props> = ({ closeModal, element, setRelat
|
||||
-
|
||||
{data.totalPages > 1 ? data.limit : data.totalDocs}
|
||||
{' '}
|
||||
of
|
||||
{t('general:of')}
|
||||
{' '}
|
||||
{data.totalDocs}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import { Transforms } from 'slate';
|
||||
import { ReactEditor, useSlateStatic, useFocused, useSelected } from 'slate-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../../../../../utilities/Config';
|
||||
import usePayloadAPI from '../../../../../../../hooks/usePayloadAPI';
|
||||
import FileGraphic from '../../../../../../graphics/File';
|
||||
@@ -10,6 +11,7 @@ import Button from '../../../../../../elements/Button';
|
||||
import { SanitizedCollectionConfig } from '../../../../../../../../collections/config/types';
|
||||
import { SwapUploadModal } from './SwapUploadModal';
|
||||
import { EditModal } from './EditModal';
|
||||
import { getTranslation } from '../../../../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -25,6 +27,7 @@ const Element = ({ attributes, children, element, path, fieldProps }) => {
|
||||
const { collections, serverURL, routes: { api } } = useConfig();
|
||||
const [modalToRender, setModalToRender] = useState(undefined);
|
||||
const [relatedCollection, setRelatedCollection] = useState<SanitizedCollectionConfig>(() => collections.find((coll) => coll.slug === relationTo));
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
|
||||
const editor = useSlateStatic();
|
||||
const selected = useSelected();
|
||||
@@ -85,7 +88,7 @@ const Element = ({ attributes, children, element, path, fieldProps }) => {
|
||||
</div>
|
||||
<div className={`${baseClass}__topRowRightPanel`}>
|
||||
<div className={`${baseClass}__collectionLabel`}>
|
||||
{relatedCollection.labels.singular}
|
||||
{getTranslation(relatedCollection.labels.singular, i18n)}
|
||||
</div>
|
||||
<div className={`${baseClass}__actions`}>
|
||||
{fieldSchema && (
|
||||
@@ -98,7 +101,7 @@ const Element = ({ attributes, children, element, path, fieldProps }) => {
|
||||
e.preventDefault();
|
||||
setModalToRender('edit');
|
||||
}}
|
||||
tooltip="Edit"
|
||||
tooltip={t('general:edit')}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
@@ -110,7 +113,7 @@ const Element = ({ attributes, children, element, path, fieldProps }) => {
|
||||
e.preventDefault();
|
||||
setModalToRender('swap');
|
||||
}}
|
||||
tooltip="Swap Upload"
|
||||
tooltip={t('swapUpload')}
|
||||
/>
|
||||
<Button
|
||||
icon="x"
|
||||
@@ -121,7 +124,7 @@ const Element = ({ attributes, children, element, path, fieldProps }) => {
|
||||
e.preventDefault();
|
||||
removeUpload();
|
||||
}}
|
||||
tooltip="Remove Upload"
|
||||
tooltip={t('removeUpload')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Label from '../../Label';
|
||||
import Error from '../../Error';
|
||||
import FieldDescription from '../../FieldDescription';
|
||||
@@ -6,7 +7,7 @@ import { OptionObject, SelectField } from '../../../../../fields/config/types';
|
||||
import { Description } from '../../FieldDescription/types';
|
||||
import ReactSelect from '../../../elements/ReactSelect';
|
||||
import { Value as ReactSelectValue } from '../../../elements/ReactSelect/types';
|
||||
// import { FieldType } from '../../useField/types';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -48,6 +49,8 @@ const SelectInput: React.FC<SelectInputProps> = (props) => {
|
||||
isClearable,
|
||||
} = props;
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const classes = [
|
||||
'field-type',
|
||||
'select',
|
||||
@@ -87,7 +90,7 @@ const SelectInput: React.FC<SelectInputProps> = (props) => {
|
||||
value={valueToRender as ReactSelectValue}
|
||||
showError={showError}
|
||||
isDisabled={readOnly}
|
||||
options={options}
|
||||
options={options.map((option) => ({ ...option, label: getTranslation(option.label, i18n) }))}
|
||||
isMulti={hasMany}
|
||||
isSortable={isSortable}
|
||||
isClearable={isClearable}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import RenderFields from '../../RenderFields';
|
||||
import withCondition from '../../withCondition';
|
||||
import { Props } from './types';
|
||||
@@ -7,6 +8,7 @@ import FieldDescription from '../../FieldDescription';
|
||||
import toKebabCase from '../../../../../utilities/toKebabCase';
|
||||
import { useCollapsible } from '../../../elements/Collapsible/provider';
|
||||
import { TabsProvider } from './provider';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
import { usePreferences } from '../../../utilities/Preferences';
|
||||
import { DocumentPreferences } from '../../../../../preferences/types';
|
||||
import { useDocumentInfo } from '../../../utilities/DocumentInfo';
|
||||
@@ -30,6 +32,7 @@ const TabsField: React.FC<Props> = (props) => {
|
||||
|
||||
const { getPreference, setPreference } = usePreferences();
|
||||
const { preferencesKey } = useDocumentInfo();
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const isWithinCollapsible = useCollapsible();
|
||||
const [activeTabIndex, setActiveTabIndex] = useState<number>(0);
|
||||
@@ -93,10 +96,10 @@ const TabsField: React.FC<Props> = (props) => {
|
||||
activeTabIndex === tabIndex && `${baseClass}__tab-button--active`,
|
||||
].filter(Boolean).join(' ')}
|
||||
onClick={() => {
|
||||
handleTabChange(tabIndex)
|
||||
handleTabChange(tabIndex);
|
||||
}}
|
||||
>
|
||||
{tab.label ? tab.label : (tabHasName(tab) && tab.name)}
|
||||
{tab.label ? getTranslation(tab.label, i18n) : (tabHasName(tab) && tab.name)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Label from '../../Label';
|
||||
import Error from '../../Error';
|
||||
import FieldDescription from '../../FieldDescription';
|
||||
import { TextField } from '../../../../../fields/config/types';
|
||||
import { Description } from '../../FieldDescription/types';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -17,7 +19,7 @@ export type TextInputProps = Omit<TextField, 'type'> & {
|
||||
description?: Description
|
||||
onChange?: (e: ChangeEvent<HTMLInputElement>) => void
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
|
||||
placeholder?: string
|
||||
placeholder?: Record<string, string> | string
|
||||
style?: React.CSSProperties
|
||||
className?: string
|
||||
width?: string
|
||||
@@ -43,6 +45,8 @@ const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
inputRef,
|
||||
} = props;
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const classes = [
|
||||
'field-type',
|
||||
'text',
|
||||
@@ -75,11 +79,12 @@ const TextInput: React.FC<TextInputProps> = (props) => {
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
disabled={readOnly}
|
||||
placeholder={placeholder}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
type="text"
|
||||
name={path}
|
||||
/>
|
||||
<FieldDescription
|
||||
className={`field-description-${path.replace(/\./gi, '__')}`}
|
||||
value={value}
|
||||
description={description}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Label from '../../Label';
|
||||
import Error from '../../Error';
|
||||
import FieldDescription from '../../FieldDescription';
|
||||
import { TextareaField } from '../../../../../fields/config/types';
|
||||
import { Description } from '../../FieldDescription/types';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -16,7 +18,7 @@ export type TextAreaInputProps = Omit<TextareaField, 'type'> & {
|
||||
value?: string
|
||||
description?: Description
|
||||
onChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void
|
||||
placeholder?: string
|
||||
placeholder?: Record<string, string> | string
|
||||
style?: React.CSSProperties
|
||||
className?: string
|
||||
width?: string
|
||||
@@ -41,6 +43,8 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
rows,
|
||||
} = props;
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const classes = [
|
||||
'field-type',
|
||||
'textarea',
|
||||
@@ -78,7 +82,7 @@ const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
disabled={readOnly}
|
||||
placeholder={placeholder}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
name={path}
|
||||
rows={rows}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useField from '../../useField';
|
||||
import withCondition from '../../withCondition';
|
||||
import { textarea } from '../../../../../fields/validations';
|
||||
import { Props } from './types';
|
||||
import TextareaInput from './Input';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -28,6 +30,8 @@ const Textarea: React.FC<Props> = (props) => {
|
||||
label,
|
||||
} = props;
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const path = pathFromProps || name;
|
||||
|
||||
const memoizedValidate = useCallback((value, options) => {
|
||||
@@ -57,7 +61,7 @@ const Textarea: React.FC<Props> = (props) => {
|
||||
required={required}
|
||||
label={label}
|
||||
value={value as string}
|
||||
placeholder={placeholder}
|
||||
placeholder={getTranslation(placeholder, i18n)}
|
||||
readOnly={readOnly}
|
||||
style={style}
|
||||
className={className}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../../../utilities/Config';
|
||||
import { useAuth } from '../../../../utilities/Auth';
|
||||
import MinimalTemplate from '../../../../templates/Minimal';
|
||||
@@ -9,6 +10,7 @@ import RenderFields from '../../../RenderFields';
|
||||
import FormSubmit from '../../../Submit';
|
||||
import Upload from '../../../../views/collections/Edit/Upload';
|
||||
import ViewDescription from '../../../../elements/ViewDescription';
|
||||
import { getTranslation } from '../../../../../../utilities/getTranslation';
|
||||
import { Props } from './types';
|
||||
|
||||
import './index.scss';
|
||||
@@ -31,6 +33,7 @@ const AddUploadModal: React.FC<Props> = (props) => {
|
||||
const { permissions } = useAuth();
|
||||
const { serverURL, routes: { api } } = useConfig();
|
||||
const { toggleModal } = useModal();
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
|
||||
const onSuccess = useCallback((json) => {
|
||||
toggleModal(slug);
|
||||
@@ -59,11 +62,9 @@ const AddUploadModal: React.FC<Props> = (props) => {
|
||||
<header className={`${baseClass}__header`}>
|
||||
<div>
|
||||
<h1>
|
||||
New
|
||||
{' '}
|
||||
{collection.labels.singular}
|
||||
{t('newLabel', { label: getTranslation(collection.labels.singular, i18n) })}
|
||||
</h1>
|
||||
<FormSubmit>Save</FormSubmit>
|
||||
<FormSubmit>{t('general:save')}</FormSubmit>
|
||||
<Button
|
||||
icon="x"
|
||||
round
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '../../../elements/Button';
|
||||
import Label from '../../Label';
|
||||
import Error from '../../Error';
|
||||
@@ -12,6 +13,7 @@ import AddModal from './Add';
|
||||
import SelectExistingModal from './SelectExisting';
|
||||
import { SanitizedCollectionConfig } from '../../../../../collections/config/types';
|
||||
import { useEditDepth, EditDepthContext } from '../../../utilities/EditDepth';
|
||||
import { getTranslation } from '../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -60,6 +62,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
} = props;
|
||||
|
||||
const { toggleModal, modalState } = useModal();
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
const editDepth = useEditDepth();
|
||||
|
||||
const addModalSlug = `${path}-add-depth-${editDepth}`;
|
||||
@@ -80,7 +83,12 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
useEffect(() => {
|
||||
if (typeof value === 'string' && value !== '') {
|
||||
const fetchFile = async () => {
|
||||
const response = await fetch(`${serverURL}${api}/${relationTo}/${value}`, { credentials: 'include' });
|
||||
const response = await fetch(`${serverURL}${api}/${relationTo}/${value}`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
});
|
||||
if (response.ok) {
|
||||
const json = await response.json();
|
||||
setFile(json);
|
||||
@@ -99,6 +107,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
relationTo,
|
||||
api,
|
||||
serverURL,
|
||||
i18n,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -144,9 +153,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
setModalToRender(addModalSlug);
|
||||
}}
|
||||
>
|
||||
Upload new
|
||||
{' '}
|
||||
{collection.labels.singular}
|
||||
{t('uploadNewLabel', { label: getTranslation(collection.labels.singular, i18n) })}
|
||||
</Button>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
@@ -155,7 +162,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
||||
setModalToRender(selectExistingModalSlug);
|
||||
}}
|
||||
>
|
||||
Choose from existing
|
||||
{t('chooseFromExisting')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { Fragment, useState, useEffect } from 'react';
|
||||
import equal from 'deep-equal';
|
||||
import { Modal, useModal } from '@faceless-ui/modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../../../utilities/Config';
|
||||
import { useAuth } from '../../../../utilities/Auth';
|
||||
import { Where } from '../../../../../../types';
|
||||
@@ -17,6 +18,7 @@ import { getFilterOptionsQuery } from '../../getFilterOptionsQuery';
|
||||
import { useDocumentInfo } from '../../../../utilities/DocumentInfo';
|
||||
import { useForm } from '../../../Form/context';
|
||||
import ViewDescription from '../../../../elements/ViewDescription';
|
||||
import { getTranslation } from '../../../../../../utilities/getTranslation';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
@@ -45,7 +47,8 @@ const SelectExistingUploadModal: React.FC<Props> = (props) => {
|
||||
const { user } = useAuth();
|
||||
const { getData, getSiblingData } = useForm();
|
||||
const { toggleModal, isModalOpen } = useModal();
|
||||
const [fields] = useState(() => formatFields(collection));
|
||||
const { t, i18n } = useTranslation('fields');
|
||||
const [fields] = useState(() => formatFields(collection, t));
|
||||
const [limit, setLimit] = useState(defaultLimit);
|
||||
const [sort, setSort] = useState(null);
|
||||
const [where, setWhere] = useState(null);
|
||||
@@ -105,10 +108,7 @@ const SelectExistingUploadModal: React.FC<Props> = (props) => {
|
||||
<header className={`${baseClass}__header`}>
|
||||
<div>
|
||||
<h1>
|
||||
{' '}
|
||||
Select existing
|
||||
{' '}
|
||||
{collection.labels.singular}
|
||||
{t('selectExistingLabel', { label: getTranslation(collection.labels.singular, i18n) })}
|
||||
</h1>
|
||||
<Button
|
||||
icon="x"
|
||||
@@ -163,7 +163,7 @@ const SelectExistingUploadModal: React.FC<Props> = (props) => {
|
||||
-
|
||||
{data.totalPages > 1 ? data.limit : data.totalDocs}
|
||||
{' '}
|
||||
of
|
||||
{t('general:of')}
|
||||
{' '}
|
||||
{data.totalDocs}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../utilities/Auth';
|
||||
import { useFormProcessing, useFormSubmitted, useFormModified, useForm, useFormFields } from '../Form/context';
|
||||
import { Options, FieldType } from './types';
|
||||
@@ -23,6 +24,7 @@ const useField = <T extends unknown>(options: Options): FieldType<T> => {
|
||||
const operation = useOperation();
|
||||
const field = useFormFields(([fields]) => fields[path]);
|
||||
const dispatchField = useFormFields(([_, dispatch]) => dispatch);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { getData, getSiblingData, setModified } = useForm();
|
||||
|
||||
@@ -92,6 +94,7 @@ const useField = <T extends unknown>(options: Options): FieldType<T> => {
|
||||
data: getData(),
|
||||
siblingData: getSiblingData(path),
|
||||
operation,
|
||||
t,
|
||||
};
|
||||
|
||||
const validationResult = typeof validate === 'function' ? await validate(value, validateOptions) : true;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import NavigationPrompt from 'react-router-navigation-prompt';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuth } from '../../utilities/Auth';
|
||||
import { useFormModified } from '../../forms/Form/context';
|
||||
import MinimalTemplate from '../../templates/Minimal';
|
||||
@@ -12,24 +13,25 @@ const modalSlug = 'leave-without-saving';
|
||||
const LeaveWithoutSaving: React.FC = () => {
|
||||
const modified = useFormModified();
|
||||
const { user } = useAuth();
|
||||
const { t } = useTranslation('general');
|
||||
|
||||
return (
|
||||
<NavigationPrompt when={Boolean(modified && user)}>
|
||||
{({ onConfirm, onCancel }) => (
|
||||
<div className={modalSlug}>
|
||||
<MinimalTemplate className={`${modalSlug}__template`}>
|
||||
<h1>Leave without saving</h1>
|
||||
<p>Your changes have not been saved. If you leave now, you will lose your changes.</p>
|
||||
<h1>{t('leaveWithoutSaving')}</h1>
|
||||
<p>{t('changesNotSaved')}</p>
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
buttonStyle="secondary"
|
||||
>
|
||||
Stay on this page
|
||||
{t('stayOnThisPage')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Leave anyway
|
||||
{t('leaveAnyway')}
|
||||
</Button>
|
||||
</MinimalTemplate>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useModal, Modal } from '@faceless-ui/modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import MinimalTemplate from '../../templates/Minimal';
|
||||
import Button from '../../elements/Button';
|
||||
@@ -23,6 +24,7 @@ const StayLoggedInModal: React.FC<Props> = (props) => {
|
||||
}
|
||||
} = config;
|
||||
const { toggleModal } = useModal();
|
||||
const { t } = useTranslation('authentication');
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -30,8 +32,8 @@ const StayLoggedInModal: React.FC<Props> = (props) => {
|
||||
slug="stay-logged-in"
|
||||
>
|
||||
<MinimalTemplate className={`${baseClass}__template`}>
|
||||
<h1>Stay logged in</h1>
|
||||
<p>You haven't been active in a little while and will shortly be automatically logged out for your own security. Would you like to stay logged in?</p>
|
||||
<h1>{t('stayLoggedIn')}</h1>
|
||||
<p>{t('youAreInactive')}</p>
|
||||
<div className={`${baseClass}__actions`}>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
@@ -40,14 +42,14 @@ const StayLoggedInModal: React.FC<Props> = (props) => {
|
||||
history.push(`${admin}${logoutRoute}`);
|
||||
}}
|
||||
>
|
||||
Log out
|
||||
{t('logOut')}
|
||||
</Button>
|
||||
<Button onClick={() => {
|
||||
refreshCookie();
|
||||
toggleModal(modalSlug);
|
||||
}}
|
||||
>
|
||||
Stay logged in
|
||||
{t('stayLoggedIn')}
|
||||
</Button>
|
||||
</div>
|
||||
</MinimalTemplate>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useConfig } from '../../utilities/Config';
|
||||
import DefaultNav from '../../elements/Nav';
|
||||
import RenderCustomComponent from '../../utilities/RenderCustomComponent';
|
||||
@@ -19,6 +20,7 @@ const Default: React.FC<Props> = ({ children, className }) => {
|
||||
},
|
||||
} = {},
|
||||
} = useConfig();
|
||||
const { t } = useTranslation('general');
|
||||
|
||||
const classes = [
|
||||
baseClass,
|
||||
@@ -28,9 +30,9 @@ const Default: React.FC<Props> = ({ children, className }) => {
|
||||
return (
|
||||
<div className={classes}>
|
||||
<Meta
|
||||
title="Dashboard"
|
||||
description="Dashboard for Payload CMS"
|
||||
keywords="Dashboard, Payload, CMS"
|
||||
title={t('dashboard')}
|
||||
description={`${t('dashboard')} Payload CMS`}
|
||||
keywords={`${t('dashboard')}, Payload CMS`}
|
||||
/>
|
||||
<RenderCustomComponent
|
||||
DefaultComponent={DefaultNav}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, {
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import { useLocation, useHistory } from 'react-router-dom';
|
||||
import { useModal } from '@faceless-ui/modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { User, Permissions } from '../../../../auth/types';
|
||||
import { useConfig } from '../Config';
|
||||
import { requests } from '../../../api';
|
||||
@@ -38,7 +39,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
const [permissions, setPermissions] = useState<Permissions>();
|
||||
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
const { openModal, closeAllModals } = useModal();
|
||||
const [lastLocationChange, setLastLocationChange] = useState(0);
|
||||
const debouncedLocationChange = useDebounce(lastLocationChange, 10000);
|
||||
@@ -51,7 +52,11 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
if (exp && remainingTime < 120) {
|
||||
setTimeout(async () => {
|
||||
const request = await requests.post(`${serverURL}${api}/${userSlug}/refresh-token`);
|
||||
const request = await requests.post(`${serverURL}${api}/${userSlug}/refresh-token`, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
});
|
||||
|
||||
if (request.status === 200) {
|
||||
const json = await request.json();
|
||||
@@ -62,7 +67,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}, [setUser, push, exp, admin, api, serverURL, userSlug]);
|
||||
}, [exp, serverURL, api, userSlug, push, admin, logoutInactivityRoute, i18n]);
|
||||
|
||||
const setToken = useCallback((token: string) => {
|
||||
const decoded = jwtDecode<User>(token);
|
||||
@@ -79,7 +84,11 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
// On mount, get user and set
|
||||
useEffect(() => {
|
||||
const fetchMe = async () => {
|
||||
const request = await requests.get(`${serverURL}${api}/${userSlug}/me`);
|
||||
const request = await requests.get(`${serverURL}${api}/${userSlug}/me`, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
});
|
||||
|
||||
if (request.status === 200) {
|
||||
const json = await request.json();
|
||||
@@ -93,7 +102,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
};
|
||||
|
||||
fetchMe();
|
||||
}, [setToken, api, serverURL, userSlug]);
|
||||
}, [i18n, setToken, api, serverURL, userSlug]);
|
||||
|
||||
// When location changes, refresh cookie
|
||||
useEffect(() => {
|
||||
@@ -109,7 +118,11 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
// When user changes, get new access
|
||||
useEffect(() => {
|
||||
async function getPermissions() {
|
||||
const request = await requests.get(`${serverURL}${api}/access`);
|
||||
const request = await requests.get(`${serverURL}${api}/access`, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
});
|
||||
|
||||
if (request.status === 200) {
|
||||
const json: Permissions = await request.json();
|
||||
@@ -120,7 +133,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
if (id) {
|
||||
getPermissions();
|
||||
}
|
||||
}, [id, api, serverURL]);
|
||||
}, [i18n, id, api, serverURL]);
|
||||
|
||||
useEffect(() => {
|
||||
let reminder: ReturnType<typeof setTimeout>;
|
||||
@@ -154,7 +167,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
return () => {
|
||||
if (forceLogOut) clearTimeout(forceLogOut);
|
||||
};
|
||||
}, [exp, push, closeAllModals, admin]);
|
||||
}, [exp, push, closeAllModals, admin, i18n, logoutInactivityRoute]);
|
||||
|
||||
return (
|
||||
<Context.Provider value={{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user