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:
Dan Ribbens
2022-11-18 07:36:30 -05:00
committed by GitHub
parent c49ee15b6a
commit bab34d82f5
279 changed files with 9547 additions and 3242 deletions

View File

@@ -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:

View File

@@ -13,7 +13,7 @@ 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. |
| **`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. |
@@ -23,8 +23,10 @@ It's often best practice to write your Collections in separate files and then im
| **`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)|
| **`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.*

View File

@@ -13,16 +13,18 @@ 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. |
| **`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
View 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.

View File

@@ -72,7 +72,7 @@ const ExampleCollection: CollectionConfig = {
{
name: 'title',
type: 'text',
}
},
{
name: 'image',
type: 'upload',

View File

@@ -23,9 +23,9 @@ 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. |
| **`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. |
@@ -58,12 +58,13 @@ Blocks are defined as separate configs of their own.
</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. |
| **`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

View File

@@ -73,12 +73,13 @@ There are two arguments available to custom validation functions.
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 |
|---------------|--------------------------------------------------------------------------------------------------------------------------|
| `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:

View File

@@ -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: [
...
],

View File

@@ -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",

View File

@@ -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> => {

View File

@@ -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 />}>

View File

@@ -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>
);

View File

@@ -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&nbsp;
{formatDistance(new Date(), new Date(lastSaved))}
&nbsp;ago
{t('lastSavedAgo', {
distance: Math.round((Number(new Date(lastSaved)) - Number(new Date())) / 1000 / 60),
})}
</React.Fragment>
)}
</div>

View File

@@ -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 && (

View File

@@ -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>
);
})}

View File

@@ -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

View File

@@ -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}
{' '}
&quot;
<Trans
i18nKey="aboutToDelete"
values={{ label: singular, title: titleToRender }}
t={t}
>
aboutToDelete
<strong>
{titleToRender}
</strong>
&quot;. Are you sure?
</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>

View File

@@ -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}`, {
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}`, {
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>

View File

@@ -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>
)}

View File

@@ -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
{' '}
<Trans
i18nKey="generatingNewAPIKeyWillInvalidate"
t={t}
>
generatingNewAPIKeyWillInvalidate
<strong>invalidate</strong>
{' '}
the previous key.
{' '}
Are you sure you wish to continue?
</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>

View File

@@ -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>

View File

@@ -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`;

View File

@@ -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>

View File

@@ -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>
)}

View File

@@ -5,7 +5,6 @@ import PopupButton from './PopupButton';
import useIntersect from '../../../hooks/useIntersect';
import './index.scss';
import useThrottledEffect from '../../../hooks/useThrottledEffect';
const baseClass = 'popup';

View File

@@ -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>
);
}

View File

@@ -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>
);
};

View File

@@ -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}

View File

@@ -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>
);
};

View File

@@ -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}>

View File

@@ -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

View File

@@ -1,5 +1,5 @@
export type Props = {
label: string,
label: Record<string, string> | string,
name: string,
disable?: boolean,
}

View File

@@ -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}

View File

@@ -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&apos;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>

View File

@@ -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

View File

@@ -1,5 +1,5 @@
export type StepNavItem = {
label: string
label: Record<string, string> | string
url?: string
}

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -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

View File

@@ -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 }) => (
const NumberField: React.FC<Props> = ({ onChange, value }) => {
const { t } = useTranslation('general');
return (
<input
placeholder="Enter a value"
placeholder={t('enterAValue')}
className={baseClass}
type="number"
onChange={(e) => onChange(e.target.value)}
value={value}
/>
);
);
};
export default NumberField;

View File

@@ -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) {

View File

@@ -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,
});

View File

@@ -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

View File

@@ -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>
)}

View File

@@ -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) {

View File

@@ -1,4 +1,4 @@
export type Props = {
showError?: boolean
message?: string
message: string
}

View File

@@ -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>
);
}

View File

@@ -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

View File

@@ -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,
});
});
}

View File

@@ -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);

View File

@@ -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,
}));
}
});

View File

@@ -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;

View File

@@ -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>
);

View File

@@ -1,5 +1,5 @@
export type Props = {
label?: string | false | JSX.Element
label?: Record<string, string> | string | false | JSX.Element
required?: boolean
htmlFor?: string
}

View File

@@ -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>
);
}

View File

@@ -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>
);
};

View File

@@ -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);

View File

@@ -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>
)}

View File

@@ -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 />

View File

@@ -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>
);
};

View File

@@ -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) => {

View File

@@ -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 }) => (

View File

@@ -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

View File

@@ -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

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}
/>

View File

@@ -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}

View File

@@ -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}

View File

@@ -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>
);

View File

@@ -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

View File

@@ -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"

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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,
});

View File

@@ -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

View File

@@ -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) => {

View File

@@ -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);
}
}

View File

@@ -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&nbsp;
<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>
)}

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>
);
})}

View File

@@ -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}
/>

View File

@@ -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}
/>

View File

@@ -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}

View File

@@ -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

View File

@@ -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>
)}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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&apos;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>

View File

@@ -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}

View File

@@ -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