Compare commits

...

25 Commits

Author SHA1 Message Date
Dan Ribbens
bb21f51f74 Merge branch 'fix/safely-validate-relationships' 2023-05-22 13:58:47 -04:00
Dan Ribbens
666c2383ba chore: lint fix 2023-05-22 13:58:25 -04:00
James
2703853edb fix: safely validates null relations 2023-05-22 13:46:11 -04:00
Dan Ribbens
368103d76d Merge branch 'master' of github.com:payloadcms/payload 2023-05-20 05:00:08 -04:00
Quentin Beauperin
3a2462baba docs: add missing admin.group property in configuration/globals (#2684) 2023-05-20 04:35:50 -04:00
Jessica Boezwinkle
bd2bfbbb93 docs: spacing fix on graphql docs 2023-05-19 16:59:23 +01:00
Jessica Boezwinkle
1300fc864c docs: additional params for find operation rest-api 2023-05-19 15:10:24 +01:00
Jessica Boezwinkle
ef2d17922b docs: adds rest-api examples for real this time 2023-05-19 14:49:06 +01:00
Dan Ribbens
b63dd40512 chore: release-it config update to include pre release on github 2023-05-18 12:00:49 -04:00
Quy Luong
fb82567f03 chore: fix and improve Vietnamese translation (#2651) 2023-05-18 11:57:22 -04:00
James Mikrut
b2c443e866 fix: #2647, slate not reinitializing after row change (#2653) 2023-05-18 11:56:25 -04:00
Jacob Fletcher
07d0324a6d Merge pull request #2677 from payloadcms/docs/node-version
docs: node version
2023-05-17 17:13:47 -04:00
Jacob Fletcher
c1e92ad27d fix: modal overflow caused by unused button tooltips (#2676) 2023-05-17 17:13:04 -04:00
Jacob Fletcher
28e481c2e2 Merge pull request #2656 from payloadcms/feat/peer-dep-conflicts
fix: peer dependencies
2023-05-17 17:12:39 -04:00
Jacob Fletcher
1ceea645b6 chore: replaces instances of the text Mongo with MongoDB 2023-05-17 16:35:55 -04:00
Jacob Fletcher
578e5e7e58 docs: updates node version requirement to v14 in installation docs 2023-05-17 16:29:03 -04:00
Jacob Fletcher
463d00732f chore: removes unused peer dependencies 2023-05-17 15:32:23 -04:00
Jacob Fletcher
698a8abe6e chore: fixes failing e2e test when searching within a relationship field 2023-05-17 12:25:24 -04:00
Roody
776877291f chore: Spelling and Grammar Fixes in German Translations (#2667) 2023-05-17 11:04:11 -04:00
Jacob Fletcher
1c25d965ac fix: react-select styles 2023-05-16 12:03:58 -04:00
Jarrod Flesch
648c38414e fix: disabled select fields 2023-05-15 16:49:40 -04:00
PatrikKozak
02b972e1ed fix: corrects sendEmail error logger (#2663) 2023-05-15 16:11:33 -04:00
Jacob Fletcher
dd38a08746 chore: bumps @trbl/eslint-config to v3.0.1 2023-05-12 17:50:29 -04:00
Jacob Fletcher
315b0059da chore: migrates to react-select v5.7.3 2023-05-12 17:50:24 -04:00
James
4d3ea70d2b chore: resolves all peer dep conflicts besides react-select 2023-05-12 11:50:05 -04:00
50 changed files with 3759 additions and 2690 deletions

2
.gitignore vendored
View File

@@ -164,7 +164,7 @@ GitHub.sublime-settings
# CMake
cmake-build-debug/
# Mongo Explorer plugin:
# MongoDB Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:

View File

@@ -7,7 +7,7 @@
"tag": false
},
"github": {
"release": false
"release": true
},
"npm": {
"skipChecks": true,

View File

@@ -589,7 +589,7 @@ If not already defined, add the following to your `compilerOptions`:
#### ✋ Versions may need to be migrated
This release includes a substantial simplification / optimization of how Versions work within Payload. They are now significantly more performant and easier to understand behind-the-scenes. We've removed ~600 lines of code and have ensured that Payload can be compatible with all flavors of Mongo - including versions earlier than 4.0, Azure Cosmos MongoDB, AWS' DocumentDB and more.
This release includes a substantial simplification / optimization of how Versions work within Payload. They are now significantly more performant and easier to understand behind-the-scenes. We've removed ~600 lines of code and have ensured that Payload can be compatible with all flavors of MongoDB - including versions earlier than 4.0, Azure Cosmos MongoDB, AWS' DocumentDB and more.
But, some of your draft-enabled documents may need to be migrated.

View File

@@ -67,7 +67,7 @@ You can customize the way that the Admin panel behaves on a collection-by-collec
| Option | Description |
|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `group` | Text used as a label for grouping collection links together in the navigation. |
| `group` | Text used as a label for grouping collection and global links together in the navigation. |
| `hidden` | Set to true or a function, called with the current user, returning true to exclude this collection from navigation and admin routing. |
| `hooks` | Admin-specific hooks for this collection. [More](#admin-hooks) |
| `useAsTitle` | Specify a top-level field to use for a document title throughout the Admin panel. If no field is defined, the ID of the document is used as the title. |

View File

@@ -67,6 +67,7 @@ You can customize the way that the Admin panel behaves on a Global-by-Global bas
| Option | Description |
|--------------|-----------------------------------------------------------------------------------------------------------------------------------|
| `group` | Text used as a label for grouping collection and global links together in the navigation. |
| `hidden` | Set to true or a function, called with the current user, returning true to exclude this global from navigation and admin routing. |
| `components` | Swap in your own React components to be used within this Global. [More](/docs/admin/components#globals) |
| `preview` | Function to generate a preview URL within the Admin panel for this global that can point to your app. [More](#preview). |

View File

@@ -143,7 +143,7 @@ payload.init({
[06:37:21] INFO (payload): Starting Payload...
[06:37:22] INFO (payload): Payload Demo Initialized
[06:37:22] INFO (payload): listening on 3000...
[06:37:22] INFO (payload): Connected to Mongo server successfully!
[06:37:22] INFO (payload): Connected to MongoDB server successfully!
[06:37:23] INFO (payload): E-mail configured with mock configuration
[06:37:23] INFO (payload): Log into mock email provider at https://ethereal.email
[06:37:23] INFO (payload): Mock email account username: hhav5jw7doo4euev@ethereal.email

View File

@@ -146,7 +146,7 @@ The shape of the data to save for a document with the field configured this way
```json
{
// Mongo ObjectID of the related user
// MongoDB ObjectID of the related user
"owner": "6031ac9e1289176380734024"
}
```

View File

@@ -11,8 +11,8 @@ keywords: documentation, getting started, guide, Content Management System, cms,
Payload requires the following software:
- Yarn or NPM
- NodeJS version 10+
- A Mongo Database
- Node.js version 14+
- A MongoDB Database
<Banner type="warning">
Before proceeding any further, please ensure that you have the above
@@ -110,15 +110,15 @@ Payload uses this secret key to generate secure user tokens (JWT). Behind the sc
##### `mongoURL`
**Required**. This is a fully qualified MongoDB connection string that points to your Mongo database. If you don't have Mongo installed locally, you can [follow these steps for Mac OSX](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-os-x/) and [these steps](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-windows/) for Windows 10. If you want to use a local database and you know you have MongoDB installed locally, a typical connection string will look like this:
**Required**. This is a fully qualified MongoDB connection string that points to your MongoDB database. If you don't have MongoDB installed locally, you can [follow these steps for Mac OSX](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-os-x/) and [these steps](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-windows/) for Windows 10. If you want to use a local database and you know you have MongoDB installed locally, a typical connection string will look like this:
`mongodb://localhost/payload`
In contrast to running Mongo locally, a popular option is to sign up for a free [MongoDB Atlas account](https://www.mongodb.com/cloud/atlas), which is a fully hosted and cloud-based installation of Mongo that you don't need to ever worry about.
In contrast to running MongoDB locally, a popular option is to sign up for a free [MongoDB Atlas account](https://www.mongodb.com/cloud/atlas), which is a fully hosted and cloud-based installation of MongoDB that you don't need to ever worry about.
##### `mongoOptions`
Customize Mongo connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose.
Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose.
##### `email`

View File

@@ -19,7 +19,7 @@ keywords: documentation, getting started, guide, Content Management System, cms,
Out of the box, Payload gives you a lot of the things that you often need when developing a new website, web app, or native app:
- A Mongo database to store your data
- A MongoDB database to store your data
- A way to store, retrieve, and manipulate data of any shape via full REST and GraphQL APIs
- Authentication—complete with commonly required functionality like registration, email verification, login, & password reset
- Deep access control to your data, based on document or field-level functions

View File

@@ -80,8 +80,9 @@ Example
async (obj, args, context, info) => { }
```
**`obj`** The previous object. Not very often used and usually discarded.
**`obj`**
The previous object. Not very often used and usually discarded.
**`args`**

View File

@@ -7,7 +7,8 @@ keywords: rest, api, documentation, Content Management System, cms, headless, ja
---
<Banner>
A fully functional REST API is automatically generated from your Collection and Global configs.
A fully functional REST API is automatically generated from your Collection
and Global configs.
</Banner>
All Payload API routes are mounted prefixed to your config's `routes.api` URL segment (default: `/api`).
@@ -26,58 +27,518 @@ Note: Collection slugs must be formatted in kebab-case
**All CRUD operations are exposed as follows:**
| Method | Path | Description |
|----------|-------------------------------|--------------------------------------------------|
| `GET` | `/api/{collection-slug}` | Find paginated documents |
| `GET` | `/api/{collection-slug}/:id` | Find a specific document by ID |
| `POST` | `/api/{collection-slug}` | Create a new document |
| `PATCH` | `/api/{collection-slug}` | Update all documents matching the `where` query |
| `PATCH` | `/api/{collection-slug}/:id` | Update a document by ID |
| `DELETE` | `/api/{collection-slug}` | Delete all documents matching the `where` query |
| `DELETE` | `/api/{collection-slug}/:id` | Delete an existing document by ID |
<RestExamples
data={[
{
operation: "Find",
method: "GET",
path: "/api/{collection-slug}",
description: "Find paginated documents",
example: {
slug: "getCollection",
req: true,
res: {
paginated: true,
data: {
id: "644a5c24cc1383022535fc7c",
title: "Home",
content: "REST API examples",
slug: "home",
createdAt: "2023-04-27T11:27:32.419Z",
updatedAt: "2023-04-27T11:27:32.419Z",
},
},
drawerContent: (
<>
<h6>Additional <code>find</code> query parameters</h6>
The <code>find</code> endpoint supports the following additional query parameters:
<ul>
<li>
<a href="/docs/queries/overview#sort">sort</a> - sort by field
</li>
<li>
<a href="/docs/queries/overview">where</a> - pass a where query to constrain returned
documents
</li>
<li>
<a href="/docs/queries/pagination#pagination-controls">limit</a> - limit the returned
documents to a certain number
</li>
<li>
<a href="/docs/queries/pagination#pagination-controls">page</a> - get a specific page of
documents
</li>
</ul>
</>
),
},
},
{
operation: "Find By ID",
method: "GET",
path: "/api/{collection-slug}/{id}",
description: "Find a specific document by ID",
example: {
slug: "findByID",
req: true,
res: {
id: "644a5c24cc1383022535fc7c",
title: "Home",
content: "REST API examples",
slug: "home",
createdAt: "2023-04-27T11:27:32.419Z",
updatedAt: "2023-04-27T11:27:32.419Z",
},
},
},
{
operation: "Create",
method: "POST",
path: "/api/{collection-slug}",
description: "Create a new document",
example: {
slug: "createDocument",
req: {
headers: true,
body: {
title: "New page",
content: "Here is some content",
},
},
res: {
message: "Page successfully created.",
doc: {
id: "644ba34c86359864f9535932",
title: "New page",
content: "Here is some content",
slug: "new-page",
createdAt: "2023-04-28T10:43:24.466Z",
updatedAt: "2023-04-28T10:43:24.466Z",
},
},
},
},
{
operation: "Update",
method: "PATCH",
path: "/api/{collection-slug}",
description: "Update all documents matching the where query",
example: {
slug: "updateDocument",
req: {
query: true,
headers: true,
body: {
title: "I have been updated!",
},
},
res: {
docs: [
{
id: "644ba34c86359864f9535932",
title: "I have been updated!",
content: "Here is some content",
slug: "new-page",
createdAt: "2023-04-28T10:43:24.466Z",
updatedAt: "2023-04-28T10:45:23.724Z",
},
],
errors: [],
},
},
},
{
operation: "Update By ID",
method: "PATCH",
path: "/api/{collection-slug}/{id}",
description: "Update a document by ID",
example: {
slug: "updateDocumentByID",
req: {
headers: true,
body: {
title: "I have been updated by ID!",
categories: "example-uuid",
tags: {
relationTo: "location",
value: "another-example-uuid",
},
},
},
res: {
message: "Updated successfully.",
doc: {
id: "644a5c24cc1383022535fc7c",
title: "I have been updated by ID!",
content: "REST API examples",
categories: {
id: "example-uuid",
name: "Test Category",
},
tags: [
{
relationTo: "location",
value: {
id: "another-example-uuid",
name: "Test Location",
},
},
],
slug: "home",
createdAt: "2023-04-27T11:27:32.419Z",
updatedAt: "2023-04-28T10:47:59.259Z",
},
},
},
},
{
operation: "Delete",
method: "DELETE",
path: "/api/{collection-slug}",
description: "Delete all documents matching the where query",
example: {
slug: "deleteDocuments",
req: {
query: true,
headers: true,
},
res: {
docs: [
{
id: "644ba4cf86359864f953594b",
title: "New page",
content: "Here is some content",
slug: "new-page",
createdAt: "2023-04-28T10:49:51.359Z",
updatedAt: "2023-04-28T10:49:51.359Z",
},
],
errors: [],
},
},
},
{
operation: "Delete by ID",
method: "DELETE",
path: "/api/{collection-slug}/{id}",
description: "Delete an existing document by ID",
example: {
slug: "deleteByID",
req: {
headers: true,
},
res: {
id: "644ba51786359864f9535954",
title: "New page",
content: "Here is some content",
slug: "new-page",
createdAt: "2023-04-28T10:51:03.028Z",
updatedAt: "2023-04-28T10:51:03.028Z",
},
},
},
##### Additional `find` query parameters
The `find` endpoint supports the following additional query parameters:
- [sort](/docs/queries/overview#sort) - sort by field
- [where](/docs/queries/overview) - pass a `where` query to constrain returned documents
- [limit](/docs/queries/pagination#pagination-controls) - limit the returned documents to a certain number
- [page](/docs/queries/pagination#pagination-controls) - get a specific page of documents
]}
/>
## Auth Operations
Auth enabled collections are also given the following endpoints:
| Method | Path | Description |
| -------- | --------------------------- | ----------- |
| `POST` | `/api/{collection-slug}/verify/:token` | [Email verification](/docs/authentication/operations#verify-by-email), if enabled. |
| `POST` | `/api/{collection-slug}/unlock` | [Unlock a user's account](/docs/authentication/operations#unlock), if enabled. |
| `POST` | `/api/{collection-slug}/login` | [Logs in](/docs/authentication/operations#login) a user with email / password. |
| `POST` | `/api/{collection-slug}/logout` | [Logs out](/docs/authentication/operations#logout) a user. |
| `POST` | `/api/{collection-slug}/refresh-token` | [Refreshes a token](/docs/authentication/operations#refresh) that has not yet expired. |
| `GET` | `/api/{collection-slug}/me` | [Returns the currently logged in user with token](/docs/authentication/operations#me). |
| `POST` | `/api/{collection-slug}/forgot-password` | [Password reset workflow](/docs/authentication/operations#forgot-password) entry point. |
| `POST` | `/api/{collection-slug}/reset-password` | [To reset the user's password](/docs/authentication/operations#reset-password). |
<RestExamples
data={[
{
operation: "Login",
method: "POST",
path: "/api/{user-collection}/login",
description: "Logs in a user with email / password",
example: {
slug: "login",
req: {
headers: true,
body: {
email: "dev@payloadcms.com",
password: "password",
},
},
res: {
message: "Auth Passed",
user: {
id: "644b8453cd20c7857da5a9b0",
email: "dev@payloadcms.com",
_verified: true,
createdAt: "2023-04-28T08:31:15.788Z",
updatedAt: "2023-04-28T11:11:03.716Z",
},
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
exp: 1682689147,
},
},
},
{
operation: "Logout",
method: "POST",
path: "/api/{user-collection}/logout",
description: "Logs out a user",
example: {
slug: "logout",
req: {
headers: true,
},
res: {
message: "You have been logged out successfully.",
},
},
},
{
operation: "Unlock",
method: "POST",
path: "/api/{user-collection}/unlock",
description: "Unlock a user account",
example: {
slug: "unlockCollection",
req: {
headers: true,
body: {
email: "dev@payloadcms.com",
},
},
res: {
message: "Success",
},
},
},
{
operation: "Refresh",
method: "POST",
path: "/api/{user-collection}/refresh-token",
description: "Refreshes a token that has not yet expired",
example: {
slug: "refreshToken",
req: {
headers: true,
},
res: {
message: "Token refresh successful",
refreshedToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
exp: 1682689362,
user: {
email: "dev@payloadcms.com",
id: "644b8453cd20c7857da5a9b0",
collection: "users",
},
},
},
},
{
operation: "Verify User",
method: "POST",
path: "/api/{user-collection}/verify/{token}",
description: "User verification",
example: {
slug: "verifyUser",
req: {
prop: "token: string, user-collection: string",
headers: true,
},
res: {
message: "Email verified successfully.",
},
},
},
{
operation: "Current User",
method: "GET",
path: "/api/{user-collection}/me",
description: "Returns the currently logged in user with token",
example: {
slug: "currentUser",
req: {
headers: true,
},
res: {
user: {
id: "644b8453cd20c7857da5a9b0",
email: "dev@payloadcms.com",
_verified: true,
createdAt: "2023-04-28T08:31:15.788Z",
updatedAt: "2023-04-28T11:45:23.926Z",
_strategy: "local-jwt",
},
collection: "users",
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
exp: 1682689523,
},
},
},
{
operation: "Forgot Password",
method: "POST",
path: "/api/{user-collection}/forgot-password",
description: "Password reset workflow entry point",
example: {
slug: "forgotPassword",
req: {
headers: true,
body: {
email: "dev@payloadcms.com",
},
},
res: {
message: "Success",
},
},
},
{
operation: "Reset Password",
method: "POST",
path: "/api/{user-collection}/reset-password",
description: "Reset user password",
example: {
slug: "resetPassword",
req: {
headers: true,
body: {
token: "7eac3830ffcfc7f9f66c00315dabeb11575dba91",
password: "newPassword",
},
},
res: {
message: "Password reset successfully.",
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
user: {
id: "644baa473ea9538765cc30fc",
email: "dev@payloadcms.com",
_verified: true,
createdAt: "2023-04-28T11:13:11.569Z",
updatedAt: "2023-04-28T11:49:23.860Z",
},
},
},
},
]}
/>
## Globals
Globals cannot be created or deleted, so there are only two REST endpoints opened:
| Method | Path | Description |
| -------- | --------------------------- | ----------------------- |
| `GET` | `/api/globals/{globalSlug}` | Get a global by slug |
| `POST` | `/api/globals/{globalSlug}` | Update a global by slug |
<RestExamples
data={[
{
operation: "Get Global",
method: "GET",
path: "/api/globals/{global-slug}",
description: "Get a global by slug",
example: {
slug: "getGlobal",
req: {
headers: true,
},
res: {
announcement: "Here is an announcement!",
globalType: "announcement",
createdAt: "2023-04-28T08:53:56.066Z",
updatedAt: "2023-04-28T08:53:56.066Z",
id: "644b89a496c64a833fe579c9",
},
},
},
{
operation: "Update Global",
method: "POST",
path: "/api/globals/{global-slug}",
description: "Update a global by slug",
example: {
slug: "updateGlobal",
req: {
headers: true,
body: {
announcement: "Paging Doctor Scrunt",
},
},
res: {
announcement: "Paging Doctor Scrunt",
globalType: "announcement",
createdAt: "2023-04-28T08:53:56.066Z",
updatedAt: "2023-04-28T08:53:56.066Z",
id: "644b89a496c64a833fe579c9",
},
},
},
]}
/>
## Preferences
In addition to the dynamically generated endpoints above Payload also has REST endpoints to manage the admin user [preferences](/docs/admin/overview#preferences) for data specific to the authenticated user.
| Method | Path | Description |
| -------- | --------------------------- | ----------------------- |
| `GET` | `/api/_preferences/{key}` | Get a preference by key |
| `POST` | `/api/_preferences/{key}` | Create or update by key |
| `DELETE` | `/api/_preferences/{key}` | Delete a user preference by key |
<RestExamples
data={[
{
operation: "Get Preference",
method: "GET",
path: "/api/_preferences/{key}",
description: "Get a preference by key",
example: {
slug: "getPreference",
req: {
headers: true,
},
res: {
_id: "644bb7a8307b3d363c6edf2c",
key: "region",
user: "644b8453cd20c7857da5a9b0",
userCollection: "users",
__v: 0,
createdAt: "2023-04-28T12:10:16.689Z",
updatedAt: "2023-04-28T12:10:16.689Z",
value: "Europe/London",
},
},
},
{
operation: "Create Preference",
method: "POST",
path: "/api/_preferences/{key}",
description: "Create or update a preference by key",
example: {
slug: "createPreference",
req: {
headers: true,
body: {
value: "Europe/London",
},
},
res: {
message: "Updated successfully.",
doc: {
user: "644b8453cd20c7857da5a9b0",
key: "region",
userCollection: "users",
value: "Europe/London",
},
},
},
},
{
operation: "Delete Preference",
method: "DELETE",
path: "/api/_preferences/{key}",
description: "Delete a preference by key",
example: {
slug: "deletePreference",
req: {
headers: true,
},
res: {
message: "deletedSuccessfully",
},
},
},
]}
/>
## Custom Endpoints
@@ -85,43 +546,48 @@ Additional REST API endpoints can be added to your application by providing an a
Each endpoint object needs to have:
| Property | Description |
| ---------- | ----------------------------------------- |
| **`path`** | A string for the endpoint route after the collection or globals slug |
| **`method`** | The lowercase HTTP verb to use: 'get', 'head', 'post', 'put', 'delete', 'connect' or 'options' |
| **`handler`** | A function or array of functions to be called with **req**, **res** and **next** arguments. [Express](https://expressjs.com/en/guide/routing.html#route-handlers) |
| **`root`** | When `true`, defines the endpoint on the root Express app, bypassing Payload handlers and the `routes.api` subpath. Note: this only applies to top-level endpoints of your Payload config, endpoints defined on `collections` or `globals` cannot be root. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| Property | Description |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`path`** | A string for the endpoint route after the collection or globals slug |
| **`method`** | The lowercase HTTP verb to use: 'get', 'head', 'post', 'put', 'delete', 'connect' or 'options' |
| **`handler`** | A function or array of functions to be called with **req**, **res** and **next** arguments. [Express](https://expressjs.com/en/guide/routing.html#route-handlers) |
| **`root`** | When `true`, defines the endpoint on the root Express app, bypassing Payload handlers and the `routes.api` subpath. Note: this only applies to top-level endpoints of your Payload config, endpoints defined on `collections` or `globals` cannot be root. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
Example:
```ts
import { CollectionConfig } from 'payload/types';
import { CollectionConfig } from "payload/types";
// a collection of 'orders' with an additional route for tracking details, reachable at /api/orders/:id/tracking
export const Orders: CollectionConfig = {
slug: 'orders',
fields: [ /* ... */ ],
slug: "orders",
fields: [
/* ... */
],
// highlight-start
endpoints: [
{
path: '/:id/tracking',
method: 'get',
path: "/:id/tracking",
method: "get",
handler: async (req, res, next) => {
const tracking = await getTrackingInfo(req.params.id);
if (tracking) {
res.status(200).send({ tracking });
} else {
res.status(404).send({ error: 'not found' });
res.status(404).send({ error: "not found" });
}
}
}
},
},
],
// highlight-end
}
};
```
<Banner>
<strong>Note:</strong><br/>
**req** will have the **payload** object and can be used inside your endpoint handlers for making calls like req.payload.find() that will make use of access control and hooks.
<strong>Note:</strong>
<br />
**req** will have the **payload** object and can be used inside your endpoint
handlers for making calls like req.payload.find() that will make use of access
control and hooks.
</Banner>

View File

@@ -87,7 +87,7 @@
"@faceless-ui/modal": "^2.0.1",
"@faceless-ui/scroll-info": "^1.3.0",
"@faceless-ui/window-info": "^2.1.1",
"@monaco-editor/react": "^4.4.6",
"@monaco-editor/react": "^4.5.1",
"@swc/core": "^1.3.26",
"@swc/register": "^0.1.10",
"@types/sharp": "^0.31.1",
@@ -97,7 +97,7 @@
"conf": "^10.2.0",
"connect-history-api-fallback": "^1.6.0",
"css-loader": "^5.2.7",
"css-minimizer-webpack-plugin": "^3.4.1",
"css-minimizer-webpack-plugin": "^5.0.0",
"dataloader": "^2.1.0",
"date-fns": "^2.29.3",
"deep-equal": "^2.2.0",
@@ -136,6 +136,7 @@
"mini-css-extract-plugin": "1.6.2",
"minimist": "^1.2.7",
"mkdirp": "^1.0.4",
"monaco-editor": "^0.38.0",
"mongoose": "6.5.0",
"mongoose-aggregate-paginate-v2": "^1.0.6",
"mongoose-paginate-v2": "^1.6.1",
@@ -161,17 +162,18 @@
"react": "^18.2.0",
"react-animate-height": "^2.1.2",
"react-datepicker": "^4.10.0",
"react-diff-viewer": "^3.1.1",
"react-diff-viewer-continued": "^3.2.6",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
"react-i18next": "^11.18.6",
"react-router-dom": "^5.3.4",
"react-router-navigation-prompt": "^1.9.6",
"react-select": "^3.2.0",
"react-select": "^5.7.3",
"react-toastify": "^8.2.0",
"sanitize-filename": "^1.6.3",
"sass": "^1.57.1",
"sass-loader": "^12.6.0",
"scheduler": "^0.23.0",
"sharp": "^0.31.3",
"slate": "^0.91.4",
"slate-history": "^0.86.0",
@@ -197,11 +199,10 @@
"@swc/jest": "^0.2.24",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@trbl/eslint-config": "^1.2.4",
"@trbl/eslint-config": "^3.0.1",
"@types/asap": "^2.0.0",
"@types/body-parser": "^1.19.2",
"@types/compression": "^1.7.2",
"@types/conf": "^3.0.0",
"@types/connect-history-api-fallback": "^1.3.5",
"@types/eslint": "^7.29.0",
"@types/express": "^4.17.15",
@@ -224,7 +225,7 @@
"@types/minimist": "^1.2.2",
"@types/mkdirp": "^1.0.2",
"@types/mongoose-aggregate-paginate-v2": "^1.0.5",
"@types/mongoose-paginate-v2": "^1.6.4",
"@types/node": "^20.1.3",
"@types/node-fetch": "^2.6.2",
"@types/nodemailer": "^6.4.7",
"@types/nodemon": "^1.19.2",
@@ -233,8 +234,6 @@
"@types/passport-anonymous": "^1.0.3",
"@types/passport-jwt": "^3.0.8",
"@types/passport-local": "^1.0.35",
"@types/pino": "^6.3.12",
"@types/pino-std-serializers": "^4.0.0",
"@types/pluralize": "^0.0.29",
"@types/prismjs": "^1.26.0",
"@types/probe-image-size": "^7.2.0",
@@ -246,13 +245,10 @@
"@types/react-dom": "^18.0.10",
"@types/react-helmet": "^6.1.6",
"@types/react-router-dom": "^5.3.3",
"@types/react-select": "^3.1.2",
"@types/sass": "^1.43.1",
"@types/shelljs": "^0.8.11",
"@types/testing-library__jest-dom": "^5.14.5",
"@types/uuid": "^8.3.4",
"@types/webpack-bundle-analyzer": "^4.6.0",
"@types/webpack-dev-middleware": "^5.3.0",
"@types/webpack-env": "^1.18.0",
"@types/webpack-hot-middleware": "2.25.6",
"@typescript-eslint/eslint-plugin": "^5.59.1",
@@ -275,7 +271,7 @@
"graphql-request": "^3.7.0",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.3.1",
"mongodb-memory-server": "^7.6.3",
"mongodb-memory-server": "^8.12.2",
"node-fetch": "2",
"nodemon": "^2.0.20",
"object.assign": "^4.1.0",

View File

@@ -28,12 +28,14 @@ const ButtonContents = ({ children, icon, tooltip, showTooltip }) => {
return (
<Fragment>
<Tooltip
className={`${baseClass}__tooltip`}
show={showTooltip}
>
{tooltip}
</Tooltip>
{tooltip && (
<Tooltip
className={`${baseClass}__tooltip`}
show={showTooltip}
>
{tooltip}
</Tooltip>
)}
<span className={`${baseClass}__content`}>
{children && (
<span className={`${baseClass}__label`}>

View File

@@ -16,6 +16,12 @@
}
.btn {
margin-right: $baseline;
margin: 0;
}
&__actions {
display: flex;
flex-wrap: wrap;
gap: $baseline;
}
}

View File

@@ -109,20 +109,22 @@ const DeleteDocument: React.FC<Props> = (props) => {
</strong>
</Trans>
</p>
<Button
id="confirm-cancel"
buttonStyle="secondary"
type="button"
onClick={deleting ? undefined : () => toggleModal(modalSlug)}
>
{t('cancel')}
</Button>
<Button
onClick={deleting ? undefined : handleDelete}
id="confirm-delete"
>
{deleting ? t('deleting') : t('confirm')}
</Button>
<div className={`${baseClass}__actions`}>
<Button
id="confirm-cancel"
buttonStyle="secondary"
type="button"
onClick={deleting ? undefined : () => toggleModal(modalSlug)}
>
{t('cancel')}
</Button>
<Button
onClick={deleting ? undefined : handleDelete}
id="confirm-delete"
>
{deleting ? t('deleting') : t('confirm')}
</Button>
</div>
</MinimalTemplate>
</Modal>
</React.Fragment>

View File

@@ -1,12 +1,12 @@
import React from 'react';
import { IndicatorProps } from 'react-select';
import { ClearIndicatorProps } from 'react-select';
import X from '../../../icons/X';
import { Option as OptionType } from '../types';
import './index.scss';
const baseClass = 'clear-indicator';
export const ClearIndicator: React.FC<IndicatorProps<OptionType, true>> = (props) => {
export const ClearIndicator: React.FC<ClearIndicatorProps<OptionType, true>> = (props) => {
const {
innerProps: { ref, ...restInnerProps },
} = props;

View File

@@ -6,12 +6,10 @@ export const Control: React.FC<ControlProps<Option, any>> = (props) => {
const {
children,
innerProps,
selectProps: {
selectProps: {
disableMouseDown,
disableKeyDown,
},
},
customProps: {
disableMouseDown,
disableKeyDown,
} = {},
} = props;
return (

View File

@@ -9,7 +9,6 @@ import { Option as OptionType } from '../types';
import './index.scss';
const baseClass = 'multi-value';
export const MultiValue: React.FC<MultiValueProps<OptionType>> = (props) => {
const {
className,
@@ -18,12 +17,9 @@ export const MultiValue: React.FC<MultiValueProps<OptionType>> = (props) => {
data: {
value,
},
selectProps: {
selectProps,
selectProps: {
disableMouseDown,
},
},
customProps: {
disableMouseDown,
} = {},
} = props;
const {
@@ -49,6 +45,8 @@ export const MultiValue: React.FC<MultiValueProps<OptionType>> = (props) => {
className={classes}
innerProps={{
...innerProps,
...attributes,
...listeners,
ref: setNodeRef,
onMouseDown: (e) => {
if (!disableMouseDown) {
@@ -61,14 +59,6 @@ export const MultiValue: React.FC<MultiValueProps<OptionType>> = (props) => {
transform,
},
}}
selectProps={{
...selectProps,
// pass the draggable props through to the label so it alone acts as the draggable handle
draggableProps: {
...attributes,
...listeners,
},
}}
/>
);
};

View File

@@ -7,9 +7,9 @@ const baseClass = 'multi-value-label';
export const MultiValueLabel: React.FC<MultiValueProps<Option>> = (props) => {
const {
selectProps: {
customProps: {
draggableProps,
},
} = {},
} = props;
return (

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { MultiValueRemoveProps } from 'react-select/src/components/MultiValue';
import { MultiValueRemoveProps } from 'react-select';
import X from '../../../icons/X';
import Tooltip from '../../Tooltip';
import { Option as OptionType } from '../types';
@@ -8,23 +8,33 @@ import './index.scss';
const baseClass = 'multi-value-remove';
export const MultiValueRemove: React.FC<MultiValueRemoveProps<OptionType>> = (props) => {
export const MultiValueRemove: React.FC<MultiValueRemoveProps<OptionType> & {
innerProps: JSX.IntrinsicElements['button']
}> = (props) => {
const {
innerProps,
innerProps: {
className,
onClick,
onTouchEnd,
},
} = props;
const [showTooltip, setShowTooltip] = React.useState(false);
const { t } = useTranslation('general');
return (
<button
{...innerProps}
type="button"
className={baseClass}
className={[
baseClass,
className,
].filter(Boolean).join(' ')}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
onTouchEnd={onTouchEnd}
onClick={(e) => {
setShowTooltip(false);
innerProps.onClick(e);
onClick(e);
}}
aria-label={t('remove')}
>

View File

@@ -1,7 +0,0 @@
@import '../../../../scss/styles.scss';
.react-select--single-value {
.rs__single-value {
color: currentColor;
}
}

View File

@@ -1,20 +1,24 @@
import React from 'react';
import { components as SelectComponents, SingleValueProps } from 'react-select';
import { Option } from '../types';
import './index.scss';
const baseClass = 'react-select--single-value';
export const SingleValue: React.FC<SingleValueProps<Option>> = (props) => {
const {
children,
className,
} = props;
return (
<div className={baseClass}>
<SelectComponents.SingleValue {...props}>
{children}
</SelectComponents.SingleValue>
</div>
<SelectComponents.SingleValue
{...props}
className={[
baseClass,
className,
].filter(Boolean).join(' ')}
>
{children}
</SelectComponents.SingleValue>
);
};

View File

@@ -12,6 +12,7 @@
margin: 0;
padding-top: 0;
padding-bottom: 0;
color: currentColor;
}
&--is-multi {

View File

@@ -8,12 +8,12 @@ const baseClass = 'value-container';
export const ValueContainer: React.FC<ValueContainerProps<Option, any>> = (props) => {
const {
selectProps,
customProps,
} = props;
return (
<div
ref={selectProps.selectProps.droppableRef}
ref={customProps?.droppableRef}
className={baseClass}
>
<SelectComponents.ValueContainer {...props} />

View File

@@ -22,13 +22,13 @@
display: none;
}
.rs__input {
.rs__input-container {
color: var(--theme-elevation-1000);
}
input {
font-family: var(--font-body);
width: 10px;
}
.rs__input {
font-family: var(--font-body);
width: 10px;
}
.rs__menu {

View File

@@ -2,7 +2,7 @@ import React from 'react';
import Select from 'react-select';
import { useTranslation } from 'react-i18next';
import { arrayMove } from '@dnd-kit/sortable';
import { Props } from './types';
import { Props as ReactSelectAdapterProps } from './types';
import Chevron from '../../icons/Chevron';
import { getTranslation } from '../../../../utilities/getTranslation';
import { SingleValue } from './SingleValue';
@@ -16,7 +16,7 @@ import DraggableSortable from '../DraggableSortable';
import './index.scss';
const SelectAdapter: React.FC<Props> = (props) => {
const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
const { t, i18n } = useTranslation();
const {
@@ -33,7 +33,6 @@ const SelectAdapter: React.FC<Props> = (props) => {
isLoading,
onMenuOpen,
components,
selectProps,
} = props;
const classes = [
@@ -50,7 +49,7 @@ const SelectAdapter: React.FC<Props> = (props) => {
{...props}
value={value}
onChange={onChange}
disabled={disabled ? 'disabled' : undefined}
isDisabled={disabled}
className={classes}
classNamePrefix="rs"
options={options}
@@ -59,9 +58,6 @@ const SelectAdapter: React.FC<Props> = (props) => {
filterOption={filterOption}
onMenuOpen={onMenuOpen}
menuPlacement="auto"
selectProps={{
...selectProps,
}}
components={{
ValueContainer,
SingleValue,
@@ -77,7 +73,7 @@ const SelectAdapter: React.FC<Props> = (props) => {
);
};
const SortableSelect: React.FC<Props> = (props) => {
const SortableSelect: React.FC<ReactSelectAdapterProps> = (props) => {
const {
onChange,
value,
@@ -103,7 +99,7 @@ const SortableSelect: React.FC<Props> = (props) => {
);
};
const ReactSelect: React.FC<Props> = (props) => {
const ReactSelect: React.FC<ReactSelectAdapterProps> = (props) => {
const {
isMulti,
isSortable,

View File

@@ -1,4 +1,34 @@
import { Ref } from 'react';
import { CommonProps, GroupBase, Props as ReactSelectStateManagerProps } from 'react-select';
import { DocumentDrawerProps } from '../DocumentDrawer/types';
type CustomSelectProps = {
disableMouseDown?: boolean
disableKeyDown?: boolean
droppableRef?: React.RefObject<HTMLDivElement>
setDrawerIsOpen?: (isOpen: boolean) => void
onSave?: DocumentDrawerProps['onSave']
draggableProps?: any
}
// augment the types for the `Select` component from `react-select`
// this is to include the `selectProps` prop at the top-level `Select` component
declare module 'react-select/dist/declarations/src/Select' {
export interface Props<
Option,
IsMulti extends boolean,
Group extends GroupBase<Option>
> {
customProps?: CustomSelectProps
}
}
// augment the types for the `CommonPropsAndClassName` from `react-select`
// this will include the `selectProps` prop to every `react-select` component automatically
declare module 'react-select/dist/declarations/src' {
export interface CommonPropsAndClassName<Option, IsMulti extends boolean, Group extends GroupBase<Option>> extends CommonProps<Option, IsMulti, Group> {
customProps?: ReactSelectStateManagerProps<Option, IsMulti, Group> & CustomSelectProps
}
}
export type Option = {
[key: string]: unknown
@@ -22,7 +52,6 @@ export type Props = {
isLoading?: boolean
isOptionSelected?: any
isSortable?: boolean,
isDisabled?: boolean
onInputChange?: (val: string) => void
onMenuScrollToBottom?: () => void
placeholder?: string
@@ -35,9 +64,5 @@ export type Props = {
components?: {
[key: string]: React.FC<any>
}
selectProps?: {
disableMouseDown?: boolean
disableKeyDown?: boolean
[key: string]: unknown
}
selectProps?: CustomSelectProps
}

View File

@@ -381,7 +381,7 @@ const Relationship: React.FC<Props> = (props) => {
{!errorLoading && (
<div className={`${baseClass}__wrap`}>
<ReactSelect
isDisabled={readOnly}
disabled={readOnly || formProcessing}
onInputChange={(newSearch) => handleInputChange(newSearch, value)}
onChange={!readOnly ? (selected) => {
if (selected === null) {
@@ -417,7 +417,6 @@ const Relationship: React.FC<Props> = (props) => {
}}
value={valueToRender ?? null}
showError={showError}
disabled={formProcessing}
options={options}
isMulti={hasMany}
isSortable={isSortable}

View File

@@ -17,11 +17,11 @@ export const MultiValueLabel: React.FC<MultiValueProps<Option>> = (props) => {
relationTo,
label,
},
selectProps: {
customProps: {
setDrawerIsOpen,
draggableProps,
onSave,
},
} = {},
} = props;
const { permissions } = useAuth();

View File

@@ -1,11 +1,8 @@
@import '../../../../../../scss/styles.scss';
.relationship--single-value {
display: flex;
align-items: center;
.rs__single-value {
color: currentColor;
.relationship--single-value {
&__label-text {
max-width: unset;
display: flex;
align-items: center;

View File

@@ -18,12 +18,10 @@ export const SingleValue: React.FC<SingleValueProps<Option>> = (props) => {
label,
},
children,
selectProps: {
selectProps: {
setDrawerIsOpen,
onSave,
},
},
customProps: {
setDrawerIsOpen,
onSave,
} = {},
} = props;
const [showTooltip, setShowTooltip] = useState(false);
@@ -41,9 +39,12 @@ export const SingleValue: React.FC<SingleValueProps<Option>> = (props) => {
}, [isDrawerOpen, setDrawerIsOpen]);
return (
<div className={baseClass}>
<SelectComponents.SingleValue
{...props}
className={baseClass}
>
<div className={`${baseClass}__label`}>
<SelectComponents.SingleValue {...props}>
<div className={`${baseClass}__label-text`}>
<div className={`${baseClass}__text`}>
{children}
</div>
@@ -67,11 +68,11 @@ export const SingleValue: React.FC<SingleValueProps<Option>> = (props) => {
</DocumentDrawerToggler>
</Fragment>
)}
</SelectComponents.SingleValue>
</div>
</div>
{relationTo && hasReadPermission && (
<DocumentDrawer onSave={onSave} />
)}
</div>
</SelectComponents.SingleValue>
);
};

View File

@@ -272,7 +272,7 @@ const RichText: React.FC<Props> = (props) => {
required={required}
/>
<Slate
key={JSON.stringify(initialValue)}
key={JSON.stringify({ initialValue, path })}
editor={editor}
value={valueToRender as any[]}
onChange={handleChange}

View File

@@ -99,7 +99,7 @@ const SelectInput: React.FC<SelectInputProps> = (props) => {
onChange={onChange}
value={valueToRender as Option}
showError={showError}
isDisabled={readOnly}
disabled={readOnly}
options={options.map((option) => ({ ...option, label: getTranslation(option.label, i18n) }))}
isMulti={hasMany}
isSortable={isSortable}

View File

@@ -12,6 +12,12 @@
}
.btn {
margin-right: base(1);
margin: 0;
}
&__actions {
display: flex;
flex-wrap: wrap;
gap: $baseline;
}
}

View File

@@ -20,8 +20,8 @@ const StayLoggedInModal: React.FC<Props> = (props) => {
const {
routes: { admin },
admin: {
logoutRoute
}
logoutRoute,
},
} = config;
const { toggleModal } = useModal();
const { t } = useTranslation('authentication');

View File

@@ -1,5 +1,5 @@
import React from 'react';
import ReactDiffViewer from 'react-diff-viewer';
import ReactDiffViewer from 'react-diff-viewer-continued';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../../../../../utilities/Config';
import { useLocale } from '../../../../../utilities/Locale';

View File

@@ -1,5 +1,5 @@
import React from 'react';
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer';
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued';
import { useTranslation } from 'react-i18next';
import type { i18n as Ii18n } from 'i18next';
import Label from '../../Label';

View File

@@ -1,5 +1,5 @@
import React from 'react';
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer';
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued';
import { useTranslation } from 'react-i18next';
import Label from '../../Label';
import { diffStyles } from '../styles';

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { DiffMethod } from 'react-diff-viewer';
import { DiffMethod } from 'react-diff-viewer-continued';
import { FieldPermissions } from '../../../../../../auth';
export type FieldComponents = Record<string, React.FC<Props>>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { DiffMethod } from 'react-diff-viewer';
import { DiffMethod } from 'react-diff-viewer-continued';
import { Props } from './types';
import { fieldAffectsData, fieldHasSubFields } from '../../../../../fields/config/types';
import Nested from './fields/Nested';

View File

@@ -79,9 +79,9 @@ export type GraphQLExtension = (
export type InitOptions = {
/** Express app for Payload to use */
express?: Express;
/** Mongo connection URL, starts with `mongo` */
/** MongoDB connection URL, starts with `mongo` */
mongoURL: string | false;
/** Extra configuration options that will be passed to Mongo */
/** Extra configuration options that will be passed to MongoDB */
mongoOptions?: ConnectOptions & {
/** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */
useFacet?: boolean
@@ -124,7 +124,7 @@ export type InitOptions = {
* and then sent to the client allowing the dashboard to show accessible data and actions.
*
* If the result is `true`, the user has access.
* If the result is an object, it is interpreted as a Mongo query.
* If the result is an object, it is interpreted as a MongoDB query.
*
* @example `{ createdBy: { equals: id } }`
*

View File

@@ -8,8 +8,8 @@ export default async function sendEmail(message: SendMailOptions): Promise<unkno
result = await email.transport.sendMail(message);
} catch (err) {
this.logger.error(
`Failed to send mail to ${message.to}, subject: ${message.subject}`,
err,
`Failed to send mail to ${message.to}, subject: ${message.subject}`,
);
return err;
}

View File

@@ -22,7 +22,7 @@ export declare type PayloadRequest<U = any> = Request & {
fallbackLocale?: string;
/** Information about the collection that is being accessed
* - Configuration from payload-config.ts
* - Mongo model for this collection
* - MongoDB model for this collection
* - GraphQL type metadata
* */
collection?: Collection;

View File

@@ -6,6 +6,7 @@ import {
CodeField,
DateField,
EmailField,
fieldAffectsData,
JSONField,
NumberField,
PointField,
@@ -18,7 +19,6 @@ import {
TextField,
UploadField,
Validate,
fieldAffectsData,
} from './config/types';
import canUseDOM from '../utilities/canUseDOM';
import { isValidID } from '../utilities/isValidID';
@@ -319,8 +319,11 @@ export const relationship: Validate<unknown, unknown, RelationshipField> = async
requestedID = val.value;
}
const idField = payload.collections[collection].config.fields.find((field) => fieldAffectsData(field) && field.name === 'id');
if (requestedID === null) return false;
const idField = payload.collections[collection]?.config?.fields?.find((field) => fieldAffectsData(field) && field.name === 'id');
let type;
if (idField) {
type = idField.type === 'number' ? 'number' : 'text';
} else {

View File

@@ -23,7 +23,7 @@ import fieldToSchemaMap from './fieldToWhereInputSchemaMap';
// 1. Everything needs to be a GraphQLInputObjectType or scalar / enum
// 2. Relationships, groups, repeaters and flex content are not
// directly searchable. Instead, we need to build a chained pathname
// using dot notation so Mongo can properly search nested paths.
// using dot notation so MongoDB can properly search nested paths.
const buildWhereInputType = (name: string, fields: Field[], parentName: string): GraphQLInputObjectType => {
// This is the function that builds nested paths for all
// field types with nested paths.

View File

@@ -11,7 +11,7 @@ const connectMongoose = async (
logger: pino.Logger,
): Promise<void | any> => {
let urlToConnect = url;
let successfulConnectionMessage = 'Connected to Mongo server successfully!';
let successfulConnectionMessage = 'Connected to MongoDB server successfully!';
const connectionOptions: ConnectOptions & { useFacet: undefined } = {
autoIndex: true,
@@ -35,7 +35,7 @@ const connectMongoose = async (
});
urlToConnect = mongoMemoryServer.getUri();
successfulConnectionMessage = 'Connected to in-memory Mongo server successfully!';
successfulConnectionMessage = 'Connected to in-memory MongoDB server successfully!';
}
try {

View File

@@ -13,7 +13,7 @@
"confirmGeneration": "Generierung bestätigen",
"confirmPassword": "Passwort bestätigen",
"createFirstUser": "Ersten Benutzer erstellen",
"emailNotValid": "Die angegebene E-Mail-Adresse ist nicht valide",
"emailNotValid": "Die angegebene E-Mail-Adresse ist ungültig",
"emailSent": "E-Mail verschickt",
"enableAPIKey": "API-Key aktivieren",
"failedToUnlock": "Konnte nicht entsperren",
@@ -29,14 +29,14 @@
"logOut": "Abmelden",
"loggedIn": "Um dich mit einem anderen Benutzer anzumelden, musst du dich zuerst <0>abmelden</0>.",
"loggedInChangePassword": "Um dein Passwort zu ändern, gehe in dein <0>Konto</0> und ändere dort dein Passwort.",
"loggedOutInactivity": "Du wurdest auf Grund von Inaktivität abgemeldet.",
"loggedOutInactivity": "Du wurdest aufgrund von Inaktivität abgemeldet.",
"loggedOutSuccessfully": "Du wurdest erfolgreich abgemeldet.",
"login": "Anmelden",
"loginAttempts": "Anmelde-Versuche",
"loginUser": "Benutzer-Anmeldung",
"loginUser": "Benutzeranmeldung",
"loginWithAnotherUser": "Um dich mit einem anderen Benutzer anzumelden, musst du dich zuerst <0>abmelden</0>.",
"logout": "Abmelden",
"logoutUser": "Benutzer-Abmeldung",
"logoutUser": "Benutzerabmeldung",
"newAPIKeyGenerated": "Neuer API-Key wurde generiert",
"newAccountCreated": "Ein neues Konto wurde gerade für dich auf <a href=\"{{serverURL}}\">{{serverURL}}</a> erstellt. Bitte klicke auf den folgenden Link oder kopiere die URL in deinen Browser um deine E-Mail-Adresse zu verifizieren: <a href=\"{{verificationURL}}\">{{verificationURL}}</a><br> Nachdem du deine E-Mail-Adresse verifiziert hast, kannst du dich erfolgreich anmelden.",
"newPassword": "Neues Passwort",
@@ -231,7 +231,7 @@
"successfullyDuplicated": "{{label}} wurde erfolgreich dupliziert.",
"thisLanguage": "Deutsch",
"titleDeleted": "{{label}} {{title}} wurde erfolgreich gelöscht.",
"unauthorized": "Nicht authorisiert",
"unauthorized": "Nicht autorisiert",
"unsavedChangesDuplicate": "Du hast ungespeicherte Änderungen, möchtest du mit dem Duplizieren fortfahren?",
"untitled": "ohne Titel",
"updatedAt": "Aktualisiert am",

View File

@@ -4,10 +4,10 @@
"account": "Tài khoản",
"accountOfCurrentUser": "Tài khoản của người dùng hiện tại",
"alreadyActivated": "Đã được kích hoạt",
"alreadyLoggedIn": "Đã ở trạng thái đăng nhập",
"alreadyLoggedIn": "Đã đăng nhập",
"apiKey": "API Key",
"backToLogin": "Trở lại màn hình đăng nhập.",
"beginCreateFirstUser": "Dể bắt đầu, hãy tạo người dùng đầu tiên.",
"backToLogin": "Quay lại đăng nhập.",
"beginCreateFirstUser": "Để bắt đầu, hãy tạo người dùng đầu tiên.",
"changePassword": "Đổi mật khẩu",
"checkYourEmailForPasswordReset": "Hãy kiểm tra email của bạn để lấy đường dẫn tạo lại mật khẩu.",
"confirmGeneration": "Xác nhận, tạo API Key",
@@ -17,20 +17,20 @@
"emailSent": "Email đã được gửi",
"enableAPIKey": "Kích hoạt API Key",
"failedToUnlock": "Mở khóa thất bại",
"forceUnlock": "Cưỡng chế mở khóa",
"forceUnlock": "Mở khóa tài khoản",
"forgotPassword": "Quên mật khẩu",
"forgotPasswordEmailInstructions": "Hãy nhập địa chỉ email của bạn. Hướng dẫn thay đổi mật khẩu sẽ được gửi tới hòm thư của bạn.",
"forgotPasswordQuestion": "Bạn đã quên mật khẩu chăng?",
"forgotPasswordEmailInstructions": "Nhập email của bạn để nhận hướng dẫn tạo lại mật khẩu.",
"forgotPasswordQuestion": "Quên mật khẩu?",
"generate": "Tạo",
"generateNewAPIKey": "Tạo API Key mới",
"generatingNewAPIKeyWillInvalidate": "Tạo API Key mới sẽ <1>vô hiệu hóa</1> API Key cũ. Bạn có muốn tiếp tục không?",
"generatingNewAPIKeyWillInvalidate": "Việc tạo API Key mới sẽ <1>vô hiệu hóa</1> API Key cũ. Bạn có muốn tiếp tục không?",
"lockUntil": "Khóa lại cho tới thời điểm sau",
"logBackIn": "Đăng nhập lại",
"logOut": "Đăng xuất",
"loggedIn": "Để đăng nhập dưới tên người dùng khác, bạn phải <0>đăng xuất</0> người dùng hiện tại.",
"loggedInChangePassword": "Để đổi mật khẩu, hãy tới mục <0>tài khoản</0> và chỉnh sửa mật khẩu tại đó.",
"loggedInChangePassword": "Để đổi mật khẩu, hãy truy cập cài đặt <0>tài khoản</0>.",
"loggedOutInactivity": "Bạn đã tự động đăng xuất sau một khoản thời gian dài không thao tác.",
"loggedOutSuccessfully": "Bạn đã đăng xuất thành công.",
"loggedOutSuccessfully": "Đăng xuất thành công.",
"login": "Đăng nhập",
"loginAttempts": "Lần đăng nhập",
"loginUser": "Đăng nhập người dùng",
@@ -39,12 +39,12 @@
"logoutUser": "Đăng xuất người dùng",
"newAPIKeyGenerated": "API Key mới đã được tạo",
"newAccountCreated": "Một tài khoản mới đã được tạo cho bạn. Tài khoản này được dùng để truy cập <a href=\"{{serverURL}}\">{{serverURL}}</a> Hãy nhấp chuột hoặc sao chép đường dẫn sau vào trình duyệt của bạn để xác thực email: <a href=\"{{verificationURL}}\">{{verificationURL}}</a><br> Sau khi email được xác thực, bạn sẽ có thể đăng nhập.",
"newPassword": "Mật khu mới",
"resetPassword": "Tạo lại mật khu",
"newPassword": "Mật khu mới",
"resetPassword": "Tạo lại mật khu",
"resetPasswordExpiration": "Hạn tạo lại mật khẩu ",
"resetPasswordToken": "Tạo lại token của mật khẩu",
"resetPasswordToken": "Tạo lại token cho mật khẩu",
"resetYourPassword": "Tạo lại mật khẩu",
"stayLoggedIn": "Lưu giữ phiên đăng nhập",
"stayLoggedIn": "Duy trì đăng nhập",
"successfullyUnlocked": "Mở khóa thành công",
"unableToVerify": "Không thể xác thực",
"verified": "Đã xác thực",
@@ -52,7 +52,7 @@
"verify": "Tiến hành xác thực",
"verifyUser": "Tiến hành xác thực người dùng",
"verifyYourEmail": "Tiến hành xác thực email",
"youAreInactive": "Bạn đã không thao tác trong một khoảng thời gian, và sẽ bị tự động đăng xuất vì lý do an ninh. Bạn có muốn tiếp tục phiên đăng nhập.",
"youAreInactive": "Bạn đã không thao tác trong một khoảng thời gian, và sẽ bị tự động đăng xuất vì lý do bảo mật. Bạn có muốn tiếp tục phiên đăng nhập.",
"youAreReceivingResetPassword": "Bạn nhận được tin nhắn này vì bạn (hoặc một người nào khác) đã gửi yêu cầu thay đổi mật khẩu tài khoản của bạn. Xin hãy nhấp chuột vào đường dẫn sau, hoặc sao chép vào trình duyệt của bạn để hoàn tất quá trình:",
"youDidNotRequestPassword": "Nếu bạn không phải là người yêu cầu thay đổi mật khẩu, xin hãy bỏ qua tin nhắn này và mật khẩu của bạn sẽ được giữ nguyên."
},
@@ -60,14 +60,14 @@
"accountAlreadyActivated": "Lỗi - Tài khoản này đã được kích hoạt.",
"autosaving": "Lỗi - Đã xảy ra vấn đề khi tự động sao lưu bản tài liệu này.",
"correctInvalidFields": "Lỗi - Xin hãy sửa lại những fields không hợp lệ.",
"deletingFile": "Lỗi - Đã xảy ra vấn đề khi xóa file này.",
"deletingFile": "Lỗi - Đã xảy ra vấn đề khi xóa tệp này.",
"deletingTitle": "Lỗi - Đã xảy ra vấn đề khi xóa {{title}}. Hãy kiểm tra kết nối mạng và thử lại.",
"emailOrPasswordIncorrect": "Lỗi - Email hoặc mật khẩu không chính xác.",
"followingFieldsInvalid_many": "Lỗi - Những fields sau không hợp lệ:",
"followingFieldsInvalid_one": "Lỗi - Field sau không hợp lệ:",
"incorrectCollection": "Lỗi - Collection không hợp lệ.",
"invalidFileType": "Lỗi - Định dạng file không hợp lệ.",
"invalidFileTypeValue": "Lỗi - Định dạng file không hợp lệ: {{value}}.",
"invalidFileType": "Lỗi - Định dạng tệp không hợp lệ.",
"invalidFileTypeValue": "Lỗi - Định dạng tệp không hợp lệ: {{value}}.",
"loadingDocument": "Lỗi - Đã xảy ra vấn để khi tải bản tài liệu với ID {{id}}.",
"missingEmail": "Lỗi - Thiếu email.",
"missingIDOfDocument": "Lỗi - Thiếu ID của bản tài liệu cần cập nhật.",
@@ -104,7 +104,7 @@
"blocks": "blocks",
"chooseBetweenCustomTextOrDocument": "Chọn giữa nhập URL văn bản tùy chỉnh hoặc liên kết đến tài liệu khác.",
"chooseDocumentToLink": "Chọn một tài liệu để liên kết đến",
"chooseFromExisting": "Chọn từ vật phẩm có sẵn",
"chooseFromExisting": "Chọn từ thư viện",
"chooseLabel": "Chọn: {{label}}",
"collapseAll": "Ẩn toàn bộ",
"customURL": "URL tùy chỉnh",
@@ -120,7 +120,7 @@
"linkedTo": "Được nối với <0>{{label}}</0>",
"longitude": "Kinh độ",
"newLabel": "Tạo {{label}} mới",
"openInNewTab": "Mở ra trong trang mới",
"openInNewTab": "Mở trong trang mới",
"passwordsDoNotMatch": "Mật khẩu không trùng.",
"relatedDocument": "bản tài liệu liên quan",
"relationTo": "Có quan hệ với",
@@ -130,7 +130,7 @@
"selectExistingLabel": "Chọn một {{label}} có sẵn",
"selectFieldsToEdit": "Chọn các trường để chỉnh sửa",
"showAll": "Hiển thị toàn bộ",
"swapRelationship": "Hoán đổi quan hệ",
"swapRelationship": "Đổi quan hệ",
"swapUpload": "Đổi bản tải lên",
"textToDisplay": "Văn bản để hiển thị",
"toggleBlock": "Bật/tắt block",
@@ -143,27 +143,27 @@
"aboutToDeleteCount_other": "Bạn sắp xóa {{count}} {{label}}",
"addBelow": "Thêm bên dưới",
"addFilter": "Thêm bộ lọc",
"adminTheme": "Phông nền của trang Admin",
"adminTheme": "Giao diện bảng điều khiển",
"and": "Và",
"ascending": "Sắp xếp theo thứ tự tăng dần",
"automatic": "Tự động",
"backToDashboard": "Quay lại bảng điều khiển",
"cancel": "Ngừng thao tác",
"cancel": "Hủy",
"changesNotSaved": "Thay đổi chưa được lưu lại. Bạn sẽ mất bản chỉnh sửa nếu thoát bây giờ.",
"close": "Gần",
"collections": "Collections",
"columnToSort": "Sắp xếp cột",
"columns": "Hiện cột",
"columns": "Hiển thị cột",
"confirm": "Xác nhận",
"confirmDeletion": "Xác nhận, tiếp tục xóa",
"confirmDuplication": "Xác nhận, hãy tạo bản sao",
"confirmDeletion": "Xác nhận xóa",
"confirmDuplication": "Xác nhận tạo bản sao",
"copied": "Đâ sao chép",
"copy": "Sao chép",
"create": "Tạo",
"createNew": "Tạo bản mới",
"createNewLabel": "Tạo bản {{label}}",
"createNew": "Tạo mới",
"createNewLabel": "Tạo mới {{label}}",
"created": "Đã tạo",
"createdAt": "Được tạo vào lúc",
"createdAt": "Ngày tạo",
"creating": "Đang tạo",
"dark": "Nền tối",
"dashboard": "Bảng điều khiển",
@@ -183,35 +183,35 @@
"email": "Email",
"emailAddress": "Địa chỉ Email",
"enterAValue": "Nhập một giá trị",
"fallbackToDefaultLocale": "Dự phòng về ngôn ngữ mặc định",
"fallbackToDefaultLocale": "Ngôn ngữ mặc định",
"filter": "Lọc",
"filterWhere": "Lọc {{label}} với điều kiện:",
"filters": "Hiện bộ lọc",
"filters": "Bộ lọc",
"globals": "Toàn thể (globals)",
"language": "Ngôn ngữ",
"lastModified": "Chỉnh sửa lần cuối vào lúc",
"leaveAnyway": "Tiếp tục thoát",
"leaveWithoutSaving": "Không lưu dữ liệu và thoát",
"leaveWithoutSaving": "Thay đổi chưa được lưu",
"light": "Nền sáng",
"loading": "Đang tải",
"locales": "Mã khu vực",
"locales": "Khu vực",
"moveDown": "Di chuyển xuống",
"moveUp": "Di chuyển lên",
"newPassword": "Mật khảu mới",
"noFiltersSet": "Không có bộ lọc nào được áp dụng",
"noLabel": "<Không có {{label}}>",
"noResults": "Không tìm thấy: {{label}}. Có thể {{label}} chưa tồn tại hoặc không có dữ kiện trùng với bộ lọc hiện tại.",
"noResults": "Danh sách rỗng: {{label}}. Có thể {{label}} chưa tồn tại hoặc không có dữ kiện trùng với bộ lọc hiện tại.",
"noValue": "Không có giá trị",
"none": "Không có",
"notFound": "Không tìm thấy",
"nothingFound": "Không tìm thấy",
"of": "của",
"of": "trong số",
"or": "hoặc",
"order": "Thứ hạng",
"pageNotFound": "Khônng tìm thấy trang",
"order": "Thứ tự",
"pageNotFound": "Không tìm thấy trang",
"password": "Mật khẩu",
"payloadSettings": "Cài đặt",
"perPage": "Kết quả mỗi trang: {{limit}}",
"perPage": "Hiển thị mỗi trang: {{limit}}",
"remove": "Loại bỏ",
"row": "Hàng",
"rows": "Những hàng",
@@ -220,7 +220,7 @@
"searchBy": "Tìm với {{label}}",
"selectAll": "Chọn tất cả {{count}} {{label}}",
"selectValue": "Chọn một giá trị",
"selectedCount": "Đã chọn {{count}} {{nhãn}}",
"selectedCount": "Đã chọn {{count}} {{label}}",
"sorryNotFound": "Xin lỗi, không có kết quả nào tương ứng với request của bạn.",
"sort": "Sắp xếp",
"stayOnThisPage": "Ở lại trang này",
@@ -232,26 +232,26 @@
"titleDeleted": "{{label}} {{title}} đã được xóa thành công.",
"unauthorized": "Không có quyền truy cập.",
"unsavedChangesDuplicate": "Bạn chưa lưu các thay đổi. Bạn có muốn tiếp tục tạo bản sao?",
"untitled": "Không tiêu đề",
"updatedAt": "Được cập nhật vào lúc",
"untitled": "Chưa có tiêu đề",
"updatedAt": "Ngày cập nhật",
"updatedCountSuccessfully": "Đã cập nhật thành công {{count}} {{label}}.",
"updatedSuccessfully": "Cập nhật thành công.",
"updating": "Đang cập nhật",
"uploading": "Đang tải lên",
"user": "Người dùng",
"users": "Những người dùng",
"users": "Người dùng",
"welcome": "Xin chào"
},
"operators": {
"contains": "chứa",
"contains": "chứa",
"equals": "bằng",
"exists": "tồn tại",
"isGreaterThan": "lớn hơn",
"isGreaterThanOrEqualTo": "lớn hơn hoặc bằng",
"isIn": "đang ở",
"isIn": "có trong",
"isLessThan": "nhỏ hơn",
"isLessThanOrEqualTo": "nhỏ hơn hoặc bằng",
"isLike": "giống như",
"isLike": "gần giống",
"isNotEqualTo": "không bằng",
"isNotIn": "không có trong",
"near": "gần"
@@ -263,48 +263,48 @@
"fileSize": "Dung lượng file",
"height": "Chiều cao",
"lessInfo": "Hiển thị ít hơn",
"moreInfo": "Hiển thị nhiều hơn",
"moreInfo": "Thêm",
"selectCollectionToBrowse": "Chọn một Collection để tìm",
"selectFile": "Chọn một file",
"sizes": "Các độ phân giải",
"width": "Chiều rộng"
},
"validation": {
"emailAddress": "Xin hãy nhập địa chỉ email hợp lệ.",
"enterNumber": "Xin hãy nhập một con số hợp lệ.",
"emailAddress": "Địa chỉ email không hợp lệ.",
"enterNumber": "Vui lòng nhập số.",
"fieldHasNo": "Field này không có: {{label}}",
"greaterThanMax": "\"{{value}}\" đã vượt quá mốc giá trị tối đa sau đây: {{max}}.",
"invalidInput": "Dữ kiện được nhập vào field này không hợp lệ.",
"invalidSelection": "Lựa chọn ở field này không hợp lê.",
"greaterThanMax": "\"{{value}}\" đã vượt quá giá trị tối đa: {{max}}.",
"invalidInput": "Dữ liệu nhập vào không hợp lệ.",
"invalidSelection": "Lựa chọn ở field này không hợp l.",
"invalidSelections": "'Field này có những lựa chọn không hợp lệ sau:'",
"lessThanMin": "\"{{value}}\" đang ở dưới giá trị tối thiểu sau đây: {{min}}.",
"lessThanMin": "\"{{value}}\" đang thấp hơn giá trị tối thiểu: {{min}}.",
"longerThanMin": "Giá trị này cần có độ dài tối thiểu {{minLength}} ký tự.",
"notValidDate": "\"{{value}}\" không phải là một ngày (date) hợp lệ.",
"required": "Field này cần được diền.",
"requiresAtLeast": "Field này cần tối thiểu {{count}} {{label}}.",
"requiresNoMoreThan": "Field này không thể vượt quá {{count}} {{label}}.",
"requiresTwoNumbers": "Field này cần tối thiểu 2 con số.",
"requiresTwoNumbers": "Field này cần tối thiểu 2 chữ số.",
"shorterThanMax": "Giá trị phải ngắn hơn hoặc bằng {{maxLength}} ký tự.",
"trueOrFalse": "Field này chỉ có thể chứa giá trị true hoặc false.",
"validUploadID": "'Field này không chứa ID tải lên hợp lệ.'"
},
"version": {
"aboutToPublishSelection": "Bạn sắp xuất bản tất cả {{nhãn}} trong lựa chọn. Bạn có chắc không?",
"aboutToRestore": "Bạn chuẩn bị khôi phục lại {{label}} về trạng thái mà bản tài liệu này sở hữu lúc {{versionDate}}.",
"aboutToRestoreGlobal": "Bạn chuẩn bị khôi phục lại bản toàn thể (global) của {{label}} về trạng thái mà bản tài liệu này sở hữu lúc {{versionDate}}.",
"aboutToRevertToPublished": "Bạn chuẩn bị khiến bản nháp này quay về trạng thái khi được xuất bản. Bạn có muốn tiếp tục không?",
"aboutToUnpublish": "Bạn chuẩn bị ẩn bản tài liệu này. Bạn chắc chứ?",
"aboutToUnpublishSelection": "Bạn sắp hủy xuất bản tất cả {{nhãn}} trong lựa chọn. Bạn có chắc không?",
"aboutToPublishSelection": "Bạn có muốn xuất bản tất cả {{label}} không?",
"aboutToRestore": "Bạn chuẩn bị khôi phục lại {{label}} về phiên bản {{versionDate}}.",
"aboutToRestoreGlobal": "Bạn chuẩn bị khôi phục lại bản toàn thể (global) của {{label}} về phiên bản {{versionDate}}.",
"aboutToRevertToPublished": "Bạn có muốn tái xuất bản bản nháp này không?",
"aboutToUnpublish": "Bạn có muốn ngưng xuất bản?",
"aboutToUnpublishSelection": "Bạn có muốn ngưng xuất bản tất cả {{label}} không?",
"autosave": "Tự động lưu dữ liệu",
"autosavedSuccessfully": "Đã tự động lưu thành công.",
"autosavedVersion": "Các phiên bản từ việc tự động lưu dữ liệu",
"changed": "Đã đổi",
"changed": "Đã thay đổi",
"compareVersion": "So sánh phiên bản này với:",
"confirmPublish": "xác nhận xuất bản",
"confirmPublish": "Xác nhận xuất bản",
"confirmRevertToSaved": "Xác nhận, quay về trạng thái đã lưu",
"confirmUnpublish": "Xác nhận, ẩn tài liệu",
"confirmVersionRestoration": "Xác nhận, khôi phục về phiên bản sau",
"currentDocumentStatus": "Trạn thái tài liệu hiện tại: {{docStatus}}",
"confirmUnpublish": "Xác nhận, ngưng xuất bản",
"confirmVersionRestoration": "Xác nhận, khôi phục về phiên bản trước",
"currentDocumentStatus": "Trạng thái tài liệu hiện tại: {{docStatus}}",
"draft": "Bản nháp",
"draftSavedSuccessfully": "Bản nháp đã được lưu thành công.",
"lastSavedAgo": "Bản lưu mới nhất: {{distance, relativetime(minutes)}} trước",
@@ -328,7 +328,7 @@
"status": "Trạng thái",
"type": "Loại",
"unpublish": "Ẩn tài liệu",
"unpublishing": "Dang ẩn tài liệu...",
"unpublishing": "Đang ẩn tài liệu...",
"version": "Phiên bản",
"versionCount_many": "{{count}} phiên bản được tìm thấy",
"versionCount_none": "Không có phiên bản nào được tìm thấy",
@@ -336,7 +336,7 @@
"versionCount_other": "Đã tìm thấy {{count}} phiên bản",
"versionCreatedOn": "Phiên bản {{version}} được tạo vào lúc:",
"versionID": "ID của phiên bản",
"versions": "Những phiên bản",
"versions": "Danh sách phiên bản",
"viewingVersion": "Xem phiên bản của {{entityLabel}} {{documentTitle}}",
"viewingVersionGlobal": "`Xem phiên bản toàn thể (global) của {{entityLabel}}",
"viewingVersions": "Xem những phiên bản của {{entityLabel}} {{documentTitle}}",

View File

@@ -329,13 +329,14 @@ describe('fields - relationship', () => {
test('should show useAsTitle on relation', async () => {
await page.goto(url.edit(docWithExistingRelations.id));
const field = page.locator('#field-relationshipWithTitle .relationship--single-value__text');
const field = page.locator('#field-relationshipWithTitle');
const value = field.locator('.relationship--single-value__text');
// Check existing relationship for correct title
await expect(field).toHaveText(relationWithTitle.name);
await expect(value).toHaveText(relationWithTitle.name);
await field.click({ delay: 100 });
const options = page.locator('.rs__option');
const options = field.locator('.rs__option');
await expect(options).toHaveCount(2);
});

5389
yarn.lock

File diff suppressed because it is too large Load Diff