Compare commits
47 Commits
payload/2.
...
fix/drizzl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a87f6e65a | ||
|
|
492ed30cb8 | ||
|
|
fca5a404db | ||
|
|
b13f7e8843 | ||
|
|
25dfdb66cd | ||
|
|
9c9e6896a5 | ||
|
|
a3085435ef | ||
|
|
1466657e8f | ||
|
|
1348483648 | ||
|
|
5321098d7e | ||
|
|
c57591bc4f | ||
|
|
9750bc217e | ||
|
|
468e5441f1 | ||
|
|
3c5cce4c6f | ||
|
|
9f0f94893d | ||
|
|
03b7892fc9 | ||
|
|
f96cf593ce | ||
|
|
8259611ce6 | ||
|
|
a3ed25a253 | ||
|
|
69e7b7a158 | ||
|
|
c6da99b4d1 | ||
|
|
ebd23caa56 | ||
|
|
faa9b21824 | ||
|
|
1690560f11 | ||
|
|
0058660b3f | ||
|
|
6d7ef919cb | ||
|
|
abffa37d85 | ||
|
|
1b208c7add | ||
|
|
2840632161 | ||
|
|
0841d5a35e | ||
|
|
bd19fcf259 | ||
|
|
18645771c8 | ||
|
|
20377bb22c | ||
|
|
7daaf3d780 | ||
|
|
667d3dc885 | ||
|
|
51474fa661 | ||
|
|
d475b16790 | ||
|
|
4d0befb67a | ||
|
|
84d214f992 | ||
|
|
51cd5942fa | ||
|
|
74105d8ee5 | ||
|
|
1cc61ddab6 | ||
|
|
99397a0bdb | ||
|
|
a5492afad6 | ||
|
|
6c1156e2e4 | ||
|
|
77e8ce980e | ||
|
|
39e34ce94e |
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@@ -35,5 +35,9 @@
|
||||
"eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }],
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
// Load .git-blame-ignore-revs file
|
||||
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"]
|
||||
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"],
|
||||
"jestrunner.jestCommand": "pnpm exec cross-env NODE_OPTIONS=\"--experimental-vm-modules --no-deprecation\" node 'node_modules/jest/bin/jest.js'",
|
||||
"jestrunner.debugOptions": {
|
||||
"runtimeArgs": ["--experimental-vm-modules", "--no-deprecation"]
|
||||
}
|
||||
}
|
||||
|
||||
60
CHANGELOG.md
60
CHANGELOG.md
@@ -1,3 +1,63 @@
|
||||
## [2.25.0](https://github.com/payloadcms/payload/compare/v2.24.2...v2.25.0) (2024-07-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* allows metadata to be appended to the file of the output media ([#7295](https://github.com/payloadcms/payload/issues/7295)) ([3c5cce4](https://github.com/payloadcms/payload/commit/3c5cce4c6f108f87e87b091bbfec976423de73a2))
|
||||
* **db-mongodb:** adds new optional `collation` feature flag behind mongodb collation option ([#7359](https://github.com/payloadcms/payload/issues/7359)) ([9750bc2](https://github.com/payloadcms/payload/commit/9750bc217ee7d63732a34908c84eb88b88dac0a8)), closes [#7349](https://github.com/payloadcms/payload/issues/7349)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* properly handles `0` value number fields in list view ([#7364](https://github.com/payloadcms/payload/issues/7364)) ([5321098](https://github.com/payloadcms/payload/commit/5321098d7eada43838f6d5c69f3233c150fe0afa)), closes [#5510](https://github.com/payloadcms/payload/issues/5510)
|
||||
* preserves objectids in deepCopyObject ([#7385](https://github.com/payloadcms/payload/issues/7385)) ([1348483](https://github.com/payloadcms/payload/commit/134848364801c72cc773ef7b48854306d1b9bac3))
|
||||
* relaxes equality check for relationship options in filter ([#7344](https://github.com/payloadcms/payload/issues/7344)) ([468e544](https://github.com/payloadcms/payload/commit/468e5441f16775134d915ec7caddb17b817d3408))
|
||||
* supports null values in query strings ([#5241](https://github.com/payloadcms/payload/issues/5241)) ([c57591b](https://github.com/payloadcms/payload/commit/c57591bc4fb8d28b7de16a111faffea7d3e11f8d))
|
||||
|
||||
## [2.24.2](https://github.com/payloadcms/payload/compare/v2.24.1...v2.24.2) (2024-07-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **db-mongodb:** add jsonParse flag to mongooseAdapter that preserves existing, untracked MongoDB data types ([#7338](https://github.com/payloadcms/payload/issues/7338)) ([f96cf59](https://github.com/payloadcms/payload/commit/f96cf593cedcae0d8ed55f9a70e8e4e77917a876))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow autosave relationship drawers to function properly ([#7325](https://github.com/payloadcms/payload/issues/7325)) ([69e7b7a](https://github.com/payloadcms/payload/commit/69e7b7a158c38058ece54a97bfa79e65192774a6))
|
||||
* **db-mongodb:** removes precedence of regular chars over international chars in sort ([#6923](https://github.com/payloadcms/payload/issues/6923)) ([0058660](https://github.com/payloadcms/payload/commit/0058660b3f8bd820abb4494ff53fa67f49f0f6b4)), closes [#6719](https://github.com/payloadcms/payload/issues/6719)
|
||||
* fetches and sets permissions before setting user ([#7337](https://github.com/payloadcms/payload/issues/7337)) ([8259611](https://github.com/payloadcms/payload/commit/8259611ce60e23f6298a07564d5f6dd2966d61ff))
|
||||
* **plugin-stripe:** properly types async webhooks ([#7316](https://github.com/payloadcms/payload/issues/7316)) ([c6da99b](https://github.com/payloadcms/payload/commit/c6da99b4d1b986089bb697486a7825db66323078))
|
||||
|
||||
## [2.24.1](https://github.com/payloadcms/payload/compare/v2.24.0...v2.24.1) (2024-07-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* aliases AfterMe, AfterLogout, and AfterRefresh hook types ([#7146](https://github.com/payloadcms/payload/issues/7146)) ([20377bb](https://github.com/payloadcms/payload/commit/20377bb22c867552e412c1cafd16869399aadd68))
|
||||
* exports fallback hook types to ensure backwards compatibility ([#7217](https://github.com/payloadcms/payload/issues/7217)) ([1864577](https://github.com/payloadcms/payload/commit/18645771c86664f1246f0fb599c8265a4cd1d6c0))
|
||||
* resizes images first before applying focal point ([#7278](https://github.com/payloadcms/payload/issues/7278)) ([1b208c7](https://github.com/payloadcms/payload/commit/1b208c7addf56ae8a1af5e408b001b3e5f080a38))
|
||||
* uploads from drawer and focal point positioning ([#7244](https://github.com/payloadcms/payload/issues/7244)) ([0841d5a](https://github.com/payloadcms/payload/commit/0841d5a35ee00650c703231a08fc9a361861ba67))
|
||||
|
||||
## [2.24.0](https://github.com/payloadcms/payload/compare/v2.23.1...v2.24.0) (2024-07-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* adds ability to upload files from a remote url ([#7087](https://github.com/payloadcms/payload/issues/7087)) ([84d214f](https://github.com/payloadcms/payload/commit/84d214f99207ad78a466b8c16eb36e29f57cd0e3))
|
||||
* **db-mongodb:** allows mongoose schemaOptions to be configured ([#7099](https://github.com/payloadcms/payload/issues/7099)) ([51474fa](https://github.com/payloadcms/payload/commit/51474fa661ae24ab8fc0d13001fafc0f35216c1e))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ensures access query runs with locale when present ([#6981](https://github.com/payloadcms/payload/issues/6981)) ([a5492af](https://github.com/payloadcms/payload/commit/a5492afad672e19dd35b1f5370b51f22656f334c))
|
||||
|
||||
## [2.23.1](https://github.com/payloadcms/payload/compare/v2.23.0...v2.23.1) (2024-06-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove unused refresh arg, this affected me/refresh hooks ([#6976](https://github.com/payloadcms/payload/issues/6976)) ([c82d2ca](https://github.com/payloadcms/payload/commit/77e8ce980ef0bcb0380b499dd1ccdfd36199b707))
|
||||
|
||||
## [2.23.0](https://github.com/payloadcms/payload/compare/v2.22.2...v2.23.0) (2024-06-28)
|
||||
|
||||
|
||||
|
||||
@@ -491,7 +491,7 @@ As an alternative to replacing the entire Field component, you may want to keep
|
||||
| Component | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| **`Label`** | Override the default Label in the Field Component. [More](#label-component) |
|
||||
| **`Error`** | Override the default Label in the Field Component. [More](#error-component) |
|
||||
| **`Error`** | Override the default Error in the Field Component. [More](#error-component) |
|
||||
| **`beforeInput`** | An array of elements that will be added before `input`/`textarea` elements. [More](#afterinput-and-beforeinput) |
|
||||
| **`afterInput`** | An array of elements that will be added after `input`/`textarea` elements. [More](#afterinput-and-beforeinput) |
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ mutation {
|
||||
|
||||
### Refresh
|
||||
|
||||
Allows for "refreshing" JWTs. If your user has a token that is about to expire, but the user is still active and using the app, you might want to use the `refresh` operation to receive a new token by sending the operation the token that is about to expire.
|
||||
Allows for "refreshing" JWTs. If your user has a token that is about to expire, but the user is still active and using the app, you might want to use the `refresh` operation to receive a new token by executing this operation via the authenticated user.
|
||||
|
||||
This operation requires a non-expired token to send back a new one. If the user's token has already expired, you will need to allow them to log in again to retrieve a new token.
|
||||
|
||||
@@ -237,13 +237,6 @@ mutation {
|
||||
}
|
||||
```
|
||||
|
||||
<Banner type="success">
|
||||
The Refresh operation will automatically find the user's token in either a JWT header or the
|
||||
HTTP-only cookie. But, you can specify the token you're looking to refresh by providing the REST
|
||||
API with a `token` within the JSON body of the request, or by providing the GraphQL resolver a
|
||||
`token` arg.
|
||||
</Banner>
|
||||
|
||||
### Verify by Email
|
||||
|
||||
If your collection supports email verification, the Verify operation will be exposed which accepts a verification token and sets the user's `_verified` property to `true`, thereby allowing the user to authenticate with the Payload API.
|
||||
@@ -290,6 +283,9 @@ const res = await fetch(`http://localhost:3000/api/[collection-slug]/unlock`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: 'dev@payloadcms.com',
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
@@ -297,7 +293,7 @@ const res = await fetch(`http://localhost:3000/api/[collection-slug]/unlock`, {
|
||||
|
||||
```
|
||||
mutation {
|
||||
unlock[collection-singular-label]
|
||||
unlock[collection-singular-label](email: "dev@payloadcms.com")
|
||||
}
|
||||
```
|
||||
|
||||
@@ -306,6 +302,9 @@ mutation {
|
||||
```ts
|
||||
const result = await payload.unlock({
|
||||
collection: '[collection-slug]',
|
||||
data: {
|
||||
email: 'dev@payloadcms.com',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -30,13 +30,18 @@ export default buildConfig({
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Description |
|
||||
|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
|
||||
| `connectOptions` | 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. |
|
||||
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. | |
|
||||
| Option | Description |
|
||||
|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
|
||||
| `schemaOptions` | Customize schema options for all Mongoose schemas created internally. |
|
||||
| `jsonParse` | Set to false to disable the automatic JSON stringify/parse of data queried by MongoDB. For example, if you have data not tracked by Payload such as `Date` fields and similar, you can use this option to ensure that existing `Date` properties remain as `Date` and not strings. |
|
||||
| `collections` | Options on a collection-by-collection basis. [More](#collections-options) |
|
||||
| `globals` | Options for the Globals collection created by Payload. [More](#globals-options) |
|
||||
| `connectOptions` | 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. |
|
||||
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
|
||||
| `migrationDir` | Customize the directory that migrations are stored. |
|
||||
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
|
||||
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
|
||||
|
||||
### Access to Mongoose models
|
||||
|
||||
@@ -48,3 +53,51 @@ You can access Mongoose models as follows:
|
||||
- Collection models - `payload.db.collections[myCollectionSlug]`
|
||||
- Globals model - `payload.db.globals`
|
||||
- Versions model (both collections and globals) - `payload.db.versions[myEntitySlug]`
|
||||
|
||||
### Collections Options
|
||||
|
||||
You can configure the way the MongoDB adapter works on a collection-by-collection basis, including customizing Mongoose `schemaOptions` for each collection schema created.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
const db = mongooseAdapter({
|
||||
url: 'your-url-here',
|
||||
collections: {
|
||||
users: {
|
||||
//
|
||||
schemaOptions: {
|
||||
strict: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Global Options
|
||||
|
||||
Payload automatically creates a single `globals` collection that correspond with any Payload globals that you define. When you initialize the `mongooseAdapter`, you can specify settings here for your globals in a similar manner to how you can for collections above. Right now, the only property available is `schemaOptions` but more may be added in the future.
|
||||
|
||||
### Preserving externally managed data
|
||||
|
||||
You can use Payload in conjunction with an existing MongoDB database, where you might have some fields "tracked" in Payload via corresponding field configs, and other fields completely unknown to Payload.
|
||||
|
||||
If you have external field data in existing MongoDB collections which you'd like to use in combination with Payload, and you don't want to lose those external fields, you can configure Payload to "preserve" that data while it makes updates to your existing documents.
|
||||
|
||||
To do this, the first step is to configure Mongoose's `strict` property, which tells Mongoose to write all data that it receives (and not disregard any data that it does not know about).
|
||||
|
||||
The second step is to disable Payload's automatic JSON parsing of documents it receives from MongoDB.
|
||||
|
||||
Here's an example for how to configure your Mongoose adapter to preserve external collection fields that are not tracked by Payload:
|
||||
|
||||
```ts
|
||||
mongooseAdapter({
|
||||
url: process.env.DATABASE_URI,
|
||||
// Disable the JSON parsing that Payload performs
|
||||
jsonParse: false,
|
||||
// Disable strict mode for Mongoose
|
||||
schemaOptions: {
|
||||
strict: false,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -70,7 +70,16 @@ In addition to being able to define access control on a document-level, you can
|
||||
|
||||
### Field names
|
||||
|
||||
Some fields use their `name` property as a unique identifier to store and retrieve from the database. `__v`, `salt`, and `hash` are all reserved field names which are sanitized from Payload's config and cannot be used.
|
||||
All fields require a `name` property. This is the key that will be used to store and retrieve the field's value in the database. This property must be unique within the Collection, Global, or nested group that it is defined in.
|
||||
|
||||
Payload reserves various field names for internal use. Using reserved field names will result in your field being sanitized from the config.
|
||||
|
||||
The following field names are forbidden and cannot be used:
|
||||
|
||||
- `__v`
|
||||
- `salt`
|
||||
- `hash`
|
||||
- `file`
|
||||
|
||||
### Validation
|
||||
|
||||
@@ -145,6 +154,7 @@ const field: Field = {
|
||||
Collections ID fields are generated automatically by default. An explicit `id` field can be declared in the `fields` array to override this behavior.
|
||||
Users are then required to provide a custom ID value when creating a record through the Admin UI or API.
|
||||
Valid ID types are `number` and `text`.
|
||||
When using the text value, remember that it shouldn't contain the / (slash) sign, as the API will read it separately and this can result in unexpected behavior.
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ const res = await fetch(`/api/stripe/rest`, {
|
||||
Development:
|
||||
|
||||
1. Login using Stripe cli `stripe login`
|
||||
1. Forward events to localhost `stripe listen --forward-to localhost:3000/stripe/webhooks`
|
||||
1. Forward events to localhost `stripe listen --forward-to localhost:3000/api/stripe/webhooks`
|
||||
1. Paste the given secret into your `.env` file as `STRIPE_WEBHOOKS_ENDPOINT_SECRET`
|
||||
|
||||
Production:
|
||||
|
||||
@@ -23,7 +23,7 @@ _Admin panel screenshot depicting a Media Collection with Upload enabled_
|
||||
**By simply enabling Upload functionality on a Collection, Payload will automatically transform your Collection into a robust file management / storage solution. The following modifications will be made:**
|
||||
|
||||
1. `filename`, `mimeType`, and `filesize` fields will be automatically added to your Collection. Optionally, if you pass `imageSizes` to your Collection's Upload config, a [`sizes`](#image-sizes) array will also be added containing auto-resized image sizes and filenames.
|
||||
1. The Admin panel will modify its built-in `List` component to show a thumbnail gallery of your Uploads instead of the default Table view
|
||||
1. The Admin panel will modify its built-in `List` component to show a thumbnail for each upload within the List View
|
||||
1. The Admin panel will modify its `Edit` view(s) to add a new set of corresponding Upload UI which will allow for file upload
|
||||
1. The `create`, `update`, and `delete` Collection operations will be modified to support file upload, re-upload, and deletion
|
||||
|
||||
@@ -56,6 +56,7 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
|
||||
| **`staticOptions`** | Set options for `express.static` to use while serving your static files. [More](http://expressjs.com/en/resources/middleware/serve-static.html) |
|
||||
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
|
||||
| **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. |
|
||||
| **`withMetadata`** | If specified, appends metadata to the output image file. Accepts a boolean or a function that receives `metadata` and `req`, returning a boolean. |
|
||||
|
||||
_An asterisk denotes that a property above is required._
|
||||
|
||||
|
||||
@@ -481,11 +481,11 @@ brace-expansion@^1.1.7:
|
||||
concat-map "0.0.1"
|
||||
|
||||
braces@^3.0.2, braces@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
||||
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
|
||||
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
|
||||
dependencies:
|
||||
fill-range "^7.0.1"
|
||||
fill-range "^7.1.1"
|
||||
|
||||
busboy@1.6.0:
|
||||
version "1.6.0"
|
||||
@@ -1071,10 +1071,10 @@ file-entry-cache@^6.0.1:
|
||||
dependencies:
|
||||
flat-cache "^3.0.4"
|
||||
|
||||
fill-range@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
||||
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
|
||||
fill-range@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
|
||||
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
|
||||
dependencies:
|
||||
to-regex-range "^5.0.1"
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ To spin up this example locally, follow these steps:
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server and seed the database
|
||||
5. `open http://localhost:3000/admin` to access the admin panel
|
||||
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
|
||||
6. Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
|
||||
|
||||
@@ -8304,9 +8304,9 @@ wrappy@1:
|
||||
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
|
||||
|
||||
ws@^7.3.1:
|
||||
version "7.5.9"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
|
||||
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
|
||||
version "7.5.10"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
|
||||
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
|
||||
|
||||
xss@^1.0.6:
|
||||
version "1.0.14"
|
||||
|
||||
@@ -9,7 +9,7 @@ To spin up this example locally, follow these steps:
|
||||
1. First clone the repo
|
||||
1. Then `cd YOUR_PROJECT_REPO && cp .env.example .env`
|
||||
1. Next `yarn && yarn dev`
|
||||
1. Now `open http://localhost:3000/admin` to access the admin panel
|
||||
1. Now Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
|
||||
1. Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
|
||||
|
||||
@@ -13,7 +13,7 @@ Follow the instructions in each respective README to get started. If you are set
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server and seed the database
|
||||
5. `open http://localhost:3000/admin` to access the admin panel
|
||||
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
|
||||
6. Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
|
||||
|
||||
@@ -5141,9 +5141,9 @@ node-releases@^2.0.12:
|
||||
integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==
|
||||
|
||||
nodemailer@^6.9.0:
|
||||
version "6.9.4"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.4.tgz#93bd4a60eb0be6fa088a0483340551ebabfd2abf"
|
||||
integrity sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA==
|
||||
version "6.9.14"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.14.tgz#845fda981f9fd5ac264f4446af908a7c78027f75"
|
||||
integrity sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==
|
||||
|
||||
nodemon@^2.0.6:
|
||||
version "2.0.22"
|
||||
|
||||
@@ -9,7 +9,7 @@ To spin up the project locally, follow these steps:
|
||||
1. First clone the repo
|
||||
1. Then `cd YOUR_PROJECT_REPO && cp .env.example .env`
|
||||
1. Next `yarn && yarn dev` (or `docker-compose up`, see [Docker](#docker))
|
||||
1. Now `open http://localhost:3000/admin` to access the admin panel
|
||||
1. Now Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
|
||||
1. Create your first admin user using the form on the page
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app.
|
||||
|
||||
@@ -15,7 +15,7 @@ Follow the instructions in each respective README to get started. If you are set
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server and seed the database
|
||||
5. `open http://localhost:3000/admin` to access the admin panel
|
||||
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
|
||||
6. Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.
|
||||
|
||||
@@ -9,7 +9,7 @@ To spin up this example locally, follow these steps:
|
||||
1. First clone the repo
|
||||
1. Then `cd YOUR_PROJECT_REPO && cp .env.example .env`
|
||||
1. Next `yarn && yarn dev`
|
||||
1. Now `open http://localhost:3000/admin` to access the admin panel
|
||||
1. Now Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
|
||||
1. Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details on how to log in as a tenant.
|
||||
|
||||
@@ -17,7 +17,7 @@ To spin up this example locally, follow these steps:
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server and seed the database
|
||||
5. `open http://localhost:3000/admin` to access the admin panel
|
||||
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
|
||||
6. Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -16,7 +16,7 @@ To spin up this example locally, follow these steps:
|
||||
2. `cd` into this directory and run `yarn` or `npm install`
|
||||
3. `cp .env.example .env` to copy the example environment variables
|
||||
4. `yarn dev` or `npm run dev` to start the server and seed the database
|
||||
5. `open http://localhost:3000/admin` to access the admin panel
|
||||
5. Open [http://localhost:3000/admin](http://localhost:3000/admin) to access the admin panel
|
||||
6. Login with email `demo@payloadcms.com` and password `demo`
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"copyfiles": "2.4.1",
|
||||
"cross-env": "7.0.3",
|
||||
"dotenv": "8.6.0",
|
||||
"drizzle-orm": "0.29.3",
|
||||
"drizzle-orm": "0.32.1",
|
||||
"express": "4.18.2",
|
||||
"form-data": "3.0.1",
|
||||
"fs-extra": "10.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "1.5.2",
|
||||
"version": "1.7.1",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -27,11 +27,11 @@
|
||||
"bson-objectid": "2.0.4",
|
||||
"deepmerge": "4.3.1",
|
||||
"get-port": "5.1.1",
|
||||
"http-status": "1.6.2",
|
||||
"mongoose": "6.12.3",
|
||||
"mongoose-aggregate-paginate-v2": "1.0.6",
|
||||
"mongoose-paginate-v2": "1.7.22",
|
||||
"prompts": "2.4.2",
|
||||
"http-status": "1.6.2",
|
||||
"uuid": "9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { Create } from 'payload/database'
|
||||
import type { Document, PayloadRequest } from 'payload/types'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
import handleError from './utilities/handleError'
|
||||
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
|
||||
import { withSession } from './withSession'
|
||||
|
||||
export const create: Create = async function create(
|
||||
@@ -19,15 +20,13 @@ export const create: Create = async function create(
|
||||
handleError(error, req)
|
||||
}
|
||||
|
||||
// doc.toJSON does not do stuff like converting ObjectIds to string, or date strings to date objects. That's why we use JSON.parse/stringify here
|
||||
const result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const result = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc.toObject()
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
}
|
||||
|
||||
return result
|
||||
return sanitizeInternalFields(result)
|
||||
}
|
||||
|
||||
@@ -19,10 +19,8 @@ export const createGlobal: CreateGlobal = async function createGlobal(
|
||||
|
||||
let [result] = (await Model.create([global], options)) as any
|
||||
|
||||
result = JSON.parse(JSON.stringify(result))
|
||||
result = this.jsonParse ? JSON.parse(JSON.stringify(result)) : result.toObject()
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
|
||||
return result
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { CreateGlobalVersion } from 'payload/database'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
import type { Document } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
|
||||
import { withSession } from './withSession'
|
||||
|
||||
export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(
|
||||
@@ -52,13 +52,12 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
|
||||
options,
|
||||
)
|
||||
|
||||
const result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const result = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc.toObject()
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
}
|
||||
return result
|
||||
return sanitizeInternalFields(result)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { CreateVersion } from 'payload/database'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
import type { Document } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
|
||||
import { withSession } from './withSession'
|
||||
|
||||
export const createVersion: CreateVersion = async function createVersion(
|
||||
@@ -60,13 +60,13 @@ export const createVersion: CreateVersion = async function createVersion(
|
||||
options,
|
||||
)
|
||||
|
||||
const result: Document = JSON.parse(JSON.stringify(doc))
|
||||
const result = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc.toObject()
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
}
|
||||
return result
|
||||
|
||||
return sanitizeInternalFields(result)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { DeleteOne } from 'payload/database'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
import type { Document } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
@@ -19,13 +18,9 @@ export const deleteOne: DeleteOne = async function deleteOne(
|
||||
where,
|
||||
})
|
||||
|
||||
const doc = await Model.findOneAndDelete(query, options).lean()
|
||||
let doc = await Model.findOneAndDelete(query, options).lean()
|
||||
|
||||
let result: Document = JSON.parse(JSON.stringify(doc))
|
||||
doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
|
||||
return result
|
||||
return sanitizeInternalFields(doc)
|
||||
}
|
||||
|
||||
@@ -55,6 +55,14 @@ export const find: Find = async function find(
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (this.collation) {
|
||||
const defaultLocale = 'en'
|
||||
paginationOptions.collation = {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
|
||||
...this.collation,
|
||||
}
|
||||
}
|
||||
|
||||
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
|
||||
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
|
||||
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
|
||||
@@ -82,13 +90,12 @@ export const find: Find = async function find(
|
||||
}
|
||||
|
||||
const result = await Model.paginate(query, paginationOptions)
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
doc.id = doc._id
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -30,12 +30,16 @@ export const findGlobal: FindGlobal = async function findGlobal(
|
||||
if (!doc) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.jsonParse) {
|
||||
doc = JSON.parse(JSON.stringify(doc))
|
||||
}
|
||||
|
||||
if (doc._id) {
|
||||
doc.id = doc._id
|
||||
doc.id = JSON.parse(JSON.stringify(doc._id))
|
||||
delete doc._id
|
||||
}
|
||||
|
||||
doc = JSON.parse(JSON.stringify(doc))
|
||||
doc = sanitizeInternalFields(doc)
|
||||
|
||||
return doc
|
||||
|
||||
@@ -74,6 +74,14 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (this.collation) {
|
||||
const defaultLocale = 'en'
|
||||
paginationOptions.collation = {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
|
||||
...this.collation,
|
||||
}
|
||||
}
|
||||
|
||||
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
|
||||
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
|
||||
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
|
||||
@@ -101,13 +109,12 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
|
||||
}
|
||||
|
||||
const result = await Model.paginate(query, paginationOptions)
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
doc.id = doc._id
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { MongooseQueryOptions } from 'mongoose'
|
||||
import type { FindOne } from 'payload/database'
|
||||
import type { PayloadRequest } from 'payload/types'
|
||||
import type { Document } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
@@ -24,17 +23,15 @@ export const findOne: FindOne = async function findOne(
|
||||
where,
|
||||
})
|
||||
|
||||
const doc = await Model.findOne(query, {}, options)
|
||||
let doc = await Model.findOne(query, {}, options)
|
||||
|
||||
if (!doc) {
|
||||
return null
|
||||
}
|
||||
|
||||
let result: Document = JSON.parse(JSON.stringify(doc))
|
||||
doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
doc = sanitizeInternalFields(doc)
|
||||
|
||||
return result
|
||||
return doc
|
||||
}
|
||||
|
||||
@@ -70,6 +70,14 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (this.collation) {
|
||||
const defaultLocale = 'en'
|
||||
paginationOptions.collation = {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
|
||||
...this.collation,
|
||||
}
|
||||
}
|
||||
|
||||
if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
|
||||
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
|
||||
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
|
||||
@@ -97,13 +105,12 @@ export const findVersions: FindVersions = async function findVersions(
|
||||
}
|
||||
|
||||
const result = await Model.paginate(query, paginationOptions)
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
doc.id = doc._id
|
||||
return sanitizeInternalFields(doc)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TransactionOptions } from 'mongodb'
|
||||
import type { ClientSession, ConnectOptions, Connection } from 'mongoose'
|
||||
import type { CollationOptions, TransactionOptions } from 'mongodb'
|
||||
import type { ClientSession, ConnectOptions, Connection, SchemaOptions } from 'mongoose'
|
||||
import type { Payload } from 'payload'
|
||||
import type { BaseDatabaseAdapter } from 'payload/database'
|
||||
|
||||
@@ -42,14 +42,58 @@ export type { MigrateDownArgs, MigrateUpArgs } from './types'
|
||||
export interface Args {
|
||||
/** Set to false to disable auto-pluralization of collection names, Defaults to true */
|
||||
autoPluralization?: boolean
|
||||
/**
|
||||
* If enabled, collation allows for language-specific rules for string comparison.
|
||||
* This configuration can include the following options:
|
||||
*
|
||||
* - `strength` (number): Comparison level (1: Primary, 2: Secondary, 3: Tertiary (default), 4: Quaternary, 5: Identical)
|
||||
* - `caseLevel` (boolean): Include case comparison at strength level 1 or 2.
|
||||
* - `caseFirst` (string): Sort order of case differences during tertiary level comparisons ("upper", "lower", "off").
|
||||
* - `numericOrdering` (boolean): Compare numeric strings as numbers.
|
||||
* - `alternate` (string): Consider whitespace and punctuation as base characters ("non-ignorable", "shifted").
|
||||
* - `maxVariable` (string): Characters considered ignorable when `alternate` is "shifted" ("punct", "space").
|
||||
* - `backwards` (boolean): Sort strings with diacritics from back of the string.
|
||||
* - `normalization` (boolean): Check if text requires normalization and perform normalization.
|
||||
*
|
||||
* Available on MongoDB version 3.4 and up.
|
||||
* The locale that gets passed is your current project's locale but defaults to "en".
|
||||
*
|
||||
* Example:
|
||||
* {
|
||||
* strength: 3
|
||||
* }
|
||||
*
|
||||
* Defaults to disabled.
|
||||
*/
|
||||
collation?: Omit<CollationOptions, 'locale'>
|
||||
/** Define Mongoose options on a collection-by-collection basis.
|
||||
*/
|
||||
collections?: {
|
||||
[slug: string]: {
|
||||
/** Define Mongoose schema options for a given collection.
|
||||
*/
|
||||
schemaOptions?: SchemaOptions
|
||||
}
|
||||
}
|
||||
/** Extra configuration options */
|
||||
connectOptions?: ConnectOptions & {
|
||||
/** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */
|
||||
useFacet?: boolean
|
||||
}
|
||||
|
||||
/** Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false */
|
||||
disableIndexHints?: boolean
|
||||
/** Define Mongoose options for the globals collection.
|
||||
*/
|
||||
globals?: {
|
||||
schemaOptions?: SchemaOptions
|
||||
}
|
||||
/** Set to false to disable the automatic JSON stringify/parse of data queried by MongoDB. For example, if you have data not tracked by Payload such as `Date` fields and similar, you can use this option to ensure that existing `Date` properties remain as `Date` and not strings. */
|
||||
jsonParse?: boolean
|
||||
migrationDir?: string
|
||||
/** Define default Mongoose schema options for all schemas created.
|
||||
*/
|
||||
schemaOptions?: SchemaOptions
|
||||
transactionOptions?: TransactionOptions | false
|
||||
/** The URL to connect to MongoDB or false to start payload and prevent connecting */
|
||||
url: false | string
|
||||
@@ -57,12 +101,22 @@ export interface Args {
|
||||
|
||||
export type MongooseAdapter = BaseDatabaseAdapter &
|
||||
Args & {
|
||||
collectionOptions: {
|
||||
[slug: string]: {
|
||||
schemaOptions?: SchemaOptions
|
||||
}
|
||||
}
|
||||
collections: {
|
||||
[slug: string]: CollectionModel
|
||||
}
|
||||
connection: Connection
|
||||
globals: GlobalModel
|
||||
globalsOptions: {
|
||||
schemaOptions?: SchemaOptions
|
||||
}
|
||||
jsonParse: boolean
|
||||
mongoMemoryServer: any
|
||||
schemaOptions?: SchemaOptions
|
||||
sessions: Record<number | string, ClientSession>
|
||||
versions: {
|
||||
[slug: string]: CollectionModel
|
||||
@@ -74,13 +128,24 @@ type MongooseAdapterResult = (args: { payload: Payload }) => MongooseAdapter
|
||||
declare module 'payload' {
|
||||
export interface DatabaseAdapter
|
||||
extends Omit<BaseDatabaseAdapter, 'sessions'>,
|
||||
Omit<Args, 'migrationDir'> {
|
||||
Omit<Args, 'collections' | 'globals' | 'migrationDir'> {
|
||||
collectionOptions: {
|
||||
[slug: string]: {
|
||||
schemaOptions?: SchemaOptions
|
||||
}
|
||||
}
|
||||
collections: {
|
||||
[slug: string]: CollectionModel
|
||||
}
|
||||
connection: Connection
|
||||
globals: GlobalModel
|
||||
globalsOptions: {
|
||||
schemaOptions?: SchemaOptions
|
||||
}
|
||||
jsonParse: boolean
|
||||
mongoMemoryServer: any
|
||||
schemaOptions?: SchemaOptions
|
||||
|
||||
sessions: Record<number | string, ClientSession>
|
||||
transactionOptions: TransactionOptions
|
||||
versions: {
|
||||
@@ -91,9 +156,13 @@ declare module 'payload' {
|
||||
|
||||
export function mongooseAdapter({
|
||||
autoPluralization = true,
|
||||
collections,
|
||||
connectOptions,
|
||||
disableIndexHints = false,
|
||||
globals,
|
||||
jsonParse = true,
|
||||
migrationDir: migrationDirArg,
|
||||
schemaOptions,
|
||||
transactionOptions = {},
|
||||
url,
|
||||
}: Args): MongooseAdapterResult {
|
||||
@@ -106,17 +175,22 @@ export function mongooseAdapter({
|
||||
|
||||
// Mongoose-specific
|
||||
autoPluralization,
|
||||
collectionOptions: collections || {},
|
||||
collections: {},
|
||||
connectOptions: connectOptions || {},
|
||||
connection: undefined,
|
||||
count,
|
||||
disableIndexHints,
|
||||
globals: undefined,
|
||||
globalsOptions: globals || {},
|
||||
jsonParse,
|
||||
mongoMemoryServer: undefined,
|
||||
schemaOptions: schemaOptions || {},
|
||||
sessions: {},
|
||||
transactionOptions: transactionOptions === false ? undefined : transactionOptions,
|
||||
url,
|
||||
versions: {},
|
||||
|
||||
// DatabaseAdapter
|
||||
beginTransaction: transactionOptions ? beginTransaction : undefined,
|
||||
commitTransaction,
|
||||
|
||||
@@ -19,20 +19,22 @@ import { getDBName } from './utilities/getDBName'
|
||||
|
||||
export const init: Init = async function init(this: MongooseAdapter) {
|
||||
this.payload.config.collections.forEach((collection: SanitizedCollectionConfig) => {
|
||||
const schema = buildCollectionSchema(collection, this.payload.config)
|
||||
const schema = buildCollectionSchema(collection, this)
|
||||
|
||||
if (collection.versions) {
|
||||
const versionModelName = getDBName({ config: collection, versions: true })
|
||||
|
||||
const versionCollectionFields = buildVersionCollectionFields(collection)
|
||||
|
||||
const versionSchema = buildSchema(this.payload.config, versionCollectionFields, {
|
||||
const versionSchema = buildSchema(this, versionCollectionFields, {
|
||||
disableUnique: true,
|
||||
draftsEnabled: true,
|
||||
indexSortableFields: this.payload.config.indexSortableFields,
|
||||
options: {
|
||||
minimize: false,
|
||||
timestamps: false,
|
||||
...this.schemaOptions,
|
||||
...(this.collectionOptions[collection.slug]?.schemaOptions || {}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -69,7 +71,7 @@ export const init: Init = async function init(this: MongooseAdapter) {
|
||||
}
|
||||
})
|
||||
|
||||
const model = buildGlobalModel(this.payload.config)
|
||||
const model = buildGlobalModel(this)
|
||||
this.globals = model
|
||||
|
||||
this.payload.config.globals.forEach((global) => {
|
||||
@@ -78,13 +80,15 @@ export const init: Init = async function init(this: MongooseAdapter) {
|
||||
|
||||
const versionGlobalFields = buildVersionGlobalFields(global)
|
||||
|
||||
const versionSchema = buildSchema(this.payload.config, versionGlobalFields, {
|
||||
const versionSchema = buildSchema(this, versionGlobalFields, {
|
||||
disableUnique: true,
|
||||
draftsEnabled: true,
|
||||
indexSortableFields: this.payload.config.indexSortableFields,
|
||||
options: {
|
||||
minimize: false,
|
||||
timestamps: false,
|
||||
...this.schemaOptions,
|
||||
...(this.globalsOptions.schemaOptions || {}),
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import type { PaginateOptions, Schema } from 'mongoose'
|
||||
import type { SanitizedConfig } from 'payload/config'
|
||||
import type { SanitizedCollectionConfig } from 'payload/types'
|
||||
|
||||
import paginate from 'mongoose-paginate-v2'
|
||||
|
||||
import type { MongooseAdapter } from '..'
|
||||
|
||||
import getBuildQueryPlugin from '../queries/buildQuery'
|
||||
import buildSchema from './buildSchema'
|
||||
|
||||
const buildCollectionSchema = (
|
||||
collection: SanitizedCollectionConfig,
|
||||
config: SanitizedConfig,
|
||||
schemaOptions = {},
|
||||
adapter: MongooseAdapter,
|
||||
): Schema => {
|
||||
const schema = buildSchema(config, collection.fields, {
|
||||
const schema = buildSchema(adapter, collection.fields, {
|
||||
draftsEnabled: Boolean(typeof collection?.versions === 'object' && collection.versions.drafts),
|
||||
indexSortableFields: config.indexSortableFields,
|
||||
indexSortableFields: adapter.payload.config.indexSortableFields,
|
||||
options: {
|
||||
minimize: false,
|
||||
timestamps: collection.timestamps !== false,
|
||||
...schemaOptions,
|
||||
...adapter.schemaOptions,
|
||||
...(adapter.collectionOptions[collection.slug]?.schemaOptions || {}),
|
||||
},
|
||||
})
|
||||
|
||||
if (config.indexSortableFields && collection.timestamps !== false) {
|
||||
if (adapter.payload.config.indexSortableFields && collection.timestamps !== false) {
|
||||
schema.index({ updatedAt: 1 })
|
||||
schema.index({ createdAt: 1 })
|
||||
}
|
||||
|
||||
@@ -1,27 +1,34 @@
|
||||
import type { SanitizedConfig } from 'payload/config'
|
||||
|
||||
import mongoose from 'mongoose'
|
||||
|
||||
import type { MongooseAdapter } from '..'
|
||||
import type { GlobalModel } from '../types'
|
||||
|
||||
import getBuildQueryPlugin from '../queries/buildQuery'
|
||||
import buildSchema from './buildSchema'
|
||||
|
||||
export const buildGlobalModel = (config: SanitizedConfig): GlobalModel | null => {
|
||||
if (config.globals && config.globals.length > 0) {
|
||||
export const buildGlobalModel = (adapter: MongooseAdapter): GlobalModel | null => {
|
||||
if (adapter.payload.config.globals && adapter.payload.config.globals.length > 0) {
|
||||
const globalsSchema = new mongoose.Schema(
|
||||
{},
|
||||
{ discriminatorKey: 'globalType', minimize: false, timestamps: true },
|
||||
{
|
||||
discriminatorKey: 'globalType',
|
||||
minimize: false,
|
||||
...adapter.schemaOptions,
|
||||
...(adapter.globalsOptions.schemaOptions || {}),
|
||||
timestamps: true,
|
||||
},
|
||||
)
|
||||
|
||||
globalsSchema.plugin(getBuildQueryPlugin())
|
||||
|
||||
const Globals = mongoose.model('globals', globalsSchema, 'globals') as unknown as GlobalModel
|
||||
|
||||
Object.values(config.globals).forEach((globalConfig) => {
|
||||
const globalSchema = buildSchema(config, globalConfig.fields, {
|
||||
Object.values(adapter.payload.config.globals).forEach((globalConfig) => {
|
||||
const globalSchema = buildSchema(adapter, globalConfig.fields, {
|
||||
options: {
|
||||
minimize: false,
|
||||
...adapter.schemaOptions,
|
||||
...(adapter.globalsOptions.schemaOptions || {}),
|
||||
},
|
||||
})
|
||||
Globals.discriminator(globalConfig.slug, globalSchema)
|
||||
|
||||
@@ -40,6 +40,8 @@ import {
|
||||
tabHasName,
|
||||
} from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '..'
|
||||
|
||||
export type BuildSchemaOptions = {
|
||||
allowIDField?: boolean
|
||||
disableUnique?: boolean
|
||||
@@ -51,7 +53,7 @@ export type BuildSchemaOptions = {
|
||||
type FieldSchemaGenerator = (
|
||||
field: Field,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
) => void
|
||||
|
||||
@@ -90,10 +92,10 @@ const localizeSchema = (
|
||||
if (fieldIsLocalized(entity) && localization && Array.isArray(localization.locales)) {
|
||||
return {
|
||||
type: localization.localeCodes.reduce(
|
||||
(localeSchema, locale) => ({
|
||||
...localeSchema,
|
||||
[locale]: schema,
|
||||
}),
|
||||
(localeSchema, locale) => {
|
||||
localeSchema[locale] = schema
|
||||
return localeSchema
|
||||
},
|
||||
{
|
||||
_id: false,
|
||||
},
|
||||
@@ -105,7 +107,7 @@ const localizeSchema = (
|
||||
}
|
||||
|
||||
const buildSchema = (
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
configFields: Field[],
|
||||
buildSchemaOptions: BuildSchemaOptions = {},
|
||||
): Schema => {
|
||||
@@ -133,7 +135,7 @@ const buildSchema = (
|
||||
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[field.type]
|
||||
|
||||
if (addFieldSchema) {
|
||||
addFieldSchema(field, schema, config, buildSchemaOptions)
|
||||
addFieldSchema(field, schema, adapter, buildSchemaOptions)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -145,20 +147,22 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
array: (
|
||||
field: ArrayField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
) => {
|
||||
const baseSchema = {
|
||||
...formatBaseSchema(field, buildSchemaOptions),
|
||||
type: [
|
||||
buildSchema(config, field.fields, {
|
||||
buildSchema(adapter, field.fields, {
|
||||
allowIDField: true,
|
||||
disableUnique: buildSchemaOptions.disableUnique,
|
||||
draftsEnabled: buildSchemaOptions.draftsEnabled,
|
||||
options: {
|
||||
minimize: false,
|
||||
...(buildSchemaOptions.options || {}),
|
||||
_id: false,
|
||||
id: false,
|
||||
minimize: false,
|
||||
timestamps: false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -166,36 +170,54 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
|
||||
})
|
||||
},
|
||||
blocks: (
|
||||
field: BlockField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const fieldSchema = {
|
||||
type: [new Schema({}, { _id: false, discriminatorKey: 'blockType' })],
|
||||
type: [
|
||||
new Schema(
|
||||
{},
|
||||
{
|
||||
_id: false,
|
||||
discriminatorKey: 'blockType',
|
||||
...(buildSchemaOptions.options || {}),
|
||||
timestamps: false,
|
||||
},
|
||||
),
|
||||
],
|
||||
default: undefined,
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, fieldSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, fieldSchema, adapter.payload.config.localization),
|
||||
})
|
||||
|
||||
field.blocks.forEach((blockItem: Block) => {
|
||||
const blockSchema = new Schema({}, { _id: false, id: false })
|
||||
const blockSchema = new Schema(
|
||||
{},
|
||||
{
|
||||
...(buildSchemaOptions.options || {}),
|
||||
_id: false,
|
||||
id: false,
|
||||
timestamps: false,
|
||||
},
|
||||
)
|
||||
|
||||
blockItem.fields.forEach((blockField) => {
|
||||
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[blockField.type]
|
||||
if (addFieldSchema) {
|
||||
addFieldSchema(blockField, blockSchema, config, buildSchemaOptions)
|
||||
addFieldSchema(blockField, blockSchema, adapter, buildSchemaOptions)
|
||||
}
|
||||
})
|
||||
|
||||
if (field.localized && config.localization) {
|
||||
config.localization.localeCodes.forEach((localeCode) => {
|
||||
if (field.localized && adapter.payload.config.localization) {
|
||||
adapter.payload.config.localization.localeCodes.forEach((localeCode) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error Possible incorrect typing in mongoose types, this works
|
||||
schema.path(`${field.name}.${localeCode}`).discriminator(blockItem.slug, blockSchema)
|
||||
@@ -210,69 +232,69 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
checkbox: (
|
||||
field: CheckboxField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Boolean }
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
|
||||
})
|
||||
},
|
||||
code: (
|
||||
field: CodeField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
|
||||
})
|
||||
},
|
||||
collapsible: (
|
||||
field: CollapsibleField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
field.fields.forEach((subField: Field) => {
|
||||
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
|
||||
|
||||
if (addFieldSchema) {
|
||||
addFieldSchema(subField, schema, config, buildSchemaOptions)
|
||||
addFieldSchema(subField, schema, adapter, buildSchemaOptions)
|
||||
}
|
||||
})
|
||||
},
|
||||
date: (
|
||||
field: DateField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Date }
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
|
||||
})
|
||||
},
|
||||
email: (
|
||||
field: EmailField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
|
||||
})
|
||||
},
|
||||
group: (
|
||||
field: GroupField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const formattedBaseSchema = formatBaseSchema(field, buildSchemaOptions)
|
||||
@@ -285,38 +307,40 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
|
||||
const baseSchema = {
|
||||
...formattedBaseSchema,
|
||||
type: buildSchema(config, field.fields, {
|
||||
type: buildSchema(adapter, field.fields, {
|
||||
disableUnique: buildSchemaOptions.disableUnique,
|
||||
draftsEnabled: buildSchemaOptions.draftsEnabled,
|
||||
indexSortableFields,
|
||||
options: {
|
||||
minimize: false,
|
||||
...(buildSchemaOptions.options || {}),
|
||||
_id: false,
|
||||
id: false,
|
||||
minimize: false,
|
||||
timestamps: false,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
|
||||
})
|
||||
},
|
||||
json: (
|
||||
field: JSONField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Schema.Types.Mixed }
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
|
||||
})
|
||||
},
|
||||
number: (
|
||||
field: NumberField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = {
|
||||
@@ -325,13 +349,13 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
|
||||
})
|
||||
},
|
||||
point: (
|
||||
field: PointField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema: SchemaTypeOptions<unknown> = {
|
||||
@@ -350,7 +374,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
|
||||
})
|
||||
|
||||
if (field.index === true || field.index === undefined) {
|
||||
@@ -359,8 +383,8 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
indexOptions.sparse = true
|
||||
indexOptions.unique = true
|
||||
}
|
||||
if (field.localized && config.localization) {
|
||||
config.localization.locales.forEach((locale) => {
|
||||
if (field.localized && adapter.payload.config.localization) {
|
||||
adapter.payload.config.localization.locales.forEach((locale) => {
|
||||
schema.index({ [`${field.name}.${locale.code}`]: '2dsphere' }, indexOptions)
|
||||
})
|
||||
} else {
|
||||
@@ -371,7 +395,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
radio: (
|
||||
field: RadioField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = {
|
||||
@@ -384,21 +408,21 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
|
||||
})
|
||||
},
|
||||
relationship: (
|
||||
field: RelationshipField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
) => {
|
||||
const hasManyRelations = Array.isArray(field.relationTo)
|
||||
let schemaToReturn: { [key: string]: any } = {}
|
||||
|
||||
if (field.localized && config.localization) {
|
||||
if (field.localized && adapter.payload.config.localization) {
|
||||
schemaToReturn = {
|
||||
type: config.localization.localeCodes.reduce((locales, locale) => {
|
||||
type: adapter.payload.config.localization.localeCodes.reduce((locales, locale) => {
|
||||
let localeSchema: { [key: string]: any } = {}
|
||||
|
||||
if (hasManyRelations) {
|
||||
@@ -467,33 +491,33 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
richText: (
|
||||
field: RichTextField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: Schema.Types.Mixed }
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
|
||||
})
|
||||
},
|
||||
row: (
|
||||
field: RowField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
field.fields.forEach((subField: Field) => {
|
||||
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
|
||||
|
||||
if (addFieldSchema) {
|
||||
addFieldSchema(subField, schema, config, buildSchemaOptions)
|
||||
addFieldSchema(subField, schema, adapter, buildSchemaOptions)
|
||||
}
|
||||
})
|
||||
},
|
||||
select: (
|
||||
field: SelectField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = {
|
||||
@@ -513,39 +537,41 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
[field.name]: localizeSchema(
|
||||
field,
|
||||
field.hasMany ? [baseSchema] : baseSchema,
|
||||
config.localization,
|
||||
adapter.payload.config.localization,
|
||||
),
|
||||
})
|
||||
},
|
||||
tabs: (
|
||||
field: TabsField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
field.tabs.forEach((tab) => {
|
||||
if (tabHasName(tab)) {
|
||||
const baseSchema = {
|
||||
type: buildSchema(config, tab.fields, {
|
||||
type: buildSchema(adapter, tab.fields, {
|
||||
disableUnique: buildSchemaOptions.disableUnique,
|
||||
draftsEnabled: buildSchemaOptions.draftsEnabled,
|
||||
options: {
|
||||
minimize: false,
|
||||
...(buildSchemaOptions.options || {}),
|
||||
_id: false,
|
||||
id: false,
|
||||
minimize: false,
|
||||
timestamps: false,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[tab.name]: localizeSchema(tab, baseSchema, config.localization),
|
||||
[tab.name]: localizeSchema(tab, baseSchema, adapter.payload.config.localization),
|
||||
})
|
||||
} else {
|
||||
tab.fields.forEach((subField: Field) => {
|
||||
const addFieldSchema: FieldSchemaGenerator = fieldToSchemaMap[subField.type]
|
||||
|
||||
if (addFieldSchema) {
|
||||
addFieldSchema(subField, schema, config, buildSchemaOptions)
|
||||
addFieldSchema(subField, schema, adapter, buildSchemaOptions)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -554,7 +580,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
text: (
|
||||
field: TextField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = {
|
||||
@@ -563,25 +589,25 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
|
||||
})
|
||||
},
|
||||
textarea: (
|
||||
field: TextareaField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = { ...formatBaseSchema(field, buildSchemaOptions), type: String }
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
|
||||
})
|
||||
},
|
||||
upload: (
|
||||
field: UploadField,
|
||||
schema: Schema,
|
||||
config: SanitizedConfig,
|
||||
adapter: MongooseAdapter,
|
||||
buildSchemaOptions: BuildSchemaOptions,
|
||||
): void => {
|
||||
const baseSchema = {
|
||||
@@ -591,7 +617,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
|
||||
}
|
||||
|
||||
schema.add({
|
||||
[field.name]: localizeSchema(field, baseSchema, config.localization),
|
||||
[field.name]: localizeSchema(field, baseSchema, adapter.payload.config.localization),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -58,6 +58,14 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
useEstimatedCount,
|
||||
}
|
||||
|
||||
if (this.collation) {
|
||||
const defaultLocale = 'en'
|
||||
paginationOptions.collation = {
|
||||
locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
|
||||
...this.collation,
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!useEstimatedCount &&
|
||||
Object.keys(versionQuery).length === 0 &&
|
||||
@@ -83,12 +91,14 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
||||
}
|
||||
|
||||
const result = await VersionModel.paginate(versionQuery, paginationOptions)
|
||||
const docs = JSON.parse(JSON.stringify(result.docs))
|
||||
|
||||
const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs
|
||||
|
||||
return {
|
||||
...result,
|
||||
docs: docs.map((doc) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
|
||||
doc = {
|
||||
_id: doc.parent,
|
||||
id: doc.parent,
|
||||
|
||||
@@ -18,12 +18,11 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal(
|
||||
}
|
||||
|
||||
let result
|
||||
|
||||
result = await Model.findOneAndUpdate({ globalType: slug }, data, options)
|
||||
|
||||
result = JSON.parse(JSON.stringify(result))
|
||||
result = this.jsonParse ? JSON.parse(JSON.stringify(result)) : result
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
|
||||
return result
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { PayloadRequest, TypeWithID } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
|
||||
import { withSession } from './withSession'
|
||||
|
||||
export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
@@ -30,16 +31,14 @@ export async function updateGlobalVersion<T extends TypeWithID>(
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
|
||||
let doc = await VersionModel.findOneAndUpdate(query, versionData, options)
|
||||
|
||||
const result = JSON.parse(JSON.stringify(doc))
|
||||
doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
doc._verificationToken = verificationToken
|
||||
}
|
||||
return result
|
||||
return sanitizeInternalFields(doc)
|
||||
}
|
||||
|
||||
@@ -32,9 +32,7 @@ export const updateOne: UpdateOne = async function updateOne(
|
||||
handleError(error, req)
|
||||
}
|
||||
|
||||
result = JSON.parse(JSON.stringify(result))
|
||||
result.id = result._id
|
||||
result = sanitizeInternalFields(result)
|
||||
result = this.jsonParse ? JSON.parse(JSON.stringify(result)) : result
|
||||
|
||||
return result
|
||||
return sanitizeInternalFields(result)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { PayloadRequest } from 'payload/types'
|
||||
|
||||
import type { MongooseAdapter } from '.'
|
||||
|
||||
import sanitizeInternalFields from './utilities/sanitizeInternalFields'
|
||||
import { withSession } from './withSession'
|
||||
|
||||
export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
@@ -23,16 +24,14 @@ export const updateVersion: UpdateVersion = async function updateVersion(
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
|
||||
|
||||
const result = JSON.parse(JSON.stringify(doc))
|
||||
let doc = await VersionModel.findOneAndUpdate(query, versionData, options)
|
||||
|
||||
const verificationToken = doc._verificationToken
|
||||
|
||||
// custom id type reset
|
||||
result.id = result._id
|
||||
doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc
|
||||
|
||||
if (verificationToken) {
|
||||
result._verificationToken = verificationToken
|
||||
doc._verificationToken = verificationToken
|
||||
}
|
||||
return result
|
||||
return sanitizeInternalFields(doc)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
const internalFields = ['__v']
|
||||
|
||||
const sanitizeInternalFields = <T extends Record<string, unknown>>(incomingDoc: T): T =>
|
||||
Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => {
|
||||
if (key === '_id') {
|
||||
const sanitizeInternalFields = <T extends Record<string, unknown>>(incomingDoc: T): T => {
|
||||
const id = incomingDoc._id ? JSON.parse(JSON.stringify(incomingDoc._id)) : incomingDoc.id
|
||||
|
||||
return Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => {
|
||||
if (key === '_id' || key === 'id') {
|
||||
return {
|
||||
...newDoc,
|
||||
id: val,
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,5 +20,6 @@ const sanitizeInternalFields = <T extends Record<string, unknown>>(incomingDoc:
|
||||
[key]: val,
|
||||
}
|
||||
}, {} as T)
|
||||
}
|
||||
|
||||
export default sanitizeInternalFields
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
"dependencies": {
|
||||
"@libsql/client": "^0.3.1",
|
||||
"console-table-printer": "2.11.2",
|
||||
"drizzle-kit": "0.20.14-1f2c838",
|
||||
"drizzle-orm": "0.29.3",
|
||||
"drizzle-kit": "0.23.1-7816536",
|
||||
"drizzle-orm": "0.32.1",
|
||||
"pg": "8.11.3",
|
||||
"prompts": "2.4.2",
|
||||
"to-snake-case": "1.0.0",
|
||||
|
||||
@@ -48,6 +48,7 @@ const connectWithReconnect = async function ({
|
||||
|
||||
export const connect: Connect = async function connect(this: PostgresAdapter, payload) {
|
||||
this.schema = {
|
||||
pgSchema: this.pgSchema,
|
||||
...this.tables,
|
||||
...this.relations,
|
||||
...this.enums,
|
||||
@@ -89,6 +90,7 @@ export const connect: Connect = async function connect(this: PostgresAdapter, pa
|
||||
const { apply, hasDataLoss, statementsToExecute, warnings } = await pushSchema(
|
||||
this.schema,
|
||||
this.drizzle,
|
||||
this.schemaName ? [this.schemaName] : undefined,
|
||||
)
|
||||
|
||||
if (warnings.length) {
|
||||
|
||||
@@ -46,9 +46,10 @@ const getDefaultDrizzleSnapshot = (): DrizzleSnapshotJSON => ({
|
||||
dialect: 'pg',
|
||||
enums: {},
|
||||
prevId: '00000000-0000-0000-0000-00000000000',
|
||||
sequences: {},
|
||||
schemas: {},
|
||||
tables: {},
|
||||
version: '5',
|
||||
version: '7',
|
||||
})
|
||||
|
||||
export const createMigration: CreateMigration = async function createMigration(
|
||||
@@ -76,6 +77,12 @@ export const createMigration: CreateMigration = async function createMigration(
|
||||
|
||||
let drizzleJsonBefore = getDefaultDrizzleSnapshot()
|
||||
|
||||
if (this.schemaName) {
|
||||
drizzleJsonBefore.schemas = {
|
||||
[this.schemaName]: this.schemaName,
|
||||
}
|
||||
}
|
||||
|
||||
// Get latest migration snapshot
|
||||
const latestSnapshot = fs
|
||||
.readdirSync(dir)
|
||||
|
||||
@@ -37,12 +37,20 @@ import { updateOne } from './update'
|
||||
import { updateGlobal } from './updateGlobal'
|
||||
import { updateGlobalVersion } from './updateGlobalVersion'
|
||||
import { updateVersion } from './updateVersion'
|
||||
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
|
||||
|
||||
export type { MigrateDownArgs, MigrateUpArgs } from './types'
|
||||
|
||||
export function postgresAdapter(args: Args): PostgresAdapterResult {
|
||||
const postgresIDType = args.idType || 'serial'
|
||||
const payloadIDType = postgresIDType === 'serial' ? 'number' : 'text'
|
||||
let adapterSchema: PostgresAdapter['pgSchema']
|
||||
|
||||
if (args.schemaName) {
|
||||
adapterSchema = pgSchema(args.schemaName)
|
||||
} else {
|
||||
adapterSchema = { enum: pgEnum, table: pgTable }
|
||||
}
|
||||
|
||||
function adapter({ payload }: { payload: Payload }) {
|
||||
const migrationDir = findMigrationDir(args.migrationDir)
|
||||
@@ -55,7 +63,7 @@ export function postgresAdapter(args: Args): PostgresAdapterResult {
|
||||
idType: postgresIDType,
|
||||
localesSuffix: args.localesSuffix || '_locales',
|
||||
logger: args.logger,
|
||||
pgSchema: undefined,
|
||||
pgSchema: adapterSchema,
|
||||
pool: undefined,
|
||||
poolOptions: args.pool,
|
||||
push: args.push,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import type { Init } from 'payload/database'
|
||||
import type { SanitizedCollectionConfig } from 'payload/types'
|
||||
|
||||
import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
|
||||
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload/versions'
|
||||
|
||||
import type { PostgresAdapter } from './types'
|
||||
@@ -11,14 +10,8 @@ import { buildTable } from './schema/build'
|
||||
import { createTableName } from './schema/createTableName'
|
||||
|
||||
export const init: Init = async function init(this: PostgresAdapter) {
|
||||
if (this.schemaName) {
|
||||
this.pgSchema = pgSchema(this.schemaName)
|
||||
} else {
|
||||
this.pgSchema = { table: pgTable }
|
||||
}
|
||||
|
||||
if (this.payload.config.localization) {
|
||||
this.enums.enum__locales = pgEnum(
|
||||
this.enums.enum__locales = this.pgSchema.enum(
|
||||
'_locales',
|
||||
this.payload.config.localization.locales.map(({ code }) => code) as [string, ...string[]],
|
||||
)
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
integer,
|
||||
jsonb,
|
||||
numeric,
|
||||
pgEnum,
|
||||
text,
|
||||
timestamp,
|
||||
varchar,
|
||||
@@ -232,7 +231,7 @@ export const traverseFields = ({
|
||||
throwValidationError,
|
||||
})
|
||||
|
||||
adapter.enums[enumName] = pgEnum(
|
||||
adapter.enums[enumName] = adapter.pgSchema.enum(
|
||||
enumName,
|
||||
field.options.map((option) => {
|
||||
if (optionIsObject(option)) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
PgSchema,
|
||||
PgTableWithColumns,
|
||||
PgTransaction,
|
||||
pgEnum,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import type { PgTableFn } from 'drizzle-orm/pg-core/table'
|
||||
import type { Payload } from 'payload'
|
||||
@@ -60,6 +61,13 @@ export type DrizzleTransaction = PgTransaction<
|
||||
ExtractTablesWithRelations<Record<string, unknown>>
|
||||
>
|
||||
|
||||
type Schema =
|
||||
| {
|
||||
enum: typeof pgEnum
|
||||
table: PgTableFn
|
||||
}
|
||||
| PgSchema
|
||||
|
||||
export type PostgresAdapter = BaseDatabaseAdapter & {
|
||||
drizzle: DrizzleDB
|
||||
enums: Record<string, GenericEnum>
|
||||
@@ -71,13 +79,13 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
|
||||
idType: Args['idType']
|
||||
localesSuffix?: string
|
||||
logger: DrizzleConfig['logger']
|
||||
pgSchema?: { table: PgTableFn } | PgSchema
|
||||
pgSchema: Schema
|
||||
pool: Pool
|
||||
poolOptions: Args['pool']
|
||||
push: boolean
|
||||
relations: Record<string, GenericRelation>
|
||||
relationshipsSuffix?: string
|
||||
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
|
||||
schema: Record<string, unknown>
|
||||
schemaName?: Args['schemaName']
|
||||
sessions: {
|
||||
[id: string]: {
|
||||
@@ -116,7 +124,7 @@ declare module 'payload' {
|
||||
push: boolean
|
||||
relations: Record<string, GenericRelation>
|
||||
relationshipsSuffix?: string
|
||||
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
|
||||
schema: Record<string, unknown>
|
||||
sessions: {
|
||||
[id: string]: {
|
||||
db: DrizzleTransaction
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"author": "Payload CMS, Inc.",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"author": "Payload CMS, Inc.",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"author": "Payload CMS, Inc.",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "pnpm copyfiles && pnpm build:swc && pnpm build:types",
|
||||
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "2.23.0",
|
||||
"version": "2.25.0",
|
||||
"description": "Node, React and MongoDB Headless CMS and Application Framework",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -17,11 +17,12 @@ import reduceFieldsToValues from '../../forms/Form/reduceFieldsToValues'
|
||||
import { reduceFieldsToValuesWithValidation } from '../../forms/Form/reduceFieldsToValuesWithValidation'
|
||||
import { useConfig } from '../../utilities/Config'
|
||||
import { useDocumentInfo } from '../../utilities/DocumentInfo'
|
||||
import { useEditDepth } from '../../utilities/EditDepth'
|
||||
import { useLocale } from '../../utilities/Locale'
|
||||
import './index.scss'
|
||||
const baseClass = 'autosave'
|
||||
|
||||
const Autosave: React.FC<Props> = ({ id, collection, global, publishedDocUpdatedAt }) => {
|
||||
const Autosave: React.FC<Props> = ({ id, collection, global, onSave, publishedDocUpdatedAt }) => {
|
||||
const {
|
||||
routes: { admin, api },
|
||||
serverURL,
|
||||
@@ -34,6 +35,7 @@ const Autosave: React.FC<Props> = ({ id, collection, global, publishedDocUpdated
|
||||
const { dispatchFields, setSubmitted } = useForm()
|
||||
const history = useHistory()
|
||||
const { i18n, t } = useTranslation('version')
|
||||
const depth = useEditDepth()
|
||||
|
||||
let interval = 800
|
||||
const validateDrafts =
|
||||
@@ -77,15 +79,19 @@ const Autosave: React.FC<Props> = ({ id, collection, global, publishedDocUpdated
|
||||
|
||||
if (res.status === 201) {
|
||||
const json = await res.json()
|
||||
history.replace(`${admin}/collections/${collection.slug}/${json.doc.id}`, {
|
||||
state: {
|
||||
data: json.doc,
|
||||
},
|
||||
})
|
||||
if (depth === 1) {
|
||||
history.replace(`${admin}/collections/${collection.slug}/${json.doc.id}`, {
|
||||
state: {
|
||||
data: json.doc,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
onSave(json)
|
||||
}
|
||||
} else {
|
||||
toast.error(t('error:autosaving'))
|
||||
}
|
||||
}, [serverURL, api, collection?.slug, locale, i18n.language, history, admin, t])
|
||||
}, [serverURL, api, collection?.slug, locale, i18n.language, history, admin, t, depth, onSave])
|
||||
|
||||
useEffect(() => {
|
||||
// If no ID, but this is used for a collection doc,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { SanitizedCollectionConfig } from '../../../../collections/config/types'
|
||||
import type { SanitizedGlobalConfig } from '../../../../globals/config/types'
|
||||
import type { CollectionEditViewProps } from '../../views/types'
|
||||
|
||||
export type Props = {
|
||||
collection?: SanitizedCollectionConfig
|
||||
global?: SanitizedGlobalConfig
|
||||
id?: number | string
|
||||
onSave?: CollectionEditViewProps['onSave']
|
||||
publishedDocUpdatedAt: string
|
||||
}
|
||||
|
||||
@@ -54,9 +54,12 @@ const DateTime: React.FC<Props> = (props) => {
|
||||
|
||||
const onChange = (incomingDate: Date) => {
|
||||
const newDate = incomingDate
|
||||
if (newDate instanceof Date && ['dayOnly', 'default', 'monthOnly'].includes(pickerAppearance)) {
|
||||
const tzOffset = incomingDate.getTimezoneOffset() / 60
|
||||
newDate.setHours(12 - tzOffset, 0)
|
||||
if (newDate instanceof Date) {
|
||||
newDate.setMilliseconds(0)
|
||||
if (['dayOnly', 'default', 'monthOnly'].includes(pickerAppearance)) {
|
||||
const tzOffset = incomingDate.getTimezoneOffset() / 60
|
||||
newDate.setHours(12 - tzOffset, 0)
|
||||
}
|
||||
}
|
||||
if (typeof onChangeFromProps === 'function') onChangeFromProps(newDate)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import type { CollectionPermission, GlobalPermission } from '../../../../auth'
|
||||
import type { SanitizedCollectionConfig } from '../../../../collections/config/types'
|
||||
import type { SanitizedGlobalConfig } from '../../../../globals/config/types'
|
||||
import type { CollectionEditViewProps } from '../../views/types'
|
||||
|
||||
import { getTranslation } from '../../../../utilities/getTranslation'
|
||||
import { formatDate } from '../../../utilities/formatDate'
|
||||
@@ -34,6 +35,7 @@ export const DocumentControls: React.FC<{
|
||||
id?: string
|
||||
isAccountView?: boolean
|
||||
isEditing?: boolean
|
||||
onSave?: CollectionEditViewProps['onSave']
|
||||
permissions?: CollectionPermission | GlobalPermission
|
||||
}> = (props) => {
|
||||
const {
|
||||
@@ -45,6 +47,7 @@ export const DocumentControls: React.FC<{
|
||||
hasSavePermission,
|
||||
isAccountView,
|
||||
isEditing,
|
||||
onSave,
|
||||
permissions,
|
||||
} = props
|
||||
|
||||
@@ -111,6 +114,7 @@ export const DocumentControls: React.FC<{
|
||||
collection={collection}
|
||||
global={global}
|
||||
id={id}
|
||||
onSave={onSave}
|
||||
publishedDocUpdatedAt={publishedDoc?.updatedAt || data?.createdAt}
|
||||
/>
|
||||
</li>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '../Button'
|
||||
|
||||
import Button from '../Button'
|
||||
import './index.scss'
|
||||
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
@@ -12,12 +12,13 @@ const handleDragOver = (e: DragEvent) => {
|
||||
const baseClass = 'dropzone'
|
||||
|
||||
type Props = {
|
||||
onChange: (e: FileList) => void
|
||||
className?: string
|
||||
mimeTypes?: string[]
|
||||
onChange: (e: FileList) => void
|
||||
onPasteUrlClick?: () => void
|
||||
}
|
||||
|
||||
export const Dropzone: React.FC<Props> = ({ onChange, className, mimeTypes }) => {
|
||||
export const Dropzone: React.FC<Props> = ({ className, mimeTypes, onChange, onPasteUrlClick }) => {
|
||||
const dropRef = React.useRef<HTMLDivElement>(null)
|
||||
const [dragging, setDragging] = React.useState(false)
|
||||
const inputRef = React.useRef(null)
|
||||
@@ -98,24 +99,31 @@ export const Dropzone: React.FC<Props> = ({ onChange, className, mimeTypes }) =>
|
||||
const classes = [baseClass, className, dragging ? 'dragging' : ''].filter(Boolean).join(' ')
|
||||
|
||||
return (
|
||||
<div ref={dropRef} className={classes}>
|
||||
<div className={classes} ref={dropRef}>
|
||||
<Button
|
||||
size="small"
|
||||
buttonStyle="secondary"
|
||||
className={`${baseClass}__file-button`}
|
||||
onClick={() => {
|
||||
inputRef.current.click()
|
||||
}}
|
||||
className={`${baseClass}__file-button`}
|
||||
size="small"
|
||||
>
|
||||
{t('selectFile')}
|
||||
{t('upload:selectFile')}
|
||||
</Button>
|
||||
<Button
|
||||
buttonStyle="secondary"
|
||||
className={`${baseClass}__file-button`}
|
||||
onClick={onPasteUrlClick}
|
||||
size="small"
|
||||
>
|
||||
{t('upload:pasteURL')}
|
||||
</Button>
|
||||
|
||||
<input
|
||||
accept={mimeTypes?.join(',')}
|
||||
className={`${baseClass}__hidden-input`}
|
||||
onChange={handleFileSelection}
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept={mimeTypes?.join(',')}
|
||||
onChange={handleFileSelection}
|
||||
className={`${baseClass}__hidden-input`}
|
||||
/>
|
||||
|
||||
<p className={`${baseClass}__label`}>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useModal } from '@faceless-ui/modal'
|
||||
import React, { forwardRef, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactCrop, { type Crop as CropType } from 'react-image-crop'
|
||||
import ReactCrop from 'react-image-crop'
|
||||
import 'react-image-crop/dist/ReactCrop.css'
|
||||
|
||||
import type { Data } from '../../forms/Form/types'
|
||||
import type { UploadEdits } from '../../../../uploads/types'
|
||||
|
||||
import Plus from '../../icons/Plus'
|
||||
import { useUploadEdits } from '../../utilities/UploadEdits'
|
||||
import { editDrawerSlug } from '../../views/collections/Edit/Upload'
|
||||
import Button from '../Button'
|
||||
import './index.scss'
|
||||
@@ -37,58 +36,85 @@ const Input = forwardRef<HTMLInputElement, Props>((props, ref) => {
|
||||
)
|
||||
})
|
||||
|
||||
export const EditUpload: React.FC<{
|
||||
doc?: Data
|
||||
type FocalPosition = {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export type EditUploadProps = {
|
||||
fileName: string
|
||||
fileSrc: string
|
||||
imageCacheTag?: string
|
||||
initialCrop?: UploadEdits['crop']
|
||||
initialFocalPoint?: FocalPosition
|
||||
onSave?: (uploadEdits: UploadEdits) => void
|
||||
showCrop?: boolean
|
||||
showFocalPoint?: boolean
|
||||
}> = ({ doc, fileName, fileSrc, imageCacheTag, showCrop, showFocalPoint }) => {
|
||||
}
|
||||
|
||||
const defaultCrop: UploadEdits['crop'] = {
|
||||
height: 100,
|
||||
unit: '%',
|
||||
width: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
}
|
||||
|
||||
export const EditUpload: React.FC<EditUploadProps> = ({
|
||||
fileName,
|
||||
fileSrc,
|
||||
imageCacheTag,
|
||||
initialCrop,
|
||||
initialFocalPoint,
|
||||
onSave,
|
||||
showCrop,
|
||||
showFocalPoint,
|
||||
}) => {
|
||||
const { closeModal } = useModal()
|
||||
const { t } = useTranslation(['general', 'upload'])
|
||||
const { updateUploadEdits, uploadEdits } = useUploadEdits()
|
||||
|
||||
const [focalPosition, setFocalPosition] = useState<{ x: number; y: number }>({
|
||||
x: uploadEdits?.focalPoint?.x || doc.focalX || 50,
|
||||
y: uploadEdits?.focalPoint?.y || doc.focalY || 50,
|
||||
})
|
||||
const [crop, setCrop] = useState<UploadEdits['crop']>(() => ({
|
||||
...defaultCrop,
|
||||
...(initialCrop || {}),
|
||||
}))
|
||||
|
||||
const defaultFocalPosition: FocalPosition = {
|
||||
x: 50,
|
||||
y: 50,
|
||||
}
|
||||
|
||||
const [focalPosition, setFocalPosition] = useState<FocalPosition>(() => ({
|
||||
...defaultFocalPosition,
|
||||
...initialFocalPoint,
|
||||
}))
|
||||
|
||||
const [checkBounds, setCheckBounds] = useState<boolean>(false)
|
||||
const [originalHeight, setOriginalHeight] = useState<number>(0)
|
||||
const [originalWidth, setOriginalWidth] = useState<number>(0)
|
||||
const [uncroppedPixelHeight, setUncroppedPixelHeight] = useState<number>(0)
|
||||
const [uncroppedPixelWidth, setUncroppedPixelWidth] = useState<number>(0)
|
||||
|
||||
const focalWrapRef = useRef<HTMLDivElement | undefined>()
|
||||
const imageRef = useRef<HTMLImageElement | undefined>()
|
||||
const cropRef = useRef<HTMLDivElement | undefined>()
|
||||
|
||||
const heightRef = useRef<HTMLInputElement | null>(null)
|
||||
const widthRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const [crop, setCrop] = useState<CropType>({
|
||||
height: 100,
|
||||
heightPixels: 0,
|
||||
unit: '%',
|
||||
width: 100,
|
||||
widthPixels: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
const heightInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const widthInputRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const [imageLoaded, setImageLoaded] = useState<boolean>(false)
|
||||
|
||||
const onImageLoad = (e) => {
|
||||
setOriginalHeight(e.currentTarget.naturalHeight)
|
||||
setOriginalWidth(e.currentTarget.naturalWidth)
|
||||
// set the default image height/width on load
|
||||
setUncroppedPixelHeight(e.currentTarget.naturalHeight)
|
||||
setUncroppedPixelWidth(e.currentTarget.naturalWidth)
|
||||
setImageLoaded(true)
|
||||
}
|
||||
|
||||
const fineTuneCrop = ({ dimension, value }: { dimension: 'height' | 'width'; value: string }) => {
|
||||
const intValue = parseInt(value)
|
||||
if (dimension === 'width' && intValue >= originalWidth) return null
|
||||
if (dimension === 'height' && intValue >= originalHeight) return null
|
||||
if (dimension === 'width' && intValue >= uncroppedPixelWidth) return null
|
||||
if (dimension === 'height' && intValue >= uncroppedPixelHeight) return null
|
||||
|
||||
const percentage = 100 * (intValue / (dimension === 'width' ? originalWidth : originalHeight))
|
||||
const percentage =
|
||||
100 * (intValue / (dimension === 'width' ? uncroppedPixelWidth : uncroppedPixelHeight))
|
||||
|
||||
if (percentage === 100 || percentage === 0) return null
|
||||
|
||||
@@ -112,16 +138,13 @@ export const EditUpload: React.FC<{
|
||||
}
|
||||
|
||||
const saveEdits = () => {
|
||||
updateUploadEdits({
|
||||
crop: crop
|
||||
? {
|
||||
...crop,
|
||||
heightPixels: Number(heightRef.current?.value ?? crop.heightPixels),
|
||||
widthPixels: Number(widthRef.current?.value ?? crop.widthPixels),
|
||||
}
|
||||
: undefined,
|
||||
focalPoint: focalPosition ? focalPosition : undefined,
|
||||
})
|
||||
if (typeof onSave === 'function')
|
||||
onSave({
|
||||
crop: crop ? crop : undefined,
|
||||
focalPoint: focalPosition,
|
||||
heightInPixels: Number(heightInputRef?.current?.value ?? uncroppedPixelHeight),
|
||||
widthInPixels: Number(widthInputRef?.current?.value ?? uncroppedPixelWidth),
|
||||
})
|
||||
closeModal(editDrawerSlug)
|
||||
}
|
||||
|
||||
@@ -176,7 +199,7 @@ export const EditUpload: React.FC<{
|
||||
className={`${baseClass}__focal-wrapper`}
|
||||
ref={focalWrapRef}
|
||||
style={{
|
||||
aspectRatio: `${originalWidth / originalHeight}`,
|
||||
aspectRatio: `${uncroppedPixelWidth / uncroppedPixelHeight}`,
|
||||
}}
|
||||
>
|
||||
{showCrop ? (
|
||||
@@ -232,10 +255,8 @@ export const EditUpload: React.FC<{
|
||||
onClick={() =>
|
||||
setCrop({
|
||||
height: 100,
|
||||
heightPixels: originalHeight,
|
||||
unit: '%',
|
||||
width: 100,
|
||||
widthPixels: originalWidth,
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
@@ -252,14 +273,14 @@ export const EditUpload: React.FC<{
|
||||
<Input
|
||||
name={`${t('upload:width')} (px)`}
|
||||
onChange={(value) => fineTuneCrop({ dimension: 'width', value })}
|
||||
ref={widthRef}
|
||||
value={((crop.width / 100) * originalWidth).toFixed(0)}
|
||||
ref={widthInputRef}
|
||||
value={((crop.width / 100) * uncroppedPixelWidth).toFixed(0)}
|
||||
/>
|
||||
<Input
|
||||
name={`${t('upload:height')} (px)`}
|
||||
onChange={(value) => fineTuneCrop({ dimension: 'height', value })}
|
||||
ref={heightRef}
|
||||
value={((crop.height / 100) * originalHeight).toFixed(0)}
|
||||
ref={heightInputRef}
|
||||
value={((crop.height / 100) * uncroppedPixelHeight).toFixed(0)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -187,7 +187,7 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
options.forEach((opt) => {
|
||||
if (opt.options) {
|
||||
opt.options.some((subOpt) => {
|
||||
if (subOpt?.value === val.value) {
|
||||
if (subOpt?.value == val.value) {
|
||||
matchedOption = subOpt
|
||||
return true
|
||||
}
|
||||
@@ -200,7 +200,7 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
return matchedOption
|
||||
}
|
||||
|
||||
return options.find((opt) => opt.value === val)
|
||||
return options.find((opt) => opt.value == val)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
options.forEach((opt) => {
|
||||
if (opt?.options) {
|
||||
opt.options.some((subOpt) => {
|
||||
if (subOpt?.value === valueWithRelation.value) {
|
||||
if (subOpt?.value == valueWithRelation.value) {
|
||||
matchedOption = subOpt
|
||||
return true
|
||||
}
|
||||
@@ -227,7 +227,7 @@ const RelationshipField: React.FC<Props> = (props) => {
|
||||
return matchedOption
|
||||
}
|
||||
|
||||
return options.find((opt) => opt.value === value)
|
||||
return options.find((opt) => opt.value == value)
|
||||
}
|
||||
|
||||
return undefined
|
||||
|
||||
@@ -139,7 +139,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
})
|
||||
|
||||
if (!errorLoading) {
|
||||
relationsToFetch.reduce(async (priorRelation, relation) => {
|
||||
await relationsToFetch.reduce(async (priorRelation, relation) => {
|
||||
const relationFilterOption = filterOptionsResult?.[relation]
|
||||
let lastLoadedPageToUse
|
||||
if (search !== searchArg) {
|
||||
@@ -197,12 +197,17 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
query.where.and.push(relationFilterOption)
|
||||
}
|
||||
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
const response = await fetch(
|
||||
`${serverURL}${api}/${relation}?${qs.stringify(query, {
|
||||
strictNullHandling: true,
|
||||
})}`,
|
||||
{
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
if (response.ok) {
|
||||
const data: PaginatedDocs<unknown> = await response.json()
|
||||
@@ -269,7 +274,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
)
|
||||
|
||||
const updateSearch = useDebouncedCallback((searchArg: string, valueArg: Value | Value[]) => {
|
||||
getResults({ search: searchArg, sort: true, value: valueArg })
|
||||
void getResults({ search: searchArg, sort: true, value: valueArg })
|
||||
setSearch(searchArg)
|
||||
}, 300)
|
||||
|
||||
@@ -280,7 +285,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
updateSearch(searchArg, valueArg, searchArg !== '')
|
||||
}
|
||||
},
|
||||
[search, updateSearch],
|
||||
[initialLoadedPageState, search, updateSearch],
|
||||
)
|
||||
|
||||
// ///////////////////////////////////
|
||||
@@ -294,15 +299,14 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
value,
|
||||
})
|
||||
|
||||
Object.entries(relationMap).reduce(async (priorRelation, [relation, ids]) => {
|
||||
void Object.entries(relationMap).reduce(async (priorRelation, [relation, ids]) => {
|
||||
await priorRelation
|
||||
|
||||
const idsToLoad = ids.filter((id) => {
|
||||
return !options.find(
|
||||
(optionGroup) =>
|
||||
optionGroup?.options?.find(
|
||||
(option) => option.value === id && option.relationTo === relation,
|
||||
),
|
||||
return !options.find((optionGroup) =>
|
||||
optionGroup?.options?.find(
|
||||
(option) => option.value === id && option.relationTo === relation,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -320,12 +324,17 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
if (!errorLoading) {
|
||||
const response = await fetch(`${serverURL}${api}/${relation}?${qs.stringify(query)}`, {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
const response = await fetch(
|
||||
`${serverURL}${api}/${relation}?${qs.stringify(query, {
|
||||
strictNullHandling: true,
|
||||
})}`,
|
||||
{
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const collection = collections.find((coll) => coll.slug === relation)
|
||||
let docs = []
|
||||
@@ -507,7 +516,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
onMenuOpen={() => {
|
||||
if (!hasLoadedFirstPage) {
|
||||
setIsLoading(true)
|
||||
getResults({
|
||||
void getResults({
|
||||
onSuccess: () => {
|
||||
setHasLoadedFirstPage(true)
|
||||
setIsLoading(false)
|
||||
@@ -517,7 +526,7 @@ const Relationship: React.FC<Props> = (props) => {
|
||||
}
|
||||
}}
|
||||
onMenuScrollToBottom={() => {
|
||||
getResults({
|
||||
void getResults({
|
||||
lastFullyLoadedRelation,
|
||||
search,
|
||||
sort: false,
|
||||
|
||||
@@ -11,7 +11,6 @@ import type { AuthContext } from './types'
|
||||
import { requests } from '../../../api'
|
||||
import useDebounce from '../../../hooks/useDebounce'
|
||||
import { useConfig } from '../Config'
|
||||
import { useLocale } from '../Locale'
|
||||
|
||||
const Context = createContext({} as AuthContext)
|
||||
|
||||
@@ -24,7 +23,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const [strategy, setStrategy] = useState<string>()
|
||||
const { pathname } = useLocation()
|
||||
const { push } = useHistory()
|
||||
const { code } = useLocale()
|
||||
|
||||
const config = useConfig()
|
||||
|
||||
@@ -40,9 +38,50 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const { closeAllModals, openModal } = useModal()
|
||||
const [lastLocationChange, setLastLocationChange] = useState(0)
|
||||
const debouncedLocationChange = useDebounce(lastLocationChange, 10000)
|
||||
const userIDRef = React.useRef<null | number | string>()
|
||||
|
||||
const id = user?.id
|
||||
|
||||
const refreshPermissions = useCallback(
|
||||
async ({ locale }: { locale?: string } = {}) => {
|
||||
const params = {
|
||||
locale,
|
||||
}
|
||||
try {
|
||||
const request = await requests.get(
|
||||
`${serverURL}${api}/access${qs.stringify(params, { addQueryPrefix: true })}`,
|
||||
{
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (request.status === 200) {
|
||||
const json: Permissions = await request.json()
|
||||
setPermissions(json)
|
||||
} else {
|
||||
throw new Error(`Fetching permissions failed with status code ${request.status}`)
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(`Refreshing permissions failed: ${e.message}`)
|
||||
}
|
||||
},
|
||||
[serverURL, api, i18n],
|
||||
)
|
||||
|
||||
const setActiveUser = React.useCallback(
|
||||
async (userToSet: User | null) => {
|
||||
if ((userIDRef.current && !userToSet?.id) || userToSet?.id) {
|
||||
// refresh on logout and login
|
||||
await refreshPermissions()
|
||||
}
|
||||
userIDRef.current = userToSet?.id || null
|
||||
setUser(userToSet)
|
||||
},
|
||||
[refreshPermissions],
|
||||
)
|
||||
|
||||
const redirectToInactivityRoute = useCallback(() => {
|
||||
if (window.location.pathname.startsWith(admin)) {
|
||||
const redirectParam = `?redirect=${encodeURIComponent(
|
||||
@@ -93,10 +132,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
if (request.status === 200) {
|
||||
const json = await request.json()
|
||||
setUser(json.user)
|
||||
await setActiveUser(json.user)
|
||||
setTokenAndExpiration(json)
|
||||
} else {
|
||||
setUser(null)
|
||||
await setActiveUser(null)
|
||||
redirectToInactivityRoute()
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -110,9 +149,10 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
serverURL,
|
||||
api,
|
||||
userSlug,
|
||||
i18n,
|
||||
redirectToInactivityRoute,
|
||||
i18n.language,
|
||||
setActiveUser,
|
||||
setTokenAndExpiration,
|
||||
redirectToInactivityRoute,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -128,13 +168,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
if (request.status === 200) {
|
||||
const json = await request.json()
|
||||
if (!skipSetUser) {
|
||||
setUser(json.user)
|
||||
await setActiveUser(json.user)
|
||||
setTokenAndExpiration(json)
|
||||
}
|
||||
return json.user
|
||||
}
|
||||
|
||||
setUser(null)
|
||||
await setActiveUser(null)
|
||||
redirectToInactivityRoute()
|
||||
return null
|
||||
} catch (e) {
|
||||
@@ -142,36 +182,22 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
return null
|
||||
}
|
||||
},
|
||||
[serverURL, api, userSlug, i18n, redirectToInactivityRoute, setTokenAndExpiration],
|
||||
[
|
||||
serverURL,
|
||||
api,
|
||||
userSlug,
|
||||
i18n,
|
||||
redirectToInactivityRoute,
|
||||
setTokenAndExpiration,
|
||||
setActiveUser,
|
||||
],
|
||||
)
|
||||
|
||||
const logOut = useCallback(() => {
|
||||
setUser(null)
|
||||
const logOut = useCallback(async () => {
|
||||
await setActiveUser(null)
|
||||
revokeTokenAndExpire()
|
||||
requests.post(`${serverURL}${api}/${userSlug}/logout`)
|
||||
}, [serverURL, api, userSlug, revokeTokenAndExpire])
|
||||
|
||||
const refreshPermissions = useCallback(async () => {
|
||||
const params = {
|
||||
locale: code,
|
||||
}
|
||||
try {
|
||||
const request = await requests.get(`${serverURL}${api}/access?${qs.stringify(params)}`, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
})
|
||||
|
||||
if (request.status === 200) {
|
||||
const json: Permissions = await request.json()
|
||||
setPermissions(json)
|
||||
} else {
|
||||
throw new Error(`Fetching permissions failed with status code ${request.status}`)
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(`Refreshing permissions failed: ${e.message}`)
|
||||
}
|
||||
}, [serverURL, api, i18n, code])
|
||||
void requests.post(`${serverURL}${api}/${userSlug}/logout`)
|
||||
}, [serverURL, api, userSlug, revokeTokenAndExpire, setActiveUser])
|
||||
|
||||
const fetchFullUser = React.useCallback(async () => {
|
||||
try {
|
||||
@@ -185,7 +211,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const json = await request.json()
|
||||
|
||||
if (json?.user) {
|
||||
setUser(json.user)
|
||||
await setActiveUser(json.user)
|
||||
if (json?.token) {
|
||||
setTokenAndExpiration(json)
|
||||
}
|
||||
@@ -204,28 +230,39 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
})
|
||||
if (autoLoginResult.status === 200) {
|
||||
const autoLoginJson = await autoLoginResult.json()
|
||||
setUser(autoLoginJson.user)
|
||||
await setActiveUser(autoLoginJson.user)
|
||||
if (autoLoginJson?.token) {
|
||||
setTokenAndExpiration(autoLoginJson)
|
||||
}
|
||||
} else {
|
||||
setUser(null)
|
||||
await setActiveUser(null)
|
||||
revokeTokenAndExpire()
|
||||
}
|
||||
} else {
|
||||
setUser(null)
|
||||
await setActiveUser(null)
|
||||
revokeTokenAndExpire()
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(`Fetching user failed: ${e.message}`)
|
||||
}
|
||||
}, [serverURL, api, userSlug, i18n, autoLogin, setTokenAndExpiration, revokeTokenAndExpire])
|
||||
}, [
|
||||
serverURL,
|
||||
api,
|
||||
userSlug,
|
||||
i18n,
|
||||
autoLogin,
|
||||
setTokenAndExpiration,
|
||||
revokeTokenAndExpire,
|
||||
setActiveUser,
|
||||
])
|
||||
|
||||
// On mount, get user and set
|
||||
useEffect(() => {
|
||||
fetchFullUser()
|
||||
}, [fetchFullUser])
|
||||
if (id === undefined || id !== userIDRef.current) {
|
||||
void fetchFullUser()
|
||||
}
|
||||
}, [fetchFullUser, id])
|
||||
|
||||
// When location changes, refresh cookie
|
||||
useEffect(() => {
|
||||
@@ -238,13 +275,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
setLastLocationChange(Date.now())
|
||||
}, [pathname])
|
||||
|
||||
// When user changes, get new access
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
refreshPermissions()
|
||||
}
|
||||
}, [i18n, id, api, serverURL, refreshPermissions])
|
||||
|
||||
useEffect(() => {
|
||||
let reminder: ReturnType<typeof setTimeout>
|
||||
const now = Math.round(new Date().getTime() / 1000)
|
||||
@@ -271,8 +301,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
if (remainingTime > 0) {
|
||||
forceLogOut = setTimeout(
|
||||
() => {
|
||||
setUser(null)
|
||||
async () => {
|
||||
await setActiveUser(null)
|
||||
revokeTokenAndExpire()
|
||||
redirectToInactivityRoute()
|
||||
},
|
||||
@@ -283,7 +313,14 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
return () => {
|
||||
if (forceLogOut) clearTimeout(forceLogOut)
|
||||
}
|
||||
}, [tokenExpiration, closeAllModals, i18n, redirectToInactivityRoute, revokeTokenAndExpire])
|
||||
}, [
|
||||
tokenExpiration,
|
||||
closeAllModals,
|
||||
i18n,
|
||||
redirectToInactivityRoute,
|
||||
revokeTokenAndExpire,
|
||||
setActiveUser,
|
||||
])
|
||||
|
||||
return (
|
||||
<Context.Provider
|
||||
@@ -294,7 +331,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
refreshCookie,
|
||||
refreshCookieAsync,
|
||||
refreshPermissions,
|
||||
setUser,
|
||||
setUser: setActiveUser,
|
||||
strategy,
|
||||
token: tokenInMemory,
|
||||
tokenExpiration,
|
||||
|
||||
@@ -2,12 +2,12 @@ import type { Permissions, User } from '../../../../auth/types'
|
||||
|
||||
export type AuthContext<T = User> = {
|
||||
fetchFullUser: () => Promise<void>
|
||||
logOut: () => void
|
||||
logOut: () => Promise<void>
|
||||
permissions?: Permissions
|
||||
refreshCookie: (forceRefresh?: boolean) => void
|
||||
refreshCookieAsync: () => Promise<User>
|
||||
refreshPermissions: () => Promise<void>
|
||||
setUser: (user: T) => void
|
||||
refreshPermissions: ({ locale }?: { locale?: string }) => Promise<void>
|
||||
setUser: (user: T) => Promise<void>
|
||||
strategy?: string
|
||||
token?: string
|
||||
tokenExpiration?: number
|
||||
|
||||
@@ -13,7 +13,7 @@ const LocaleContext = createContext({} as Locale)
|
||||
export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
const { localization } = useConfig()
|
||||
|
||||
const { user } = useAuth()
|
||||
const { refreshPermissions, user } = useAuth()
|
||||
const defaultLocale =
|
||||
localization && localization.defaultLocale ? localization.defaultLocale : 'en'
|
||||
const searchParams = useSearchParams()
|
||||
@@ -26,6 +26,22 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ child
|
||||
const { getPreference, setPreference } = usePreferences()
|
||||
const localeFromParams = searchParams.locale
|
||||
|
||||
const handleLocaleChange = React.useCallback(
|
||||
(newLocaleCode: string) => {
|
||||
if (!localization) return
|
||||
|
||||
if (localization.localeCodes.indexOf(newLocaleCode) > -1) {
|
||||
setLocaleCode(newLocaleCode)
|
||||
setLocale(findLocaleFromCode(localization, newLocaleCode))
|
||||
if (user) {
|
||||
void setPreference('locale', newLocaleCode)
|
||||
void refreshPermissions({ locale: newLocaleCode })
|
||||
}
|
||||
}
|
||||
},
|
||||
[localization, setPreference, user, refreshPermissions],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!localization) {
|
||||
return
|
||||
@@ -33,31 +49,23 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ child
|
||||
|
||||
// set locale from search param
|
||||
if (localeFromParams && localization.localeCodes.indexOf(localeFromParams as string) > -1) {
|
||||
setLocaleCode(localeFromParams as string)
|
||||
setLocale(findLocaleFromCode(localization, localeFromParams as string))
|
||||
if (user) setPreference('locale', localeFromParams)
|
||||
handleLocaleChange(localeFromParams as string)
|
||||
return
|
||||
}
|
||||
|
||||
// set locale from preferences or default
|
||||
;(async () => {
|
||||
const initializeLocale = async () => {
|
||||
let preferenceLocale: string
|
||||
let isPreferenceInConfig: boolean
|
||||
if (user) {
|
||||
preferenceLocale = await getPreference<string>('locale')
|
||||
isPreferenceInConfig =
|
||||
preferenceLocale && localization.localeCodes.indexOf(preferenceLocale) > -1
|
||||
if (isPreferenceInConfig) {
|
||||
setLocaleCode(preferenceLocale)
|
||||
setLocale(findLocaleFromCode(localization, preferenceLocale))
|
||||
return
|
||||
}
|
||||
setPreference('locale', defaultLocale)
|
||||
handleLocaleChange(preferenceLocale)
|
||||
return
|
||||
}
|
||||
setLocaleCode(defaultLocale)
|
||||
setLocale(findLocaleFromCode(localization, defaultLocale))
|
||||
})()
|
||||
}, [defaultLocale, getPreference, localeFromParams, setPreference, user, localization])
|
||||
handleLocaleChange(defaultLocale)
|
||||
}
|
||||
|
||||
void initializeLocale()
|
||||
}, [defaultLocale, getPreference, handleLocaleChange, localeFromParams, localization, user])
|
||||
|
||||
return <LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ import React from 'react'
|
||||
import type { UploadEdits } from '../../../../uploads/types'
|
||||
|
||||
export type UploadEditsContext = {
|
||||
resetUploadEdits: () => void
|
||||
updateUploadEdits: (edits: UploadEdits) => void
|
||||
uploadEdits: UploadEdits
|
||||
}
|
||||
|
||||
const Context = React.createContext<UploadEditsContext>({
|
||||
resetUploadEdits: undefined,
|
||||
updateUploadEdits: undefined,
|
||||
uploadEdits: undefined,
|
||||
})
|
||||
@@ -15,6 +17,10 @@ const Context = React.createContext<UploadEditsContext>({
|
||||
export const UploadEditsProvider = ({ children }) => {
|
||||
const [uploadEdits, setUploadEdits] = React.useState<UploadEdits>(undefined)
|
||||
|
||||
const resetUploadEdits = () => {
|
||||
setUploadEdits({})
|
||||
}
|
||||
|
||||
const updateUploadEdits = (edits: UploadEdits) => {
|
||||
setUploadEdits((prevEdits) => ({
|
||||
...(prevEdits || {}),
|
||||
@@ -22,7 +28,11 @@ export const UploadEditsProvider = ({ children }) => {
|
||||
}))
|
||||
}
|
||||
|
||||
return <Context.Provider value={{ updateUploadEdits, uploadEdits }}>{children}</Context.Provider>
|
||||
return (
|
||||
<Context.Provider value={{ resetUploadEdits, updateUploadEdits, uploadEdits }}>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useUploadEdits = (): UploadEditsContext => React.useContext(Context)
|
||||
|
||||
@@ -25,7 +25,7 @@ const Logout: React.FC<{ inactivity?: boolean }> = (props) => {
|
||||
const redirect = query.get('redirect')
|
||||
|
||||
useEffect(() => {
|
||||
logOut()
|
||||
void logOut()
|
||||
}, [logOut])
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,12 +3,14 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
import Button from '../../elements/Button'
|
||||
import MinimalTemplate from '../../templates/Minimal'
|
||||
import { useAuth } from '../../utilities/Auth'
|
||||
import { useConfig } from '../../utilities/Config'
|
||||
import Meta from '../../utilities/Meta'
|
||||
|
||||
const Unauthorized: React.FC = () => {
|
||||
const { t } = useTranslation('general')
|
||||
const config = useConfig()
|
||||
const { user } = useAuth()
|
||||
const {
|
||||
admin: { logoutRoute },
|
||||
routes: { admin },
|
||||
@@ -20,11 +22,11 @@ const Unauthorized: React.FC = () => {
|
||||
keywords={t('error:unauthorized')}
|
||||
title={t('error:unauthorized')}
|
||||
/>
|
||||
<h2>{t('error:unauthorized')}</h2>
|
||||
<h2>{user ? t('general:unauthorized') : t('error:unauthorized')}</h2>
|
||||
<p>{t('error:notAllowedToAccessPage')}</p>
|
||||
<br />
|
||||
<Button el="link" to={`${admin}${logoutRoute}`}>
|
||||
{t('authentication:logOut')}
|
||||
<Button el="link" to={`${admin}${user ? '' : logoutRoute}`}>
|
||||
{user ? t('general:backToDashboard') : t('authentication:logOut')}
|
||||
</Button>
|
||||
</MinimalTemplate>
|
||||
)
|
||||
|
||||
@@ -35,6 +35,7 @@ export const DefaultCollectionEdit: React.FC<
|
||||
hasSavePermission,
|
||||
internalState,
|
||||
isEditing,
|
||||
onSave,
|
||||
permissions,
|
||||
} = props
|
||||
|
||||
@@ -68,6 +69,7 @@ export const DefaultCollectionEdit: React.FC<
|
||||
hasSavePermission={hasSavePermission}
|
||||
id={id}
|
||||
isEditing={isEditing}
|
||||
onSave={onSave}
|
||||
permissions={permissions}
|
||||
/>
|
||||
<DocumentFields
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
place-self: flex-start;
|
||||
}
|
||||
|
||||
&__file-adjustments {
|
||||
&__file-adjustments,
|
||||
&__remote-file-wrap {
|
||||
padding: $baseline;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@@ -43,12 +44,14 @@
|
||||
gap: calc(var(--base) / 2);
|
||||
}
|
||||
|
||||
&__filename {
|
||||
&__filename,
|
||||
&__remote-file {
|
||||
@include formInput;
|
||||
background-color: var(--theme-bg);
|
||||
}
|
||||
|
||||
&__file-mutation {
|
||||
&__file-mutation,
|
||||
&__add-file-wrap {
|
||||
display: flex;
|
||||
gap: calc(var(--base) / 2);
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
import type { UploadEdits } from '../../../../../../uploads/types'
|
||||
import type { Props } from './types'
|
||||
|
||||
import isImage from '../../../../../../uploads/isImage'
|
||||
@@ -12,10 +14,12 @@ import FileDetails from '../../../../elements/FileDetails'
|
||||
import PreviewSizes from '../../../../elements/PreviewSizes'
|
||||
import Thumbnail from '../../../../elements/Thumbnail'
|
||||
import Error from '../../../../forms/Error'
|
||||
import { useForm } from '../../../../forms/Form/context'
|
||||
import reduceFieldsToValues from '../../../../forms/Form/reduceFieldsToValues'
|
||||
import { fieldBaseClass } from '../../../../forms/field-types/shared'
|
||||
import useField from '../../../../forms/useField'
|
||||
import { useDocumentInfo } from '../../../../utilities/DocumentInfo'
|
||||
import { useUploadEdits } from '../../../../utilities/UploadEdits'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'file-field'
|
||||
@@ -49,10 +53,12 @@ export const UploadActions = ({ canEdit, showSizePreviews }) => {
|
||||
}
|
||||
|
||||
export const Upload: React.FC<Props> = (props) => {
|
||||
const { collection, internalState, onChange, updatedAt } = props
|
||||
const { collection, internalState, onChange } = props
|
||||
const [replacingFile, setReplacingFile] = useState(false)
|
||||
const [fileSrc, setFileSrc] = useState<null | string>(null)
|
||||
const { t } = useTranslation(['upload', 'general'])
|
||||
const { setModified } = useForm()
|
||||
const { resetUploadEdits, updateUploadEdits, uploadEdits } = useUploadEdits()
|
||||
const [doc, setDoc] = useState(reduceFieldsToValues(internalState || {}, true))
|
||||
const { docPermissions } = useDocumentInfo()
|
||||
const { errorMessage, setValue, showError, value } = useField<File>({
|
||||
@@ -60,29 +66,105 @@ export const Upload: React.FC<Props> = (props) => {
|
||||
validate,
|
||||
})
|
||||
|
||||
const [showUrlInput, setShowUrlInput] = useState(false)
|
||||
const [fileUrl, setFileUrl] = useState<string>('')
|
||||
|
||||
const cursorPositionRef = useRef(null)
|
||||
const urlInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleFileChange = useCallback(
|
||||
(newFile: File) => {
|
||||
if (newFile instanceof File) {
|
||||
const fileReader = new FileReader()
|
||||
fileReader.onload = (e) => {
|
||||
const imgSrc = e.target?.result
|
||||
|
||||
if (typeof imgSrc === 'string') {
|
||||
setFileSrc(imgSrc)
|
||||
}
|
||||
}
|
||||
fileReader.readAsDataURL(newFile)
|
||||
}
|
||||
|
||||
setValue(newFile)
|
||||
setShowUrlInput(false)
|
||||
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(newFile)
|
||||
}
|
||||
},
|
||||
[onChange, setValue],
|
||||
)
|
||||
|
||||
const handleFileNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const updatedFileName = e.target.value
|
||||
const cursorPosition = e.target.selectionStart
|
||||
|
||||
cursorPositionRef.current = cursorPosition
|
||||
|
||||
if (value) {
|
||||
const fileValue = value
|
||||
// Creating a new File object with updated properties
|
||||
const newFile = new File([fileValue], updatedFileName, { type: fileValue.type })
|
||||
setValue(newFile) // Updating the state with the new File object
|
||||
handleFileChange(newFile)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
const inputElement = document.querySelector(`.${baseClass}__filename`) as HTMLInputElement
|
||||
if (inputElement && cursorPositionRef.current !== null) {
|
||||
inputElement.setSelectionRange(cursorPositionRef.current, cursorPositionRef.current)
|
||||
}
|
||||
}, [value])
|
||||
|
||||
const handleFileSelection = React.useCallback(
|
||||
(files: FileList) => {
|
||||
const fileToUpload = files?.[0]
|
||||
setValue(fileToUpload)
|
||||
handleFileChange(fileToUpload)
|
||||
},
|
||||
[setValue],
|
||||
[handleFileChange],
|
||||
)
|
||||
|
||||
const handleFileRemoval = useCallback(() => {
|
||||
setReplacingFile(true)
|
||||
setValue(null)
|
||||
handleFileChange(null)
|
||||
setFileSrc('')
|
||||
}, [setValue])
|
||||
setFileUrl('')
|
||||
setDoc({})
|
||||
resetUploadEdits()
|
||||
setShowUrlInput(false)
|
||||
}, [handleFileChange, resetUploadEdits])
|
||||
|
||||
const onEditsSave = useCallback(
|
||||
(args: UploadEdits) => {
|
||||
setModified(true)
|
||||
updateUploadEdits(args)
|
||||
},
|
||||
[setModified, updateUploadEdits],
|
||||
)
|
||||
|
||||
const handlePasteUrlClick = () => {
|
||||
setShowUrlInput((prev) => !prev)
|
||||
}
|
||||
|
||||
const handleUrlSubmit = async () => {
|
||||
if (fileUrl) {
|
||||
try {
|
||||
const response = await fetch(fileUrl)
|
||||
const data = await response.blob()
|
||||
|
||||
// Extract the file name from the URL
|
||||
const fileName = fileUrl.split('/').pop()
|
||||
|
||||
// Create a new File object from the Blob data
|
||||
const file = new File([data], fileName, { type: data.type })
|
||||
handleFileChange(file)
|
||||
} catch (e) {
|
||||
toast.error(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setDoc(reduceFieldsToValues(internalState || {}, true))
|
||||
@@ -90,22 +172,10 @@ export const Upload: React.FC<Props> = (props) => {
|
||||
}, [internalState])
|
||||
|
||||
useEffect(() => {
|
||||
if (value instanceof File) {
|
||||
const fileReader = new FileReader()
|
||||
fileReader.onload = (e) => {
|
||||
const imgSrc = e.target?.result
|
||||
|
||||
if (typeof imgSrc === 'string') {
|
||||
setFileSrc(imgSrc)
|
||||
}
|
||||
}
|
||||
fileReader.readAsDataURL(value)
|
||||
if (showUrlInput && urlInputRef.current) {
|
||||
urlInputRef.current.focus() // Focus on the remote-url input field when showUrlInput is true
|
||||
}
|
||||
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(value)
|
||||
}
|
||||
}, [value, onChange, updatedAt])
|
||||
}, [showUrlInput])
|
||||
|
||||
const canRemoveUpload =
|
||||
docPermissions?.update?.permission &&
|
||||
@@ -135,17 +205,49 @@ export const Upload: React.FC<Props> = (props) => {
|
||||
imageCacheTag={doc.updatedAt}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(!doc.filename || replacingFile) && (
|
||||
<div className={`${baseClass}__upload`}>
|
||||
{!value && (
|
||||
{!value && !showUrlInput && (
|
||||
<Dropzone
|
||||
className={`${baseClass}__dropzone`}
|
||||
mimeTypes={collection?.upload?.mimeTypes}
|
||||
onChange={handleFileSelection}
|
||||
onPasteUrlClick={handlePasteUrlClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showUrlInput && (
|
||||
<React.Fragment>
|
||||
<div className={`${baseClass}__remote-file-wrap`}>
|
||||
<input
|
||||
className={`${baseClass}__remote-file`}
|
||||
onChange={(e) => {
|
||||
setFileUrl(e.target.value)
|
||||
}}
|
||||
ref={urlInputRef}
|
||||
type="text"
|
||||
value={fileUrl}
|
||||
/>
|
||||
<div className={`${baseClass}__add-file-wrap`}>
|
||||
<button
|
||||
className={`${baseClass}__add-file`}
|
||||
onClick={handleUrlSubmit}
|
||||
type="button"
|
||||
>
|
||||
{t('upload:addFile')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
buttonStyle="icon-label"
|
||||
className={`${baseClass}__remove`}
|
||||
icon="x"
|
||||
iconStyle="with-border"
|
||||
onClick={handleFileRemoval}
|
||||
round
|
||||
tooltip={t('general:cancel')}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{value && (
|
||||
<React.Fragment>
|
||||
<div className={`${baseClass}__thumbnail-wrap`}>
|
||||
@@ -183,10 +285,15 @@ export const Upload: React.FC<Props> = (props) => {
|
||||
{(value || doc.filename) && (
|
||||
<Drawer header={null} slug={editDrawerSlug}>
|
||||
<EditUpload
|
||||
doc={doc || undefined}
|
||||
fileName={value?.name || doc?.filename}
|
||||
fileSrc={fileSrc || doc?.url}
|
||||
fileSrc={doc?.url || fileSrc}
|
||||
imageCacheTag={doc.updatedAt}
|
||||
initialCrop={uploadEdits?.crop ?? undefined}
|
||||
initialFocalPoint={{
|
||||
x: uploadEdits?.focalPoint?.x || doc.focalX || 50,
|
||||
y: uploadEdits?.focalPoint?.y || doc.focalY || 50,
|
||||
}}
|
||||
onSave={onEditsSave}
|
||||
showCrop={showCrop}
|
||||
showFocalPoint={showFocalPoint}
|
||||
/>
|
||||
|
||||
@@ -4,15 +4,3 @@ export type IndexProps = {
|
||||
collection: SanitizedCollectionConfig
|
||||
isEditing?: boolean
|
||||
}
|
||||
export type UploadEdits = {
|
||||
crop?: {
|
||||
height?: number
|
||||
width?: number
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
focalPoint?: {
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ const DefaultCell: React.FC<Props> = (props) => {
|
||||
<WrapElement {...wrapElementProps}>
|
||||
<CodeCell
|
||||
collection={collection}
|
||||
data={`ID: ${cellData}`}
|
||||
data={`ID: ${String(cellData)}`}
|
||||
field={field as CodeField}
|
||||
nowrap
|
||||
rowData={rowData}
|
||||
@@ -68,13 +68,22 @@ const DefaultCell: React.FC<Props> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
let CellComponent: React.FC<CellComponentProps> = cellData && cellComponents[field.type]
|
||||
let CellComponent: React.FC<CellComponentProps> | false =
|
||||
(cellData || typeof cellData === 'boolean') &&
|
||||
cellData !== null &&
|
||||
typeof cellData !== 'undefined' &&
|
||||
cellComponents[field.type]
|
||||
|
||||
if (!CellComponent) {
|
||||
if (collection.upload && fieldAffectsData(field) && field.name === 'filename') {
|
||||
CellComponent = cellComponents.File
|
||||
} else {
|
||||
if (!cellData && 'label' in field) {
|
||||
if (
|
||||
(cellData === undefined ||
|
||||
cellData === null ||
|
||||
(typeof cellData === 'string' && cellData.trim() === '')) &&
|
||||
'label' in field
|
||||
) {
|
||||
return (
|
||||
<WrapElement {...wrapElementProps}>
|
||||
{t('noLabel', {
|
||||
@@ -85,7 +94,7 @@ const DefaultCell: React.FC<Props> = (props) => {
|
||||
})}
|
||||
</WrapElement>
|
||||
)
|
||||
} else if (typeof cellData === 'string' || typeof cellData === 'number') {
|
||||
} else if (['number', 'string'].includes(typeof cellData)) {
|
||||
return <WrapElement {...wrapElementProps}>{cellData}</WrapElement>
|
||||
} else if (typeof cellData === 'object') {
|
||||
return <WrapElement {...wrapElementProps}>{JSON.stringify(cellData)}</WrapElement>
|
||||
@@ -95,7 +104,9 @@ const DefaultCell: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<WrapElement {...wrapElementProps}>
|
||||
<CellComponent collection={collection} data={cellData} field={field} rowData={rowData} />
|
||||
{CellComponent ? (
|
||||
<CellComponent collection={collection} data={cellData} field={field} rowData={rowData} />
|
||||
) : null}
|
||||
</WrapElement>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export type Props = {
|
||||
onClick?: (Props) => void
|
||||
rowData: {
|
||||
[path: string]: unknown
|
||||
id: number | string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,26 +2,15 @@ import type { Collection } from '../../../collections/config/types'
|
||||
import type { PayloadRequest } from '../../../express/types'
|
||||
|
||||
import isolateObjectProperty from '../../../utilities/isolateObjectProperty'
|
||||
import getExtractJWT from '../../getExtractJWT'
|
||||
import refresh from '../../operations/refresh'
|
||||
|
||||
function refreshResolver(collection: Collection) {
|
||||
async function resolver(_, args, context) {
|
||||
let token
|
||||
|
||||
const extractJWT = getExtractJWT(context.req.payload.config)
|
||||
token = extractJWT(context.req)
|
||||
|
||||
if (args.token) {
|
||||
token = args.token
|
||||
}
|
||||
|
||||
async function resolver(_, __, context) {
|
||||
const options = {
|
||||
collection,
|
||||
depth: 0,
|
||||
req: isolateObjectProperty<PayloadRequest>(context.req, 'transactionID'),
|
||||
res: context.res,
|
||||
token,
|
||||
}
|
||||
|
||||
const result = await refresh(options)
|
||||
|
||||
@@ -26,7 +26,6 @@ export type Arguments = {
|
||||
collection: Collection
|
||||
req: PayloadRequest
|
||||
res?: Response
|
||||
token: string
|
||||
}
|
||||
|
||||
async function refresh(incomingArgs: Arguments): Promise<Result> {
|
||||
@@ -66,7 +65,7 @@ async function refresh(incomingArgs: Arguments): Promise<Result> {
|
||||
},
|
||||
} = args
|
||||
|
||||
if (typeof args.token !== 'string' || !args.req.user) throw new Forbidden(args.req.t)
|
||||
if (!args.req.user) throw new Forbidden(args.req.t)
|
||||
|
||||
const parsedURL = url.parse(args.req.url)
|
||||
const isGraphQL = parsedURL.pathname === config.routes.graphQL
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { NextFunction, Response } from 'express'
|
||||
|
||||
import type { PayloadRequest } from '../../express/types'
|
||||
|
||||
import getExtractJWT from '../getExtractJWT'
|
||||
import refresh from '../operations/refresh'
|
||||
|
||||
export default async function refreshHandler(
|
||||
@@ -11,20 +10,10 @@ export default async function refreshHandler(
|
||||
next: NextFunction,
|
||||
): Promise<any> {
|
||||
try {
|
||||
let token
|
||||
|
||||
const extractJWT = getExtractJWT(req.payload.config)
|
||||
token = extractJWT(req)
|
||||
|
||||
if (req.body.token) {
|
||||
token = req.body.token
|
||||
}
|
||||
|
||||
const result = await refresh({
|
||||
collection: req.collection,
|
||||
req,
|
||||
res,
|
||||
token,
|
||||
})
|
||||
|
||||
return res.status(200).json({
|
||||
|
||||
@@ -217,6 +217,7 @@ const collectionSchema = joi.object().keys({
|
||||
joi.number(),
|
||||
),
|
||||
useTempFiles: joi.bool(),
|
||||
withMetadata: joi.alternatives().try(joi.boolean(), joi.func()),
|
||||
}),
|
||||
joi.boolean(),
|
||||
),
|
||||
|
||||
@@ -204,6 +204,7 @@ export type MeHook<T extends TypeWithID = any> = (args: {
|
||||
args: MeArguments
|
||||
user: T
|
||||
}) => ({ exp: number; user: T } | void) | Promise<{ exp: number; user: T } | void>
|
||||
|
||||
export type AfterRefreshHook<T extends TypeWithID = any> = (args: {
|
||||
/** The collection which this hook is being run on */
|
||||
collection: SanitizedCollectionConfig
|
||||
|
||||
@@ -423,9 +423,6 @@ function initCollectionsGraphQL(payload: Payload): void {
|
||||
},
|
||||
},
|
||||
}),
|
||||
args: {
|
||||
token: { type: GraphQLString },
|
||||
},
|
||||
resolve: refresh(collection),
|
||||
}
|
||||
|
||||
|
||||
@@ -31,8 +31,14 @@ export type {
|
||||
AfterDeleteHook as CollectionAfterDeleteHook,
|
||||
AfterForgotPasswordHook as CollectionAfterForgotPasswordHook,
|
||||
AfterLoginHook as CollectionAfterLoginHook,
|
||||
AfterLogoutHook,
|
||||
AfterLogoutHook as CollectionAfterLogoutHook,
|
||||
AfterMeHook,
|
||||
AfterMeHook as CollectionAfterMeHook,
|
||||
AfterOperationHook as CollectionAfterOperationHook,
|
||||
AfterReadHook as CollectionAfterReadHook,
|
||||
AfterRefreshHook,
|
||||
AfterRefreshHook as CollectionAfterRefreshHook,
|
||||
BeforeChangeHook as CollectionBeforeChangeHook,
|
||||
BeforeDeleteHook as CollectionBeforeDeleteHook,
|
||||
BeforeDuplicate,
|
||||
|
||||
@@ -45,7 +45,7 @@ const middleware = (payload: Payload): any => {
|
||||
i18nMiddleware(payload.config.i18n),
|
||||
identifyAPI('REST'),
|
||||
methodOverride('X-HTTP-Method-Override'),
|
||||
qsMiddleware({ arrayLimit: 1000, depth: 10 }),
|
||||
qsMiddleware({ arrayLimit: 1000, depth: 10, strictNullHandling: true }),
|
||||
bodyParser.urlencoded({ extended: true }),
|
||||
compression(payload.config.express.compression),
|
||||
localizationMiddleware,
|
||||
|
||||
@@ -128,12 +128,13 @@ export type Labels = {
|
||||
singular: Record<string, string> | string
|
||||
}
|
||||
|
||||
export type ValidateOptions<TData, TSiblingData, TFieldConfig> = {
|
||||
export type ValidateOptions<TData, TSiblingData, TFieldConfig, TValue> = {
|
||||
config: SanitizedConfig
|
||||
data: Partial<TData>
|
||||
id?: number | string
|
||||
operation?: Operation
|
||||
payload?: Payload
|
||||
previousValue?: TValue
|
||||
req?: PayloadRequest
|
||||
siblingData: Partial<TSiblingData>
|
||||
t: TFunction
|
||||
@@ -143,7 +144,7 @@ export type ValidateOptions<TData, TSiblingData, TFieldConfig> = {
|
||||
// TODO: Having TFieldConfig as any breaks all type checking / auto-completions for the base ValidateOptions properties.
|
||||
export type Validate<TValue = any, TData = any, TSiblingData = any, TFieldConfig = any> = (
|
||||
value: TValue,
|
||||
options: ValidateOptions<TData, TSiblingData, TFieldConfig>,
|
||||
options: ValidateOptions<TData, TSiblingData, TFieldConfig, TValue>,
|
||||
) => Promise<string | true> | string | true
|
||||
|
||||
export type OptionObject = {
|
||||
|
||||
@@ -11,6 +11,10 @@ export const cloneDataFromOriginalDoc = (originalDocData: unknown): unknown => {
|
||||
})
|
||||
}
|
||||
|
||||
if (originalDocData instanceof Date) {
|
||||
return originalDocData
|
||||
}
|
||||
|
||||
if (typeof originalDocData === 'object' && originalDocData !== null) {
|
||||
return { ...originalDocData }
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ type Args<T> = {
|
||||
req: PayloadRequest
|
||||
siblingData: Record<string, unknown>
|
||||
siblingDoc: Record<string, unknown>
|
||||
siblingDocKeys: Set<string>
|
||||
}
|
||||
|
||||
// This function is responsible for the following actions, in order:
|
||||
@@ -45,8 +46,18 @@ export const promise = async <T>({
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocKeys,
|
||||
}: Args<T>): Promise<void> => {
|
||||
if (fieldAffectsData(field)) {
|
||||
// Remove the key from siblingDocKeys
|
||||
// the goal is to keep any existing data present
|
||||
// before updating, for users that want to maintain
|
||||
// external data in the same collections as Payload manages,
|
||||
// without having fields defined for them
|
||||
if (siblingDocKeys.has(field.name)) {
|
||||
siblingDocKeys.delete(field.name)
|
||||
}
|
||||
|
||||
if (field.name === 'id') {
|
||||
if (field.type === 'number' && typeof siblingData[field.name] === 'string') {
|
||||
const value = siblingData[field.name] as string
|
||||
@@ -368,6 +379,7 @@ export const promise = async <T>({
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocKeys,
|
||||
})
|
||||
|
||||
break
|
||||
@@ -376,7 +388,10 @@ export const promise = async <T>({
|
||||
case 'tab': {
|
||||
let tabSiblingData
|
||||
let tabSiblingDoc
|
||||
if (tabHasName(field)) {
|
||||
|
||||
const isNamedTab = tabHasName(field)
|
||||
|
||||
if (isNamedTab) {
|
||||
if (typeof siblingData[field.name] !== 'object') siblingData[field.name] = {}
|
||||
if (typeof siblingDoc[field.name] !== 'object') siblingDoc[field.name] = {}
|
||||
|
||||
@@ -400,6 +415,7 @@ export const promise = async <T>({
|
||||
req,
|
||||
siblingData: tabSiblingData,
|
||||
siblingDoc: tabSiblingDoc,
|
||||
siblingDocKeys: isNamedTab ? undefined : siblingDocKeys,
|
||||
})
|
||||
|
||||
break
|
||||
@@ -419,6 +435,7 @@ export const promise = async <T>({
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocKeys,
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
@@ -18,6 +18,7 @@ type Args<T> = {
|
||||
req: PayloadRequest
|
||||
siblingData: Record<string, unknown>
|
||||
siblingDoc: Record<string, unknown>
|
||||
siblingDocKeys?: Set<string>
|
||||
}
|
||||
|
||||
export const traverseFields = async <T>({
|
||||
@@ -33,8 +34,11 @@ export const traverseFields = async <T>({
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocKeys: incomingSiblingDocKeys,
|
||||
}: Args<T>): Promise<void> => {
|
||||
const promises = []
|
||||
const siblingDocKeys = incomingSiblingDocKeys || new Set(Object.keys(siblingDoc))
|
||||
|
||||
fields.forEach((field) => {
|
||||
promises.push(
|
||||
promise({
|
||||
@@ -50,8 +54,19 @@ export const traverseFields = async <T>({
|
||||
req,
|
||||
siblingData,
|
||||
siblingDoc,
|
||||
siblingDocKeys,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
// For any siblingDocKeys that have not been deleted,
|
||||
// we will move the data to the siblingData object
|
||||
// to preserve it
|
||||
siblingDocKeys.forEach((key) => {
|
||||
if (!['createdAt', 'globalType', 'id', 'updatedAt'].includes(key)) {
|
||||
siblingData[key] = siblingDoc[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -209,6 +209,10 @@ export const checkbox: Validate<unknown, unknown, CheckboxField> = (
|
||||
}
|
||||
|
||||
export const date: Validate<unknown, unknown, DateField> = (value, { required, t }) => {
|
||||
if (value instanceof Date) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (value && !isNaN(Date.parse(value.toString()))) {
|
||||
/* eslint-disable-line */
|
||||
return true
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"near": "قريب من"
|
||||
},
|
||||
"upload": {
|
||||
"addFile": "إضافة ملف",
|
||||
"crop": "محصول",
|
||||
"cropToolDescription": "اسحب الزوايا المحددة للمنطقة، رسم منطقة جديدة أو قم بضبط القيم أدناه.",
|
||||
"dragAndDrop": "قم بسحب وإسقاط ملفّ",
|
||||
@@ -295,6 +296,7 @@
|
||||
"height": "الطّول",
|
||||
"lessInfo": "معلومات أقلّ",
|
||||
"moreInfo": "معلومات أكثر",
|
||||
"pasteURL": "لصق الرابط",
|
||||
"previewSizes": "أحجام المعاينة",
|
||||
"selectCollectionToBrowse": "حدّد مجموعة لاستعراضها",
|
||||
"selectFile": "اختر ملفّ",
|
||||
@@ -381,4 +383,4 @@
|
||||
"viewingVersions": "يتمّ استعراض النُّسَخ ل {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "يتمّ استعراض النُّسَخ للاعداد العامّ {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"near": "yaxın"
|
||||
},
|
||||
"upload": {
|
||||
"addFile": "Fayl əlavə et",
|
||||
"crop": "Məhsul",
|
||||
"cropToolDescription": "Seçilmiş sahənin köşələrini sürükləyin, yeni bir sahə çəkin və ya aşağıdakı dəyərləri düzəltin.",
|
||||
"dragAndDrop": "Faylı buraya sürükləyin və buraxın",
|
||||
@@ -295,6 +296,7 @@
|
||||
"height": "Hündürlük",
|
||||
"lessInfo": "Daha az məlumat",
|
||||
"moreInfo": "Daha çox məlumat",
|
||||
"pasteURL": "URL yapışdır",
|
||||
"previewSizes": "Öncədən baxış ölçüləri",
|
||||
"selectCollectionToBrowse": "Gözdən keçirmək üçün bir Kolleksiya seçin",
|
||||
"selectFile": "Fayl seçin",
|
||||
@@ -381,4 +383,4 @@
|
||||
"viewingVersions": "{{entityLabel}} {{documentTitle}} üçün versiyaları göstərir",
|
||||
"viewingVersionsGlobal": "Qlobal {{entityLabel}} üçün versiyaları göstərir"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"near": "близко"
|
||||
},
|
||||
"upload": {
|
||||
"addFile": "Добавяне на файл",
|
||||
"crop": "Изрязване",
|
||||
"cropToolDescription": "Плъзни ъглите на избраната област, избери нова област или коригирай стойностите по-долу.",
|
||||
"dragAndDrop": "Дръпни и пусни файл",
|
||||
@@ -295,6 +296,7 @@
|
||||
"height": "Височина",
|
||||
"lessInfo": "По-малко информация",
|
||||
"moreInfo": "Повече информация",
|
||||
"pasteURL": "Поставяне на URL",
|
||||
"previewSizes": "Преглед на размери",
|
||||
"selectCollectionToBrowse": "Избери колекция, която да разгледаш",
|
||||
"selectFile": "Избери файл",
|
||||
@@ -381,4 +383,4 @@
|
||||
"viewingVersions": "Гледане на версии за {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Гледане на версии за глобалния документ {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"near": "blízko"
|
||||
},
|
||||
"upload": {
|
||||
"addFile": "Přidat soubor",
|
||||
"crop": "Ořez",
|
||||
"cropToolDescription": "Přetáhněte rohy vybrané oblasti, nakreslete novou oblast nebo upravte níže uvedené hodnoty.",
|
||||
"dragAndDrop": "Přetáhněte soubor",
|
||||
@@ -295,6 +296,7 @@
|
||||
"height": "Výška",
|
||||
"lessInfo": "Méně informací",
|
||||
"moreInfo": "Více informací",
|
||||
"pasteURL": "Vložit URL",
|
||||
"previewSizes": "Náhled velikostí",
|
||||
"selectCollectionToBrowse": "Vyberte kolekci pro procházení",
|
||||
"selectFile": "Vyberte soubor",
|
||||
@@ -381,4 +383,4 @@
|
||||
"viewingVersions": "Zobrazuji verze pro {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Zobrazuji verze pro globální {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"near": "in der Nähe"
|
||||
},
|
||||
"upload": {
|
||||
"addFile": "Datei hinzufügen",
|
||||
"crop": "Zuschneiden",
|
||||
"cropToolDescription": "Ziehen Sie die Ecken des ausgewählten Bereichs, zeichnen Sie einen neuen Bereich oder passen Sie die Werte unten an.",
|
||||
"dragAndDrop": "Ziehen Sie eine Datei per Drag-and-Drop",
|
||||
@@ -295,6 +296,7 @@
|
||||
"height": "Höhe",
|
||||
"lessInfo": "Weniger Info",
|
||||
"moreInfo": "Mehr Info",
|
||||
"pasteURL": "URL einfügen",
|
||||
"previewSizes": "Vorschaugrößen",
|
||||
"selectCollectionToBrowse": "Wähle eine Sammlung zum Durchsuchen aus",
|
||||
"selectFile": "Datei auswählen",
|
||||
@@ -381,4 +383,4 @@
|
||||
"viewingVersions": "Betrachte Versionen für {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Betrachte Versionen für das Globale Dokument {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"near": "near"
|
||||
},
|
||||
"upload": {
|
||||
"addFile": "Add File",
|
||||
"crop": "Crop",
|
||||
"cropToolDescription": "Drag the corners of the selected area, draw a new area or adjust the values below.",
|
||||
"dragAndDrop": "Drag and drop a file",
|
||||
@@ -295,6 +296,7 @@
|
||||
"height": "Height",
|
||||
"lessInfo": "Less info",
|
||||
"moreInfo": "More info",
|
||||
"pasteURL": "Paste URL",
|
||||
"previewSizes": "Preview Sizes",
|
||||
"selectCollectionToBrowse": "Select a Collection to Browse",
|
||||
"selectFile": "Select a file",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"near": "cerca"
|
||||
},
|
||||
"upload": {
|
||||
"addFile": "Añadir archivo",
|
||||
"crop": "Cultivo",
|
||||
"cropToolDescription": "Arrastra las esquinas del área seleccionada, dibuja un nuevo área o ajusta los valores a continuación.",
|
||||
"dragAndDrop": "Arrastra y suelta un archivo",
|
||||
@@ -295,6 +296,7 @@
|
||||
"height": "Alto",
|
||||
"lessInfo": "Menos info",
|
||||
"moreInfo": "Más info",
|
||||
"pasteURL": "Pegar URL",
|
||||
"previewSizes": "Tamaños de Vista Previa",
|
||||
"selectCollectionToBrowse": "Selecciona una Colección",
|
||||
"selectFile": "Selecciona un archivo",
|
||||
@@ -381,4 +383,4 @@
|
||||
"viewingVersions": "Viendo versiones para {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Viendo versiones para el global {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"near": "نزدیک"
|
||||
},
|
||||
"upload": {
|
||||
"addFile": "اضافه کردن فایل",
|
||||
"crop": "محصول",
|
||||
"cropToolDescription": "گوشههای منطقه انتخاب شده را بکشید، یک منطقه جدید رسم کنید یا مقادیر زیر را تنظیم کنید.",
|
||||
"dragAndDrop": "یک سند را بکشید و رها کنید",
|
||||
@@ -295,6 +296,7 @@
|
||||
"height": "ارتفاع",
|
||||
"lessInfo": "اطلاعات کمتر",
|
||||
"moreInfo": "اطلاعات بیشتر",
|
||||
"pasteURL": "چسباندن آدرس اینترنتی",
|
||||
"previewSizes": "اندازه های پیش نمایش",
|
||||
"selectCollectionToBrowse": "یک مجموعه را برای مرور انتخاب کنید",
|
||||
"selectFile": "برگزیدن رسانه",
|
||||
@@ -381,4 +383,4 @@
|
||||
"viewingVersions": "مشاهده نگارشها برای {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "مشاهده نگارشهای کلی {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"near": "proche"
|
||||
},
|
||||
"upload": {
|
||||
"addFile": "Ajouter un fichier",
|
||||
"crop": "Recadrer",
|
||||
"cropToolDescription": "Faites glisser les coins de la zone sélectionnée, dessinez une nouvelle zone ou ajustez les valeurs ci-dessous.",
|
||||
"dragAndDrop": "Glisser-déposer un fichier",
|
||||
@@ -295,6 +296,7 @@
|
||||
"height": "Hauteur",
|
||||
"lessInfo": "Moins d’infos",
|
||||
"moreInfo": "Plus d’infos",
|
||||
"pasteURL": "Coller l'URL",
|
||||
"previewSizes": "Tailles d’aperçu",
|
||||
"selectCollectionToBrowse": "Sélectionnez une collection à parcourir",
|
||||
"selectFile": "Sélectionnez un fichier",
|
||||
@@ -381,4 +383,4 @@
|
||||
"viewingVersions": "Affichage des versions de ou du {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Affichage des versions globales de ou du {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,36 +2,36 @@
|
||||
"$schema": "./translation-schema.json",
|
||||
"authentication": {
|
||||
"account": "Račun",
|
||||
"accountOfCurrentUser": "Račun od trenutnog korisnika",
|
||||
"accountOfCurrentUser": "Račun trenutnog korisnika",
|
||||
"alreadyActivated": "Već aktivirano",
|
||||
"alreadyLoggedIn": "Već prijavljen",
|
||||
"alreadyLoggedIn": "Već prijavljeni",
|
||||
"apiKey": "API ključ",
|
||||
"authenticated": "Autenticiran",
|
||||
"backToLogin": "Nazad na prijavu",
|
||||
"beginCreateFirstUser": "Za početak, kreiraj svog prvog korisnika.",
|
||||
"backToLogin": "Natrag na prijavu",
|
||||
"beginCreateFirstUser": "Za početak, kreirajte prvog korisnika.",
|
||||
"changePassword": "Promjeni lozinku",
|
||||
"checkYourEmailForPasswordReset": "Provjerite email s poveznicom koja će Vam omogućiti sigurnu promjenu lozinke.",
|
||||
"confirmGeneration": "Potvrdi kreiranje",
|
||||
"checkYourEmailForPasswordReset": "Provjerite e-mail s poveznicom koja će vam omogućiti sigurnu promjenu lozinke.",
|
||||
"confirmGeneration": "Potvrdi generiranje",
|
||||
"confirmPassword": "Potvrdi lozinku",
|
||||
"createFirstUser": "Kreiraj prvog korisnika",
|
||||
"emailNotValid": "Email nije ispravan",
|
||||
"emailSent": "Email poslan",
|
||||
"emailNotValid": "E-mail adresa nije ispravna",
|
||||
"emailSent": "E-mail poslan",
|
||||
"enableAPIKey": "Omogući API ključ",
|
||||
"failedToUnlock": "Neuspješno otključavanje.",
|
||||
"failedToUnlock": "Otključavanje nije uspjelo.",
|
||||
"forceUnlock": "Prisilno otključaj",
|
||||
"forgotPassword": "Zaboravljena lozinka",
|
||||
"forgotPasswordEmailInstructions": "Molim unesite svoj email. Primit ćete poruku s uputama za ponovno postavljanje lozinke.",
|
||||
"forgotPasswordEmailInstructions": "Molimo unesite svoju e-mail adresu. Primit ćete poruku s uputama za ponovno postavljanje lozinke.",
|
||||
"forgotPasswordQuestion": "Zaboravljena lozinka?",
|
||||
"generate": "Generiraj",
|
||||
"generateNewAPIKey": "Generiraj novi API ključ",
|
||||
"generatingNewAPIKeyWillInvalidate": "Generiranje novog API ključa će <1>poništiti</1> prethodni ključ. Jeste li sigurni da želite nastaviti?",
|
||||
"lockUntil": "Zaključaj dok",
|
||||
"logBackIn": "Ponovna prijava",
|
||||
"logBackIn": "Ponovo se prijavite",
|
||||
"logOut": "Odjava",
|
||||
"loggedIn": "Za prijavu s drugim korisničkim računom potrebno je prvo <0>odjaviti se</0>",
|
||||
"loggedInChangePassword": "Da biste promijenili lozinku, otvorite svoj <0>račun</0> i promijenite lozinku tamo.",
|
||||
"loggedOutInactivity": "Odjavljeni se zbog neaktivnosti.",
|
||||
"loggedOutSuccessfully": "Uspješno ste odjavljeni..",
|
||||
"loggedIn": "Za prijavu s drugim korisničkim računom potrebno se prvo <0>odjaviti</0>",
|
||||
"loggedInChangePassword": "Da biste promijenili lozinku, otvorite svoj <0>račun</0> i promijenite je tamo.",
|
||||
"loggedOutInactivity": "Odjavljeni ste zbog neaktivnosti.",
|
||||
"loggedOutSuccessfully": "Uspješno ste odjavljeni.",
|
||||
"login": "Prijava",
|
||||
"loginAttempts": "Pokušaji prijave",
|
||||
"loginUser": "Prijava korisnika",
|
||||
@@ -39,32 +39,32 @@
|
||||
"logout": "Odjava",
|
||||
"logoutUser": "Odjava korisnika",
|
||||
"newAPIKeyGenerated": "Novi API ključ generiran.",
|
||||
"newAccountCreated": "Novi račun je kreiran. Pristupite računu klikom na <a href=\"{{serverURL}}\">{{serverURL}}</a>. Molim kliknite na sljedeći link ili zalijepite URL, koji se nalazi ispod, u preglednik da biste potvrdili svoj email: <a href=\"{{verificationURL}}\">{{verificationURL}}</a><br> Nakon što potvrdite email, moći ćete se prijaviti.",
|
||||
"newAccountCreated": "Novi račun je kreiran. Pristupite računu klikom na: <a href=\"{{serverURL}}\">{{serverURL}}</a>. Molimo kliknite na sljedeću poveznicu ili zalijepite URL, koji se nalazi ispod, u preglednik da biste potvrdili svoju e-mail adresu: <a href=\"{{verificationURL}}\">{{verificationURL}}</a><br> Nakon što potvrdite e-mail adresu, moći ćete se prijaviti.",
|
||||
"newPassword": "Nova lozinka",
|
||||
"resetPassword": "Restartiranje lozinke",
|
||||
"resetPasswordExpiration": "Restartiranje roka trajanja lozinke",
|
||||
"resetPasswordToken": "Restartiranje lozinke tokena",
|
||||
"resetYourPassword": "Restartiraj svoju lozinku",
|
||||
"stayLoggedIn": "Ostani prijavljen",
|
||||
"resetPassword": "Resetiranje lozinke",
|
||||
"resetPasswordExpiration": "Rok trajanja resetiranja lozinke",
|
||||
"resetPasswordToken": "Resetiranje lozinke tokena",
|
||||
"resetYourPassword": "Resetirajte svoju lozinku",
|
||||
"stayLoggedIn": "Ostanite prijavljeni",
|
||||
"successfullyUnlocked": "Uspješno otključano",
|
||||
"unableToVerify": "Nije moguće potvrditi",
|
||||
"verified": "Potvrđeno",
|
||||
"verifiedSuccessfully": "Uspješno potvrđeno",
|
||||
"verify": "Potvrdi",
|
||||
"verifyUser": "Potvrdi korisnika",
|
||||
"verifyYourEmail": "Potvrdi svoj email",
|
||||
"verifyYourEmail": "Potvrdi svoju e-mail adresu",
|
||||
"youAreInactive": "Neaktivni ste neko vrijeme i uskoro ćete biti automatski odjavljeni zbog vlastite sigurnosti. Želite li ostati prijavljeni?",
|
||||
"youAreReceivingResetPassword": "Primili ste ovo jer ste Vi (ili netko drugi) zatražili promjenu lozinke za Vaš račun. Molim kliknite na poveznicu ili zalijepite ovo u svoje preglednik da biste završili proces:",
|
||||
"youDidNotRequestPassword": "Ako niste zatražili ovo, molim ignorirajte ovaj email i Vaša lozinka ostat će nepromijenjena."
|
||||
"youAreReceivingResetPassword": "Primili ste ovo jer ste Vi (ili netko drugi) zatražili promjenu lozinke za Vaš račun. Molimo kliknite na poveznicu ili zalijepite ovo u svoje preglednik da biste završili proces:",
|
||||
"youDidNotRequestPassword": "Ako niste zatražili ovo, molimo ignorirajte ovaj e-mail i Vaša će lozinka ostati nepromijenjena."
|
||||
},
|
||||
"error": {
|
||||
"accountAlreadyActivated": "Ovaj račun je već aktiviran.",
|
||||
"autosaving": "Nastao je problem pri automatskom spremanju ovog dokumenta.",
|
||||
"correctInvalidFields": "Molim ispravite nevaljana polja.",
|
||||
"correctInvalidFields": "Molimo ispravite nevaljana polja.",
|
||||
"deletingFile": "Dogodila se pogreška pri brisanju datoteke.",
|
||||
"deletingTitle": "Dogodila se pogreška pri brisanju {{title}}. Molim provjerite svoju internetsku vezu i pokušajte ponovno.",
|
||||
"emailOrPasswordIncorrect": "Email ili lozinka netočni.",
|
||||
"followingFieldsInvalid_one": " Ovo polje je nevaljano:",
|
||||
"deletingTitle": "Dogodila se pogreška pri brisanju {{title}}. Molimo provjerite svoju internet vezu i pokušajte ponovno.",
|
||||
"emailOrPasswordIncorrect": "E-mail adresa ili lozinka netočni.",
|
||||
"followingFieldsInvalid_one": "Ovo polje je nevaljano:",
|
||||
"followingFieldsInvalid_other": "Ova polja su nevaljana:",
|
||||
"incorrectCollection": "Nevaljana kolekcija",
|
||||
"invalidFileType": "Nevaljan tip datoteke",
|
||||
@@ -72,7 +72,7 @@
|
||||
"loadingDocument": "Pojavio se problem pri učitavanju dokumenta čiji je ID {{id}}.",
|
||||
"localesNotSaved_one": "Sljedeću lokalnu postavku nije bilo moguće spremiti:",
|
||||
"localesNotSaved_other": "Sljedeće lokalne postavke nije bilo moguće spremiti:",
|
||||
"missingEmail": "Nedostaje email.",
|
||||
"missingEmail": "Nedostaje e-mail.",
|
||||
"missingIDOfDocument": "Nedostaje ID dokumenta da bi se ažurirao.",
|
||||
"missingIDOfVersion": "Nedostaje ID verzije.",
|
||||
"missingRequiredData": "Nedostaju obvezni podaci.",
|
||||
@@ -88,10 +88,10 @@
|
||||
"unPublishingDocument": "Pojavio se problem pri poništavanju objave ovog dokumenta.",
|
||||
"unableToDeleteCount": "Nije moguće izbrisati {{count}} od {{total}} {{label}}.",
|
||||
"unableToUpdateCount": "Nije moguće ažurirati {{count}} od {{total}} {{label}}.",
|
||||
"unauthorized": "Neovlašten, morate biti prijavljeni da biste uputili ovaj zahtjev.",
|
||||
"unauthorized": "Neovlašteno, morate biti prijavljeni da biste uputili ovaj zahtjev.",
|
||||
"unknown": "Došlo je do nepoznate pogreške.",
|
||||
"unspecific": "Došlo je do pogreške.",
|
||||
"userEmailAlreadyRegistered": "Korisnik s navedenom e-poštom je već registriran.",
|
||||
"userEmailAlreadyRegistered": "Korisnik s navedenom e-mail adresom je već registriran.",
|
||||
"userLocked": "Ovaj korisnik je zaključan zbog previše neuspješnih pokušaja prijave.",
|
||||
"valueMustBeUnique": "Vrijednost mora biti jedinstvena.",
|
||||
"verificationTokenInvalid": "Verifikacijski token je nevaljan."
|
||||
@@ -121,18 +121,18 @@
|
||||
"labelRelationship": "{{label}} veza",
|
||||
"latitude": "Zemljopisna širina",
|
||||
"linkType": "Tip poveznce",
|
||||
"linkedTo": "Povezabi sa <0>{{label}}</0>",
|
||||
"linkedTo": "Povezan s <0>{{label}}</0>",
|
||||
"longitude": "Zemljopisna dužina",
|
||||
"newLabel": "Novo {{label}}",
|
||||
"openInNewTab": "Otvori u novoj kartici.",
|
||||
"passwordsDoNotMatch": "Lozinke nisu iste.",
|
||||
"passwordsDoNotMatch": "Lozinke nisu jednake.",
|
||||
"relatedDocument": "Povezani dokument",
|
||||
"relationTo": "Veza sa",
|
||||
"removeRelationship": "Ukloni vezu",
|
||||
"removeUpload": "Ukloni prijenos",
|
||||
"saveChanges": "Spremi promjene",
|
||||
"searchForBlock": "Potraži blok",
|
||||
"selectExistingLabel": "Odaberi postojeće{{label}}",
|
||||
"selectExistingLabel": "Odaberi postojeće {{label}}",
|
||||
"selectFieldsToEdit": "Odaberite polja za uređivanje",
|
||||
"showAll": "Pokaži sve",
|
||||
"swapRelationship": "Zamijeni vezu",
|
||||
@@ -149,7 +149,7 @@
|
||||
"addBelow": "Dodaj ispod",
|
||||
"addFilter": "Dodaj filter",
|
||||
"adminTheme": "Administratorska tema",
|
||||
"and": "I",
|
||||
"and": "i",
|
||||
"applyChanges": "Primijeni promjene",
|
||||
"ascending": "Uzlazno",
|
||||
"automatic": "Automatsko",
|
||||
@@ -177,7 +177,7 @@
|
||||
"dashboard": "Nadzorna ploča",
|
||||
"delete": "Obriši",
|
||||
"deletedCountSuccessfully": "Uspješno izbrisano {{count}} {{label}}.",
|
||||
"deletedSuccessfully": "Uspješno obrisano.",
|
||||
"deletedSuccessfully": "Uspješno izbrisano.",
|
||||
"deleting": "Brisanje...",
|
||||
"depth": "Dubina",
|
||||
"descending": "Silazno",
|
||||
@@ -192,8 +192,8 @@
|
||||
"editingLabel_many": "Uređivanje {{count}} {{label}}",
|
||||
"editingLabel_one": "Uređivanje {{count}} {{label}}",
|
||||
"editingLabel_other": "Uređivanje {{count}} {{label}}",
|
||||
"email": "Email",
|
||||
"emailAddress": "Email adresa",
|
||||
"email": "E-mail",
|
||||
"emailAddress": "E-mail adresa",
|
||||
"enterAValue": "Unesi vrijednost",
|
||||
"error": "Greška",
|
||||
"errors": "Greške",
|
||||
@@ -224,9 +224,9 @@
|
||||
"none": "Nijedan",
|
||||
"notFound": "Nije pronađeno",
|
||||
"nothingFound": "Ništa nije pronađeno",
|
||||
"of": "Od",
|
||||
"of": "od",
|
||||
"open": "Otvori",
|
||||
"or": "Ili",
|
||||
"or": "ili",
|
||||
"order": "Poredak",
|
||||
"pageNotFound": "Stranica nije pronađena",
|
||||
"password": "Lozinka",
|
||||
@@ -283,7 +283,8 @@
|
||||
"near": "blizu"
|
||||
},
|
||||
"upload": {
|
||||
"crop": "Usjev",
|
||||
"addFile": "Dodaj datoteku",
|
||||
"crop": "Izreži",
|
||||
"cropToolDescription": "Povucite kutove odabranog područja, nacrtajte novo područje ili prilagodite vrijednosti ispod.",
|
||||
"dragAndDrop": "Povucite i ispustite datoteku",
|
||||
"dragAndDropHere": "ili povucite i ispustite datoteku ovdje",
|
||||
@@ -295,6 +296,7 @@
|
||||
"height": "Visina",
|
||||
"lessInfo": "Manje informacija",
|
||||
"moreInfo": "Više informacija",
|
||||
"pasteURL": "Zalijepi URL",
|
||||
"previewSizes": "Veličine pregleda",
|
||||
"selectCollectionToBrowse": "Odaberite kolekciju za pregled",
|
||||
"selectFile": "Odaberite datoteku",
|
||||
@@ -305,8 +307,8 @@
|
||||
"width": "Širina"
|
||||
},
|
||||
"validation": {
|
||||
"emailAddress": "Molim unestie valjanu email adresu.",
|
||||
"enterNumber": "Molim unesite valjani broj.",
|
||||
"emailAddress": "Molimo unesite valjanu e-mail adresu.",
|
||||
"enterNumber": "Molimo unesite valjani broj.",
|
||||
"fieldHasNo": "Ovo polje nema {{label}}",
|
||||
"greaterThanMax": "{{value}} exceeds the maximum allowable {{label}} limit of {{max}}.",
|
||||
"invalidInput": "Ovo polje ima nevaljan unos.",
|
||||
@@ -325,12 +327,12 @@
|
||||
"validUploadID": "Ovo polje nije valjani ID prijenosa."
|
||||
},
|
||||
"version": {
|
||||
"aboutToPublishSelection": "Upravo ćete objaviti sve {{label}} u izboru. Jesi li siguran?",
|
||||
"aboutToPublishSelection": "Upravo ćete objaviti sve {{label}} u izboru. Jeste li sigurani?",
|
||||
"aboutToRestore": "Vratit ćete {{label}} dokument u stanje u kojem je bio {{versionDate}}",
|
||||
"aboutToRestoreGlobal": "Vratit ćete globalni {{label}} u stanje u kojem je bio {{versionDate}}.",
|
||||
"aboutToRevertToPublished": "Vratit ćete promjene u dokumentu u objavljeno stanje. Jeste li sigurni? ",
|
||||
"aboutToUnpublish": "Poništit ćete objavu ovog dokumenta. Jeste li sigurni?",
|
||||
"aboutToUnpublishSelection": "Upravo ćete poništiti objavu svih {{label}} u odabiru. Jesi li siguran?",
|
||||
"aboutToUnpublishSelection": "Upravo ćete poništiti objavu svih {{label}} u odabiru. Jeste li sigurni?",
|
||||
"autosave": "Automatsko spremanje",
|
||||
"autosavedSuccessfully": "Automatsko spremanje uspješno.",
|
||||
"autosavedVersion": "Verzija automatski spremljenog dokumenta",
|
||||
@@ -341,8 +343,8 @@
|
||||
"confirmUnpublish": "Potvrdite poništavanje objave",
|
||||
"confirmVersionRestoration": "Potvrdite vraćanje verzije",
|
||||
"currentDocumentStatus": "Trenutni {{docStatus}} dokumenta",
|
||||
"currentDraft": "Trenutačni nacrt",
|
||||
"currentPublishedVersion": "Trenutačno objavljena verzija",
|
||||
"currentDraft": "Trenutni nacrt",
|
||||
"currentPublishedVersion": "Trenutno objavljena verzija",
|
||||
"draft": "Nacrt",
|
||||
"draftSavedSuccessfully": "Nacrt uspješno spremljen.",
|
||||
"lastSavedAgo": "Zadnji put spremljeno prije {{distance}",
|
||||
@@ -381,4 +383,4 @@
|
||||
"viewingVersions": "Pregled verzija za {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "Pregled verzije za globalni {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"near": "közel"
|
||||
},
|
||||
"upload": {
|
||||
"addFile": "Fájl hozzáadása",
|
||||
"crop": "Termés",
|
||||
"cropToolDescription": "Húzza a kijelölt terület sarkait, rajzoljon új területet, vagy igazítsa a lentebb található értékeket.",
|
||||
"dragAndDrop": "Húzzon ide egy fájlt",
|
||||
@@ -295,6 +296,7 @@
|
||||
"height": "Magasság",
|
||||
"lessInfo": "Kevesebb információ",
|
||||
"moreInfo": "További információ",
|
||||
"pasteURL": "URL beillesztése",
|
||||
"previewSizes": "Előnézeti méretek",
|
||||
"selectCollectionToBrowse": "Válassza ki a böngészni kívánt gyűjteményt",
|
||||
"selectFile": "Válasszon ki egy fájlt",
|
||||
@@ -381,4 +383,4 @@
|
||||
"viewingVersions": "A {{entityLabel}} {{documentTitle}} verzióinak megtekintése",
|
||||
"viewingVersionsGlobal": "A globális {{entityLabel}} verzióinak megtekintése"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,6 +284,7 @@
|
||||
"near": "vicino"
|
||||
},
|
||||
"upload": {
|
||||
"addFile": "Aggiungi file",
|
||||
"crop": "Raccolto",
|
||||
"cropToolDescription": "Trascina gli angoli dell'area selezionata, disegna una nuova area o regola i valori qui sotto.",
|
||||
"dragAndDrop": "Trascina e rilascia un file",
|
||||
@@ -296,6 +297,7 @@
|
||||
"height": "Altezza",
|
||||
"lessInfo": "Meno info",
|
||||
"moreInfo": "Più info",
|
||||
"pasteURL": "Incolla URL",
|
||||
"previewSizes": "Anteprime Dimensioni",
|
||||
"selectCollectionToBrowse": "Seleziona una Collezione da Sfogliare",
|
||||
"selectFile": "Seleziona un file",
|
||||
@@ -382,4 +384,4 @@
|
||||
"viewingVersions": "Visualizzazione delle versioni per {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "`Visualizzazione delle versioni per {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"near": "近く"
|
||||
},
|
||||
"upload": {
|
||||
"addFile": "ファイルを追加",
|
||||
"crop": "クロップ",
|
||||
"cropToolDescription": "選択したエリアのコーナーをドラッグしたり、新たなエリアを描画したり、下記の値を調整してください。",
|
||||
"dragAndDrop": "ファイルをドラッグ アンド ドロップする",
|
||||
@@ -295,6 +296,7 @@
|
||||
"height": "高さ",
|
||||
"lessInfo": "詳細を隠す",
|
||||
"moreInfo": "詳細を表示",
|
||||
"pasteURL": "URLを貼り付け",
|
||||
"previewSizes": "プレビューサイズ",
|
||||
"selectCollectionToBrowse": "閲覧するコレクションを選択",
|
||||
"selectFile": "ファイルを選択",
|
||||
@@ -381,4 +383,4 @@
|
||||
"viewingVersions": "表示バージョン: {{entityLabel}} {{documentTitle}}",
|
||||
"viewingVersionsGlobal": "表示バージョン: グローバルな {{entityLabel}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"near": "근처"
|
||||
},
|
||||
"upload": {
|
||||
"addFile": "파일 추가",
|
||||
"crop": "자르기",
|
||||
"cropToolDescription": "선택한 영역의 모퉁이를 드래그하거나 새로운 영역을 그리거나 아래의 값을 조정하세요.",
|
||||
"dragAndDrop": "파일을 끌어다 놓으세요",
|
||||
@@ -295,6 +296,7 @@
|
||||
"height": "높이",
|
||||
"lessInfo": "정보 숨기기",
|
||||
"moreInfo": "정보 더보기",
|
||||
"pasteURL": "URL 붙여넣기",
|
||||
"previewSizes": "미리보기 크기",
|
||||
"selectCollectionToBrowse": "찾을 컬렉션 선택",
|
||||
"selectFile": "파일 선택",
|
||||
@@ -381,4 +383,4 @@
|
||||
"viewingVersions": "{{entityLabel}} {{documentTitle}}에 대한 버전 보기",
|
||||
"viewingVersionsGlobal": "글로벌 {{entityLabel}}에 대한 버전 보기"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user