mirror of
https://github.com/strapi/strapi.git
synced 2025-12-27 23:24:03 +00:00
commit
c2fb4904cd
@ -43,6 +43,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'import/no-unresolved': 0,
|
||||
'generator-star-spacing': 0,
|
||||
'no-console': 0,
|
||||
'require-atomic-updates': 0,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
const frontPaths = [
|
||||
'packages/**/admin/src/**/**/*.js',
|
||||
'packages/**/ee/admin/**/**/*.js',
|
||||
'packages/strapi-helper-plugin/**/*.js',
|
||||
'packages/**/test/front/**/*.js',
|
||||
'test/config/front/**/*.js',
|
||||
|
||||
@ -6,7 +6,7 @@ All efforts to contribute are highly appreciated, we recommend you talk to a mai
|
||||
|
||||
## Open Development & Community Driven
|
||||
|
||||
Strapi is open-source under the [MIT license](https://github.com/strapi/strapi/blob/master/LICENSE.md). All the work done is available on GitHub.
|
||||
Strapi is an open-source project. See the [LICENSE](https://github.com/strapi/strapi/blob/master/LICENSE) file for licensing information. All the work done is available on GitHub.
|
||||
|
||||
The core team and the contributors send pull requests which go through the same validation process.
|
||||
|
||||
|
||||
22
LICENSE
Normal file
22
LICENSE
Normal file
@ -0,0 +1,22 @@
|
||||
Copyright (c) 2015-present Strapi Solutions SAS
|
||||
|
||||
Portions of the Strapi software are licensed as follows:
|
||||
|
||||
* All software that resides under an "ee/" directory (the “EE Software”), if that directory exists, is licensed under the license defined in "ee/LICENSE".
|
||||
|
||||
* All software outside of the above-mentioned directories or restrictions above is available under the "MIT Expat" license as set forth below.
|
||||
|
||||
MIT Expat License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -1,7 +0,0 @@
|
||||
Copyright (c) 2015-2020 Strapi Solutions.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@ -151,4 +151,4 @@ Check out our [roadmap](https://portal.productboard.com/strapi) to get informed
|
||||
|
||||
## License
|
||||
|
||||
[MIT License](LICENSE.md) Copyright (c) 2015-2020 [Strapi Solutions](https://strapi.io/).
|
||||
See the [LICENSE](./LICENSE) file for licensing information.
|
||||
|
||||
@ -223,6 +223,7 @@ module.exports = {
|
||||
'/v3.x/admin-panel/customization',
|
||||
'/v3.x/admin-panel/custom-webpack-config',
|
||||
'/v3.x/admin-panel/deploy',
|
||||
'/v3.x/admin-panel/forgot-password',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -44,7 +44,7 @@ Should you decide to opt-out, you may do so by removing the `uuid` property in t
|
||||
"strapi": {
|
||||
"uuid": "7b581c0d-89b7-479e-b379-a76ab90b8754"
|
||||
},
|
||||
"license": "MIT"
|
||||
"license": "SEE LICENSE IN LICENSE"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
"dev": "vuepress dev",
|
||||
"build": "vuepress build"
|
||||
},
|
||||
"license": "MIT",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@vuepress/plugin-medium-zoom": "^1.2.0",
|
||||
"@vuepress/plugin-google-analytics": "^1.2.0",
|
||||
|
||||
48
docs/v3.x/admin-panel/forgot-password.md
Normal file
48
docs/v3.x/admin-panel/forgot-password.md
Normal file
@ -0,0 +1,48 @@
|
||||
# Forgot Password Email
|
||||
|
||||
## Customize forgot password email
|
||||
|
||||
You may want to customize the forgot password email.
|
||||
You can do it by providing your own template (formatted as a [lodash template](https://lodash.com/docs/4.17.15#template)).
|
||||
|
||||
The template will be compiled with the following variables: `url`, `user.email`, `user.username`, `user.firstname`, `user.lastname`.
|
||||
|
||||
### Example
|
||||
|
||||
**Path -** `./config/servers.js`
|
||||
|
||||
```js
|
||||
const forgotPasswordTemplate = require('./email-templates/forgot-password');
|
||||
|
||||
module.exports = ({ env }) => ({
|
||||
// ...
|
||||
admin: {
|
||||
// ...
|
||||
forgotPassword: {
|
||||
from: 'support@mywebsite.fr',
|
||||
replyTo: 'support@mywebsite.fr',
|
||||
emailTemplate: forgotPasswordTemplate,
|
||||
},
|
||||
// ...
|
||||
},
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
**Path -** `./config/email-templates/forgot-password.js`
|
||||
|
||||
```js
|
||||
const subject = `Reset password`;
|
||||
|
||||
const html = `<p>Hi <%= user.firstname %></p>
|
||||
<p>Sorry you lost your password. You can click here to reset it: <%= url %></p>`;
|
||||
|
||||
const text = `Hi <%= user.firstname %>
|
||||
Sorry you lost your password. You can click here to reset it: <%= url %>`;
|
||||
|
||||
module.exports = {
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
};
|
||||
```
|
||||
@ -167,22 +167,27 @@ module.exports = ({ env }) => ({
|
||||
```
|
||||
|
||||
**Available options**
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | ----------- |
|
||||
| `host` | Host name | string | `localhost` |
|
||||
| `port` | Port on which the server should be running. | integer | `1337` |
|
||||
| `emitErrors` | Enable errors to be emitted to `koa` when they happen in order to attach custom logic or use error reporting services. | boolean | `false` |
|
||||
| `url` | Url of the server. Enable proxy support such as Apache or Nginx, example: `https://mywebsite.com/api`. The url can be relative, if so, it is used with `http://${host}:${port}` as the base url. | string | `''` |
|
||||
| `cron` | Cron configuration (powered by [`node-schedule`](https://github.com/node-schedule/node-schedule)) | Object | |
|
||||
| `cron.enabled` | Enable or disable CRON tasks to schedule jobs at specific dates. | boolean | `false` |
|
||||
| `admin` | Admin panel configuration | Object | |
|
||||
| `admin.url` | Url of your admin panel. Default value: `/admin`. Note: If the url is relative, it will be concatenated with `url`. | string | `/admin` |
|
||||
| `admin.autoOpen` | Enable or disabled administration opening on start. | boolean | `true` |
|
||||
| `admin.watchIgnoreFiles` | Add custom files that should not be watched during development. See more [here](https://github.com/paulmillr/chokidar#path-filtering) (property `ignored`). | Array(string) | `[]` |
|
||||
| `admin.host` | Use a different host for the admin panel. Only used along with `strapi develop --watch-admin` | string | `localhost` |
|
||||
| `admin.port` | Use a different port for the admin panel. Only used along with `strapi develop --watch-admin` | string | `8000` |
|
||||
| `admin.serveAdminPanel` | If false, the admin panel won't be served. Note: the `index.html` will still be served, see [defaultIndex option](./middlewares#global-middlewares) | boolean | `true` |
|
||||
| Property | Description | Type | Default |
|
||||
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `host` | Host name | string | `localhost` |
|
||||
| `port` | Port on which the server should be running. | integer | `1337` |
|
||||
| `emitErrors` | Enable errors to be emitted to `koa` when they happen in order to attach custom logic or use error reporting services. | boolean | `false` |
|
||||
| `url` | Url of the server. Enable proxy support such as Apache or Nginx, example: `https://mywebsite.com/api`. The url can be relative, if so, it is used with `http://${host}:${port}` as the base url. | string | `''` |
|
||||
| `cron` | Cron configuration (powered by [`node-schedule`](https://github.com/node-schedule/node-schedule)) | Object | |
|
||||
| `cron.enabled` | Enable or disable CRON tasks to schedule jobs at specific dates. | boolean | `false` |
|
||||
| `admin` | Admin panel configuration | Object | |
|
||||
| `admin.auth` | Authentication configuration | Object | |
|
||||
| `admin.auth.secret`| Secret used to encode JWT tokens | string| `undefined` |
|
||||
| `admin.url` | Url of your admin panel. Default value: `/admin`. Note: If the url is relative, it will be concatenated with `url`. | string | `/admin` |
|
||||
| `admin.autoOpen` | Enable or disabled administration opening on start. | boolean | `true` |
|
||||
| `admin.watchIgnoreFiles` | Add custom files that should not be watched during development. See more [here](https://github.com/paulmillr/chokidar#path-filtering) (property `ignored`). | Array(string) | `[]` |
|
||||
| `admin.host` | Use a different host for the admin panel. Only used along with `strapi develop --watch-admin` | string | `localhost` |
|
||||
| `admin.port` | Use a different port for the admin panel. Only used along with `strapi develop --watch-admin` | string | `8000` |
|
||||
| `admin.serveAdminPanel` | If false, the admin panel won't be served. Note: the `index.html` will still be served, see [defaultIndex option](./middlewares#global-middlewares) | boolean | `true` |
|
||||
| `admin.forgotPassword` | Settings to customize the forgot password email (see more here: [Forgot Password Email](../admin-panel/forgot-password)) | Object | {} |
|
||||
| `admin.forgotPassword.emailTemplate` | Email template as defined in [email plugin](../plugins/email#create-an-email-from-a-template-fillemailoptions) | Object | [Default template](https://github.com/strapi/strapi/tree/master/packages/strapi-admin/config/email-templates/forgot-password.js) |
|
||||
| `admin.forgotPassword.from` | Sender mail address | string | Default value defined in your [provider configuration](../plugins/email#configure-your-provider) |
|
||||
| `admin.forgotPassword.replyTo` | Default address or addresses the receiver is asked to reply to | string | Default value defined in your [provider configuration](../plugins/email#configure-your-provider) |
|
||||
|
||||
## Functions
|
||||
|
||||
|
||||
@ -20,6 +20,8 @@ The available operators are separated in four different categories:
|
||||
|
||||
## Filters
|
||||
|
||||
When using filters you can either pass simple filters in the root of the query parameters or pass them in a `_where` parameter.
|
||||
|
||||
Filters are used as a suffix of a field name:
|
||||
|
||||
- No suffix or `eq`: Equals
|
||||
@ -52,18 +54,137 @@ or
|
||||
|
||||
`GET /restaurants?id_in=3&id_in=6&id_in=8`
|
||||
|
||||
#### Or clauses
|
||||
#### Using `_where`
|
||||
|
||||
If you use the same operator (except for in and nin) the values will be used to build an `OR` query
|
||||
`GET /restaurants?_where[price_gte]=3`
|
||||
|
||||
`GET /restaurants?name_contains=pizza&name_contains=giovanni`
|
||||
`GET /restaurants?_where[0][price_gte]=3&[0][price_lte]=7`
|
||||
|
||||
## Complexe queries
|
||||
|
||||
When building more complexe queries you must use the `_where` query parameter in combination with the [`qs`](https://github.com/ljharb/qs) library.
|
||||
|
||||
We are taking advantage of the capability of `qs` to parse nested objects to create more complexe queries.
|
||||
|
||||
This will give you full power to create complexe queries with logical `AND` and `OR` operations.
|
||||
|
||||
::: tip NOTE
|
||||
We strongly recommend using `qs` directly to generate complexe queries instead of creating them manually.
|
||||
:::
|
||||
|
||||
### `AND` operator
|
||||
|
||||
The filtering implicitly supports the `AND` operation when specifying an array of expressions in the filtering.
|
||||
|
||||
**Examples**
|
||||
|
||||
Restaurants that have 1 `stars` and a `pricing` less than or equal to 20:
|
||||
|
||||
```js
|
||||
const query = qs.stringify({
|
||||
_where: [{ stars: 1 }, { pricing_lte: 20 }],
|
||||
});
|
||||
|
||||
await request(`/restaurants?${query}`);
|
||||
// GET /restaurants?_where[0][stars]=1&_where[1][pricing_lte]=20
|
||||
```
|
||||
|
||||
Restaurants that have a `pricing` greater than or equal to 20 and a `pricing` less than or equal to 50:
|
||||
|
||||
```js
|
||||
const query = qs.stringify({
|
||||
_where: [{ pricing_gte: 20 }, { pricing_lte: 50 }],
|
||||
});
|
||||
|
||||
await request(`/restaurants?${query}`);
|
||||
// GET /restaurants?_where[0][pricing_gte]=20&_where[1][pricing_lte]=50
|
||||
```
|
||||
|
||||
### `OR` operator
|
||||
|
||||
To use the `OR` operation, you will need to use the `_or` filter and specify an array of expressions on which to perform the operation.
|
||||
|
||||
**Examples**
|
||||
|
||||
Restaurants that have 1 `stars` OR a `pricing` greater than 30:
|
||||
|
||||
```js
|
||||
const query = qs.stringify({ _where: { _or: [{ stars: 1 }, { pricing_gt: 30 }] } });
|
||||
|
||||
await request(`/restaurant?${query}`);
|
||||
// GET /restaurants?_where[_or][0][stars]=1&_where[_or][1][pricing_gt]=30
|
||||
```
|
||||
|
||||
Restaurants that have a `pricing` less than 10 OR greater than 30:
|
||||
|
||||
```js
|
||||
const query = qs.stringify({ _where: { _or: [{ pricing_lt: 10 }, { pricing_gt: 30 }] } });
|
||||
|
||||
await request(`/restaurant?${query}`);
|
||||
// GET /restaurants?_where[_or][0][pricing_lt]=10&_where[_or][1][pricing_gt]=30
|
||||
```
|
||||
|
||||
### Implicit `OR` operator
|
||||
|
||||
The query engine implicitly uses the `OR` operation when you pass an array of values in an expression.
|
||||
|
||||
**Examples**
|
||||
|
||||
Restaurants that have 1 or 2 `stars`:
|
||||
|
||||
`GET /restaurants?stars=1&stars=2`
|
||||
|
||||
or
|
||||
|
||||
```js
|
||||
const query = qs.stringify({ _where: { stars: [1, 2] } });
|
||||
|
||||
await request(`/restaurant?${query}`);
|
||||
// GET /restaurants?_where[stars][0]=1&_where[stars][1]=2
|
||||
```
|
||||
|
||||
::: tip NOTE
|
||||
When using the `in` and `nin` filters the array is not transformed into a OR.
|
||||
:::
|
||||
|
||||
### Combining AND and OR operators
|
||||
|
||||
Restaurants that have (2 `stars` AND a `pricing` less than 80) OR (1 `stars` AND a `pricing` greater than or equal to 50)
|
||||
|
||||
```js
|
||||
const query = qs.stringify({
|
||||
_where: {
|
||||
_or: [
|
||||
[{ stars: 2 }, { pricing_lt: 80 }], // implicit AND
|
||||
[{ stars: 1 }, { pricing_gte: 50 }], // implicit AND
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await request(`/restaurants?${query}`);
|
||||
// GET /restaurants?_where[_or][0][0][stars]=2&_where[_or][0][1][pricing_lt]=80&_where[_or][1][0][stars]=1&_where[_or][1][1][pricing_gte]=50
|
||||
```
|
||||
|
||||
**This also works with deep filtering**
|
||||
|
||||
Restaurants that have (2 `stars` AND a `pricing` less than 80) OR (1 `stars` AND serves French food)
|
||||
|
||||
```js
|
||||
const query = qs.stringify({
|
||||
_where: {
|
||||
_or: [
|
||||
[{ stars: 2 }, { pricing_lt: 80 }], // implicit AND
|
||||
[{ stars: 1 }, { 'categories.name': 'French' }], // implicit AND
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await request(`/restaurants?${query}`);
|
||||
// GET /restaurants?_where[_or][0][0][stars]=2&_where[_or][0][1][pricing_lt]=80&_where[_or][1][0][stars]=1&_where[_or][1][1][categories.name]=French
|
||||
```
|
||||
|
||||
::: warning
|
||||
|
||||
If you want to use `AND` for your query you will have to create a custom query by using the [Query](../concepts/queries.md) documentation.
|
||||
|
||||
Strapi doesn't support the `AND` operator for now.
|
||||
|
||||
When creating nested queries, make sure the depth is less than 20 or the query string parsing will fail for now.
|
||||
:::
|
||||
|
||||
## Deep filtering
|
||||
|
||||
@ -43,7 +43,7 @@ Don't be bogged down by tech you don't need!
|
||||
|
||||
### Source Code
|
||||
|
||||
Strapi is an open-source project, all of Strapi's source code is under the MIT license. The core project can be found at the [strapi/strapi](https://github.com/strapi/strapi) GitHub repository and you will find all other tools in the [Strapi](https://github.com/strapi) GitHub organization.
|
||||
Strapi is an open-source project. See the [LICENSE](https://github.com/strapi/strapi/blob/master/LICENSE) file for licensing information. The core project can be found at the [strapi/strapi](https://github.com/strapi/strapi) GitHub repository and you will find all other tools in the [Strapi](https://github.com/strapi) GitHub organization.
|
||||
|
||||
### Marketplace
|
||||
|
||||
|
||||
@ -44,7 +44,7 @@ Should you decide to opt-out, you may do so by removing the `uuid` property in t
|
||||
"strapi": {
|
||||
"uuid": "7b581c0d-89b7-479e-b379-a76ab90b8754"
|
||||
},
|
||||
"license": "MIT"
|
||||
"license": "SEE LICENSE IN LICENSE"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -410,8 +410,6 @@ export default strapi => {
|
||||
injectedComponents: [],
|
||||
isReady: true,
|
||||
isRequired: pluginPkg.strapi.required || false,
|
||||
leftMenuLinks: [],
|
||||
leftMenuSections: [],
|
||||
mainComponent: null,
|
||||
name: pluginPkg.strapi.name,
|
||||
preventComponentRendering: false,
|
||||
|
||||
@ -6,52 +6,52 @@ How to upgrade your application to the latest version of Strapi.
|
||||
|
||||
Start by upgrading all your Strapi package version.
|
||||
|
||||
For example moving from `3.0.0-beta.16` to `3.0.0-beta.17`
|
||||
For example moving from `3.0.4` to `3.0.5`
|
||||
|
||||
:::: tabs
|
||||
|
||||
::: tab 3.0.0-beta.16
|
||||
::: tab 3.0.5
|
||||
|
||||
```json
|
||||
{
|
||||
//...
|
||||
"dependencies": {
|
||||
"strapi": "3.0.0-beta.16",
|
||||
"strapi-admin": "3.0.0-beta.16",
|
||||
"strapi-hook-bookshelf": "3.0.0-beta.16",
|
||||
"strapi-hook-knex": "3.0.0-beta.16",
|
||||
"strapi-plugin-content-manager": "3.0.0-beta.16",
|
||||
"strapi-plugin-content-type-builder": "3.0.0-beta.16",
|
||||
"strapi-plugin-email": "3.0.0-beta.16",
|
||||
"strapi-plugin-graphql": "3.0.0-beta.16",
|
||||
"strapi-plugin-settings-manager": "3.0.0-beta.16",
|
||||
"strapi-plugin-upload": "3.0.0-beta.16",
|
||||
"strapi-plugin-users-permissions": "3.0.0-beta.16",
|
||||
"strapi-utils": "3.0.0-beta.16"
|
||||
"strapi": "3.0.5",
|
||||
"strapi-admin": "3.0.5",
|
||||
"strapi-hook-bookshelf": "3.0.5",
|
||||
"strapi-hook-knex": "3.0.5",
|
||||
"strapi-plugin-content-manager": "3.0.5",
|
||||
"strapi-plugin-content-type-builder": "3.0.5",
|
||||
"strapi-plugin-email": "3.0.5",
|
||||
"strapi-plugin-graphql": "3.0.5",
|
||||
"strapi-plugin-settings-manager": "3.0.5",
|
||||
"strapi-plugin-upload": "3.0.5",
|
||||
"strapi-plugin-users-permissions": "3.0.5",
|
||||
"strapi-utils": "3.0.5"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::: tab 3.0.0-beta.17
|
||||
::: tab 3.0.6
|
||||
|
||||
```json
|
||||
{
|
||||
//...
|
||||
"dependencies": {
|
||||
"strapi": "3.0.0-beta.17",
|
||||
"strapi-admin": "3.0.0-beta.17",
|
||||
"strapi-hook-bookshelf": "3.0.0-beta.17",
|
||||
"strapi-hook-knex": "3.0.0-beta.17",
|
||||
"strapi-plugin-content-manager": "3.0.0-beta.17",
|
||||
"strapi-plugin-content-type-builder": "3.0.0-beta.17",
|
||||
"strapi-plugin-email": "3.0.0-beta.17",
|
||||
"strapi-plugin-graphql": "3.0.0-beta.17",
|
||||
"strapi-plugin-settings-manager": "3.0.0-beta.17",
|
||||
"strapi-plugin-upload": "3.0.0-beta.17",
|
||||
"strapi-plugin-users-permissions": "3.0.0-beta.17",
|
||||
"strapi-utils": "3.0.0-beta.17"
|
||||
"strapi": "3.0.6",
|
||||
"strapi-admin": "3.0.6",
|
||||
"strapi-hook-bookshelf": "3.0.6",
|
||||
"strapi-hook-knex": "3.0.6",
|
||||
"strapi-plugin-content-manager": "3.0.6",
|
||||
"strapi-plugin-content-type-builder": "3.0.6",
|
||||
"strapi-plugin-email": "3.0.6",
|
||||
"strapi-plugin-graphql": "3.0.6",
|
||||
"strapi-plugin-settings-manager": "3.0.6",
|
||||
"strapi-plugin-upload": "3.0.6",
|
||||
"strapi-plugin-users-permissions": "3.0.6",
|
||||
"strapi-utils": "3.0.6"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -2,6 +2,10 @@
|
||||
|
||||
Please also refer to the following [documentation](../guides/update-version.md) for a better understanding of how to update your project
|
||||
|
||||
## Guides
|
||||
|
||||
- [Migration guide from 3.0.x to 3.1.x](migration-guide-3.0.x-to-3.1.x.md)
|
||||
|
||||
## Migrating from Beta ?
|
||||
|
||||
Read the [Migration guide from beta.20+ to stable](migration-guide-beta.20-to-3.0.0.md).
|
||||
|
||||
140
docs/v3.x/migration-guide/migration-guide-3.0.x-to-3.1.x.md
Normal file
140
docs/v3.x/migration-guide/migration-guide-3.0.x-to-3.1.x.md
Normal file
@ -0,0 +1,140 @@
|
||||
# Migration guide from 3.0.x to 3.1.x
|
||||
|
||||
**Make sure your server is not running until the end of the migration**
|
||||
|
||||
## Summary
|
||||
|
||||
[[toc]]
|
||||
|
||||
## 1. Upgrading your dependencies
|
||||
|
||||
First, run the following command to get the last version of `3.1.x`:
|
||||
|
||||
```bash
|
||||
npm info strapi@3.1.x version
|
||||
```
|
||||
|
||||
Then, update your `package.json` with the highest version given by the previous command.
|
||||
|
||||
**Example —** `package.json`
|
||||
|
||||
```json
|
||||
{
|
||||
// ...
|
||||
"dependencies": {
|
||||
"strapi": "$version",
|
||||
"strapi-admin": "$version",
|
||||
"strapi-connector-bookshelf": "$version",
|
||||
"strapi-plugin-content-manager": "$version",
|
||||
"strapi-plugin-content-type-builder": "$version",
|
||||
"strapi-plugin-email": "$version",
|
||||
"strapi-plugin-graphql": "$version",
|
||||
"strapi-plugin-upload": "$version",
|
||||
"strapi-plugin-users-permissions": "$version",
|
||||
"strapi-utils": "$version"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::tip NOTE
|
||||
Make sure to replace `$version` with the highest version given by the previous command.
|
||||
:::
|
||||
|
||||
Finally, update your `dependencies` with one of the following commands:
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
# or
|
||||
npm install
|
||||
```
|
||||
|
||||
## 2. Define the admin JWT Token
|
||||
|
||||
This version comes with a new feature: Role & Permissions for the administrators. In the process, the authentication system for administrators has been updated and the `secret` used to encode the jwt token is not automatically generated anymore.
|
||||
In order to make the login work again you need to define the `secret` you want to use in `server.js`.
|
||||
|
||||
**Example**
|
||||
|
||||
1. Generate a secure token.
|
||||
|
||||
```bash
|
||||
openssl rand 64 | base64 # (linux/macOS users)
|
||||
# or
|
||||
node -e "console.log(require('crypto').randomBytes(64).toString('base64'))" # (all users)
|
||||
```
|
||||
|
||||
2. Add it to you env variables (for example in `.env`).
|
||||
|
||||
`.env`
|
||||
|
||||
```bash
|
||||
ADMIN_JWT_SECRET=token_generated_above
|
||||
```
|
||||
|
||||
3. Add it to your config file.
|
||||
|
||||
`config/server.js`
|
||||
|
||||
```js
|
||||
module.exports = ({ env }) => ({
|
||||
// ...
|
||||
admin: {
|
||||
auth: {
|
||||
secret: env('ADMIN_JWT_SECRET'),
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You're done!
|
||||
|
||||
:::tip NOTE
|
||||
All currently logged in administrators will be disconnected from the app and will need to log in again.
|
||||
:::
|
||||
|
||||
## 3. Migrate your custom admin panel plugins
|
||||
|
||||
If you don't have custom plugins, you can jump to the next section.
|
||||
|
||||
In order to display your custom plugin link into the mail `LeftMenu` you need to update the plugin registration by adding `icon`, `name` and `menu` in the following file.
|
||||
|
||||
**Path —** `plugins/${pluginName}/admin/src/index.js`
|
||||
|
||||
```js
|
||||
export default strapi => {
|
||||
// ...
|
||||
const icon = pluginPkg.strapi.icon;
|
||||
const name = pluginPkg.strapi.name;
|
||||
const plugin = {
|
||||
// ...
|
||||
icon,
|
||||
name,
|
||||
menu: {
|
||||
// Set a link into the PLUGINS section
|
||||
pluginsSectionLinks: [
|
||||
{
|
||||
destination: `/plugins/${pluginId}`, // Endpoint of the link
|
||||
icon,
|
||||
name,
|
||||
label: {
|
||||
id: `${pluginId}.plugin.name`, // Refers to a i18n
|
||||
defaultMessage: 'MY PLUGIN',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 4. Rebuild the admin panel
|
||||
|
||||
Rebuild the admin panel with one of the following commands:
|
||||
|
||||
```bash
|
||||
yarn build --clean
|
||||
# or
|
||||
npm run build -- --clean
|
||||
```
|
||||
|
||||
🎉 Congrats, your application has been migrated!
|
||||
@ -69,8 +69,8 @@ Here are its properties:
|
||||
| initializer | node | Refer to the [Initializer documentation](#initializer) |
|
||||
| injectedComponents | array | Refer to the [Injected Component documentation](#injected-components) |
|
||||
| isReady | boolean | The app will load until this proprety is true |
|
||||
| leftMenuLinks | array | Array of links to inject in the menu |
|
||||
| mainComponent | node | The plugin's App container, setting it to null will prevent the plugin from being displayed in the menu |
|
||||
| mainComponent | node | The plugin's App container, |
|
||||
| menu | object | Define where the link of your plugin will be set. Without this your plugin will not display a link in the left menu |
|
||||
| name | string | The plugin's name retrieved from the package.json |
|
||||
| pluginLogo | file | The plugin's logo |
|
||||
| preventComponentRendering | boolean | Whether or not display the plugin's blockerComponent instead of the main component |
|
||||
@ -78,6 +78,64 @@ Here are its properties:
|
||||
| reducers | object | The plugin's redux reducers |
|
||||
| trads | object | The plugin's translation files |
|
||||
|
||||
### Displaying the plugin's link in the main menu
|
||||
|
||||
To display a plugin link into the main menu the plugin needs to export a menu object.
|
||||
|
||||
**Path —** `plugins/my-plugin/admin/src/index.js`.
|
||||
|
||||
```js
|
||||
import pluginPkg from '../../package.json';
|
||||
import pluginLogo from './assets/images/logo.svg';
|
||||
import App from './containers/App';
|
||||
import lifecycles from './lifecycles';
|
||||
import trads from './translations';
|
||||
import pluginId from './pluginId';
|
||||
|
||||
export default strapi => {
|
||||
const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
|
||||
const icon = pluginPkg.strapi.icon;
|
||||
const name = pluginPkg.strapi.name;
|
||||
const plugin = {
|
||||
blockerComponent: null,
|
||||
blockerComponentProps: {},
|
||||
description: pluginDescription,
|
||||
icon,
|
||||
id: pluginId,
|
||||
initializer: null,
|
||||
isRequired: pluginPkg.strapi.required || false,
|
||||
layout: null,
|
||||
lifecycles,
|
||||
mainComponent: App,
|
||||
name,
|
||||
pluginLogo,
|
||||
preventComponentRendering: false,
|
||||
trads,
|
||||
menu: {
|
||||
// Set a link into the PLUGINS section
|
||||
pluginsSectionLinks: [
|
||||
{
|
||||
destination: `/plugins/${pluginId}`, // Endpoint of the link
|
||||
icon,
|
||||
label: {
|
||||
id: `${pluginId}.plugin.name`, // Refers to a i18n
|
||||
defaultMessage: 'My PLUGIN',
|
||||
},
|
||||
name,
|
||||
// If the plugin has some permissions on whether or not it should be accessible
|
||||
// depending on the logged in user's role you can set them here.
|
||||
// Each permission object performs an OR comparison so if one matches the user's ones
|
||||
// the link will be displayed
|
||||
permissions: [{ action: 'plugins::content-type-builder.read', subject: null }],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
return strapi.registerPlugin(plugin);
|
||||
};
|
||||
```
|
||||
|
||||
### Initializer
|
||||
|
||||
The component is generated by default when you create a new plugin. Use this component to execute some logic when the app is loading. When the logic has been executed this component should emit the `isReady` event so the user can interact with the application.
|
||||
|
||||
@ -56,8 +56,6 @@ export default strapi => {
|
||||
initializer: () => null,
|
||||
injectedComponents: [],
|
||||
isReady: true,
|
||||
leftMenuLinks: [],
|
||||
leftMenuSections: [],
|
||||
mainComponent: null,
|
||||
name: pluginPkg.strapi.name,
|
||||
preventComponentRendering: false,
|
||||
@ -141,8 +139,6 @@ export default strapi => {
|
||||
isRequired: pluginPkg.strapi.required || false,
|
||||
layout: null,
|
||||
lifecycles,
|
||||
leftMenuLinks: [],
|
||||
leftMenuSections: [],
|
||||
mainComponent: App,
|
||||
name: pluginPkg.strapi.name,
|
||||
pluginLogo,
|
||||
|
||||
@ -42,6 +42,7 @@ export default strapi => {
|
||||
title: 'Setting page 1',
|
||||
to: `${strapi.settingsBaseURL}/${pluginId}/setting1`,
|
||||
name: 'setting1',
|
||||
permissions: [{ action: 'plugins::my-plugin.action-name', subject: null }], // This key is not mandatory it can be null, undefined or an empty array
|
||||
},
|
||||
{
|
||||
// Using i18n with a corresponding translation key
|
||||
@ -51,6 +52,8 @@ export default strapi => {
|
||||
},
|
||||
to: `${strapi.settingsBaseURL}/${pluginId}/setting2`,
|
||||
name: 'setting2',
|
||||
// Define a specific component if needed:
|
||||
Component: () => <div />,
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -64,8 +67,6 @@ export default strapi => {
|
||||
initializer: () => null,
|
||||
injectedComponents: [],
|
||||
isReady: true,
|
||||
leftMenuLinks: [],
|
||||
leftMenuSections: [],
|
||||
mainComponent: null,
|
||||
name: pluginPkg.strapi.name,
|
||||
preventComponentRendering: false,
|
||||
@ -147,6 +148,7 @@ export default strapi => {
|
||||
title: 'Setting page 1',
|
||||
to: `${strapi.settingsBaseURL}/${pluginId}/setting1`,
|
||||
name: 'setting1',
|
||||
permissions: [{ action: 'plugins::my-plugin.action-name', subject: null }],
|
||||
},
|
||||
{
|
||||
title: {
|
||||
@ -168,8 +170,6 @@ export default strapi => {
|
||||
initializer: () => null,
|
||||
injectedComponents: [],
|
||||
isReady: true,
|
||||
leftMenuLinks: [],
|
||||
leftMenuSections: [],
|
||||
mainComponent: null,
|
||||
name: pluginPkg.strapi.name,
|
||||
preventComponentRendering: false,
|
||||
@ -233,23 +233,24 @@ export default strapi => {
|
||||
initializer: () => null,
|
||||
injectedComponents: [],
|
||||
isReady: true,
|
||||
leftMenuLinks: [],
|
||||
leftMenuSections: [],
|
||||
mainComponent: null,
|
||||
name: pluginPkg.strapi.name,
|
||||
preventComponentRendering: false,
|
||||
settings: {
|
||||
// Add a link into the global section of the settings view
|
||||
global: [
|
||||
{
|
||||
title: 'Setting link 1',
|
||||
to: `${strapi.settingsBaseURL}/setting-link-1`,
|
||||
name: 'settingLink1',
|
||||
Component: SettingLink,
|
||||
// Bool : https://reacttraining.com/react-router/web/api/Route/exact-bool
|
||||
exact: false,
|
||||
},
|
||||
],
|
||||
global: {
|
||||
links: [
|
||||
{
|
||||
title: 'Setting link 1',
|
||||
to: `${strapi.settingsBaseURL}/setting-link-1`,
|
||||
name: 'settingLink1',
|
||||
Component: SettingLink,
|
||||
// Bool : https://reacttraining.com/react-router/web/api/Route/exact-bool
|
||||
exact: false,
|
||||
permissions: [{ action: 'plugins::my-plugin.action-name', subject: null }],
|
||||
},
|
||||
],
|
||||
},
|
||||
mainComponent: Settings,
|
||||
menuSection,
|
||||
},
|
||||
|
||||
@ -9,7 +9,6 @@ plugin/
|
||||
| └─── src/ # Source code directory
|
||||
| └─── index.js # Entry point of the plugin
|
||||
| └─── pluginId.js # Name of the plugin
|
||||
| └─── lifecycles.js # File in which the plugin sets the hooks to be ran in another plugin.
|
||||
| |
|
||||
| └─── components/ # Contains the list of React components used by the plugin
|
||||
| └─── containers/
|
||||
|
||||
@ -4,9 +4,13 @@ Thanks to the plugin `Email`, you can send email from your server or externals p
|
||||
|
||||
## Programmatic usage
|
||||
|
||||
### Send an email - `.send()`
|
||||
|
||||
In your custom controllers or services you may want to send email.
|
||||
By using the following function, Strapi will use the configured provider to send an email.
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
await strapi.plugins['email'].services.email.send({
|
||||
to: 'paulbocuse@strapi.io',
|
||||
@ -20,6 +24,40 @@ await strapi.plugins['email'].services.email.send({
|
||||
});
|
||||
```
|
||||
|
||||
### Send an email using a template - `.sendTemplatedEmail()`
|
||||
|
||||
When you send an email, you will most likely want to build it from a template you wrote.
|
||||
The email plugin provides the service `sendTemplatedEmail` that compile the email and then sends it. The function have the following params:
|
||||
|
||||
| param | description | type | default |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------------------ | ------ | ------- |
|
||||
| `emailOptions` | Object that contains email options (`to`, `from`, `replyTo`, `cc`, `bcc`) except `subject`, `text` and `html` | object | `{}` |
|
||||
| `emailTemplate` | Object that contains `subject`, `text` and `html` as [lodash string templates](https://lodash.com/docs/4.17.15#template) | object | `{}` |
|
||||
| `data` | Object that contains the data used to compile the templates | object | `{}` |
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
const emailTemplate = {
|
||||
subject: 'Welcome <%= user.firstname %>',
|
||||
text: `Welcome on mywebsite.fr!
|
||||
Your account is now linked with: <%= user.email %>.`,
|
||||
html: `<h1>Welcome on mywebsite.fr!</h1>
|
||||
<p>Your account is now linked with: <%= user.email %>.<p>`,
|
||||
},
|
||||
|
||||
await strapi.plugins.email.services.email.sendTemplatedEmail(
|
||||
{
|
||||
to: user.email,
|
||||
// from: is not specified, so it's the defaultFrom that will be used instead
|
||||
},
|
||||
emailTemplate,
|
||||
{
|
||||
user: _.pick(user, ['username', 'email', 'firstname', 'lastname']),
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Configure the plugin
|
||||
|
||||
### Install the provider you want
|
||||
|
||||
@ -45,10 +45,6 @@
|
||||
],
|
||||
"plugin": "upload",
|
||||
"required": false
|
||||
},
|
||||
"full_name": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
{
|
||||
"kind": "collectionType",
|
||||
"collectionName": "menus",
|
||||
"info": {
|
||||
"name": "menu",
|
||||
@ -14,8 +15,8 @@
|
||||
"type": "text"
|
||||
},
|
||||
"menusections": {
|
||||
"collection": "menusection",
|
||||
"via": "menu"
|
||||
"via": "menu",
|
||||
"collection": "menusection"
|
||||
},
|
||||
"restaurant": {
|
||||
"via": "menu",
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
{
|
||||
"kind": "collectionType",
|
||||
"collectionName": "menusections",
|
||||
"info": {
|
||||
"name": "menusection",
|
||||
@ -21,7 +22,8 @@
|
||||
"dishes": {
|
||||
"component": "default.dish",
|
||||
"type": "component",
|
||||
"repeatable": true
|
||||
"repeatable": true,
|
||||
"required": true
|
||||
},
|
||||
"menu": {
|
||||
"model": "menu",
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
"attributes": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"required": false,
|
||||
"default": "My super dish"
|
||||
},
|
||||
"description": {
|
||||
@ -26,8 +26,8 @@
|
||||
"very_long_description": {
|
||||
"type": "richtext"
|
||||
},
|
||||
"category": {
|
||||
"model": "category"
|
||||
"categories": {
|
||||
"collection": "category"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
module.exports = ({ env }) => ({
|
||||
host: env('HOST', '0.0.0.0'),
|
||||
port: env.int('PORT', 1337),
|
||||
admin: {
|
||||
auth: {
|
||||
secret: env('ADMIN_JWT_SECRET', 'example-token'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -5,8 +5,10 @@
|
||||
"description": "A Strapi application.",
|
||||
"scripts": {
|
||||
"develop": "strapi develop",
|
||||
"develop:ce": "STRAPI_DISABLE_EE=true strapi develop",
|
||||
"start": "strapi start",
|
||||
"build": "strapi build",
|
||||
"build:ce": "STRAPI_DISABLE_EE=true strapi build",
|
||||
"strapi": "strapi"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -39,5 +41,5 @@
|
||||
"node": ">=10.10.0",
|
||||
"npm": ">=6.0.0"
|
||||
},
|
||||
"license": "MIT"
|
||||
"license": "SEE LICENSE IN LICENSE"
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ module.exports = {
|
||||
name: 'API integration tests',
|
||||
testMatch: ['**/?(*.)+(spec|test).e2e.js'],
|
||||
testEnvironment: 'node',
|
||||
setupFilesAfterEnv: ['<rootDir>/test/jest2e2.setup.js'],
|
||||
coveragePathIgnorePatterns: [
|
||||
'<rootDir>/dist/',
|
||||
'<rootDir>/node_modules/',
|
||||
|
||||
@ -1,3 +1,26 @@
|
||||
const IS_EE = process.env.IS_EE === 'true';
|
||||
|
||||
const moduleNameMapper = {
|
||||
'.*\\.(css|less|styl|scss|sass)$': '<rootDir>/test/config/front/mocks/cssModule.js',
|
||||
'.*\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|ico)$':
|
||||
'<rootDir>/test/config/front/mocks/image.js',
|
||||
'^ee_else_ce(/.*)$': [
|
||||
'<rootDir>/packages/strapi-admin/admin/src$1',
|
||||
'<rootDir>/packages/strapi-plugin-*/admin/src$1',
|
||||
],
|
||||
};
|
||||
|
||||
if (IS_EE) {
|
||||
const rootDirEE = [
|
||||
'<rootDir>/packages/strapi-admin/ee/admin$1',
|
||||
'<rootDir>/packages/strapi-plugin-*/ee/admin$1',
|
||||
];
|
||||
|
||||
Object.assign(moduleNameMapper, {
|
||||
'^ee_else_ce(/.*)$': rootDirEE,
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
collectCoverageFrom: [
|
||||
'packages/strapi-admin/admin/src/**/**/*.js',
|
||||
@ -27,11 +50,7 @@ module.exports = {
|
||||
'<rootDir>/packages/strapi-admin/node_modules',
|
||||
'<rootDir>/test/config/front',
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'.*\\.(css|less|styl|scss|sass)$': '<rootDir>/test/config/front/mocks/cssModule.js',
|
||||
'.*\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|ico)$':
|
||||
'<rootDir>/test/config/front/mocks/image.js',
|
||||
},
|
||||
moduleNameMapper,
|
||||
rootDir: process.cwd(),
|
||||
setupFiles: ['<rootDir>/test/config/front/test-bundler.js'],
|
||||
testPathIgnorePatterns: [
|
||||
|
||||
23
package.json
23
package.json
@ -3,9 +3,9 @@
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.9.0",
|
||||
"@testing-library/jest-dom": "^4.0.0",
|
||||
"@testing-library/react": "^9.1.0",
|
||||
"@testing-library/react-hooks": "^2.0.0",
|
||||
"@testing-library/jest-dom": "^5.8.0",
|
||||
"@testing-library/react": "^10.0.4",
|
||||
"@testing-library/react-hooks": "^3.2.1",
|
||||
"axios-mock-adapter": "^1.17.0",
|
||||
"babel-eslint": "^10.0.0",
|
||||
"chokidar": "3.3.1",
|
||||
@ -27,9 +27,9 @@
|
||||
"glob": "7.1.6",
|
||||
"husky": "^3.0.0",
|
||||
"istanbul": "~0.4.2",
|
||||
"jest": "^24.5.0",
|
||||
"jest-cli": "^24.5.0",
|
||||
"jest-styled-components": "^7.0.0",
|
||||
"jest": "^26.0.1",
|
||||
"jest-cli": "^26.0.1",
|
||||
"jest-styled-components": "^7.0.2",
|
||||
"lerna": "^3.13.1",
|
||||
"lint-staged": "^9.2.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@ -61,9 +61,12 @@
|
||||
"prettier:code": "prettier \"**/*.js\"",
|
||||
"prettier:other": "prettier \"**/*.{md,css,scss,yaml,yml}\"",
|
||||
"test:clean": "rimraf ./coverage",
|
||||
"test:front": "npm run test:clean && cross-env NODE_ENV=test jest --config ./jest.config.front.js --coverage",
|
||||
"test:front:watch": "cross-env NODE_ENV=test jest --config ./jest.config.front.js --watchAll",
|
||||
"test:front:update": "cross-env NODE_ENV=test jest --config ./jest.config.front.js --u",
|
||||
"test:front": "npm run test:clean && cross-env NODE_ENV=test IS_EE=true jest --config ./jest.config.front.js --coverage",
|
||||
"test:front:watch": "cross-env NODE_ENV=test IS_EE=true jest --config ./jest.config.front.js --watchAll",
|
||||
"test:front:update": "cross-env NODE_ENV=test IS_EE=true jest --config ./jest.config.front.js --u",
|
||||
"test:front:ce": "npm run test:clean && cross-env NODE_ENV=test IS_EE=false jest --config ./jest.config.front.js --coverage",
|
||||
"test:front:watch:ce": "cross-env NODE_ENV=test IS_EE=false jest --config ./jest.config.front.js --watchAll",
|
||||
"test:front:update:ce": "cross-env NODE_ENV=test IS_EE=false jest --config ./jest.config.front.js --u",
|
||||
"test:snyk": "snyk test",
|
||||
"test:unit": "jest --verbose",
|
||||
"test:e2e": "FORCE_COLOR=true jest --config jest.config.e2e.js --runInBand --verbose --forceExit --detectOpenHandles",
|
||||
@ -93,7 +96,7 @@
|
||||
"node": ">=10.10.0",
|
||||
"npm": ">=6.0.0"
|
||||
},
|
||||
"license": "MIT",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"name": "strapi-monorepo",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
|
||||
22
packages/create-strapi-app/LICENSE
Normal file
22
packages/create-strapi-app/LICENSE
Normal file
@ -0,0 +1,22 @@
|
||||
Copyright (c) 2015-present Strapi Solutions SAS
|
||||
|
||||
Portions of the Strapi software are licensed as follows:
|
||||
|
||||
* All software that resides under an "ee/" directory (the “EE Software”), if that directory exists, is licensed under the license defined in "ee/LICENSE".
|
||||
|
||||
* All software outside of the above-mentioned directories or restrictions above is available under the "MIT Expat" license as set forth below.
|
||||
|
||||
MIT Expat License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -1,7 +0,0 @@
|
||||
Copyright (c) 2015-2020 Strapi Solutions.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@ -2,7 +2,7 @@
|
||||
"name": "create-strapi-app",
|
||||
"version": "3.0.6",
|
||||
"description": "Generate a new Strapi application.",
|
||||
"license": "MIT",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"homepage": "http://strapi.io",
|
||||
"keywords": [
|
||||
"create-strapi-app",
|
||||
|
||||
@ -101,3 +101,4 @@ node_modules
|
||||
test
|
||||
testApp
|
||||
coverage
|
||||
webpack.config.dev.js
|
||||
22
packages/strapi-admin/LICENSE
Normal file
22
packages/strapi-admin/LICENSE
Normal file
@ -0,0 +1,22 @@
|
||||
Copyright (c) 2015-present Strapi Solutions SAS
|
||||
|
||||
Portions of the Strapi software are licensed as follows:
|
||||
|
||||
* All software that resides under an "ee/" directory (the “EE Software”), if that directory exists, is licensed under the license defined in "ee/LICENSE".
|
||||
|
||||
* All software outside of the above-mentioned directories or restrictions above is available under the "MIT Expat" license as set forth below.
|
||||
|
||||
MIT Expat License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -1,7 +0,0 @@
|
||||
Copyright (c) 2015-2020 Strapi Solutions.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@ -130,6 +130,15 @@ const unlockApp = () => {
|
||||
dispatch(unfreezeApp());
|
||||
};
|
||||
|
||||
const lockAppWithOverlay = () => {
|
||||
const overlayblockerParams = {
|
||||
children: <div />,
|
||||
noGradient: true,
|
||||
};
|
||||
|
||||
lockApp(overlayblockerParams);
|
||||
};
|
||||
|
||||
window.strapi = Object.assign(window.strapi || {}, {
|
||||
node: MODE || 'host',
|
||||
env: NODE_ENV,
|
||||
@ -165,6 +174,7 @@ window.strapi = Object.assign(window.strapi || {}, {
|
||||
window.navigator.userLanguage ||
|
||||
'en',
|
||||
lockApp,
|
||||
lockAppWithOverlay,
|
||||
unlockApp,
|
||||
injectReducer,
|
||||
injectSaga,
|
||||
|
||||
BIN
packages/strapi-admin/admin/src/assets/images/oops.png
Normal file
BIN
packages/strapi-admin/admin/src/assets/images/oops.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@ -0,0 +1,33 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
|
||||
// You can see in the index.js file that I used the design system to do the UI integration but
|
||||
// sometimes, we need to create some "temporary" custom style to fix the baseline alignment.
|
||||
// -----
|
||||
// TODO : remove this component. I think that this kind components should not exist in Strapi.
|
||||
// I create it to temporary fix the baseline alignment until we have the design system.
|
||||
|
||||
const BaselineAlignment = styled.div`
|
||||
padding-top: ${({ size, top }) => top && size};
|
||||
padding-right: ${({ size, right }) => right && size};
|
||||
padding-bottom: ${({ size, bottom }) => bottom && size};
|
||||
padding-left: ${({ size, left }) => left && size};
|
||||
`;
|
||||
|
||||
BaselineAlignment.defaultProps = {
|
||||
bottom: false,
|
||||
left: false,
|
||||
right: false,
|
||||
size: '0',
|
||||
top: false,
|
||||
};
|
||||
|
||||
BaselineAlignment.propTypes = {
|
||||
bottom: PropTypes.bool,
|
||||
left: PropTypes.bool,
|
||||
right: PropTypes.bool,
|
||||
size: PropTypes.string,
|
||||
top: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default BaselineAlignment;
|
||||
9
packages/strapi-admin/admin/src/components/Bloc/index.js
Normal file
9
packages/strapi-admin/admin/src/components/Bloc/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Bloc = styled.div`
|
||||
background: ${({ theme }) => theme.main.colors.white};
|
||||
border-radius: ${({ theme }) => theme.main.sizes.borderRadius};
|
||||
box-shadow: 0 2px 4px #e3e9f3;
|
||||
`;
|
||||
|
||||
export default Bloc;
|
||||
@ -2,11 +2,12 @@ import styled from 'styled-components';
|
||||
import { Container } from 'reactstrap';
|
||||
|
||||
const ContainerFluid = styled(Container)`
|
||||
padding: 18px 30px !important;
|
||||
padding: ${({ padding }) => padding};
|
||||
`;
|
||||
|
||||
ContainerFluid.defaultProps = {
|
||||
fluid: true,
|
||||
padding: '18px 30px !important',
|
||||
};
|
||||
|
||||
export default ContainerFluid;
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 372px;
|
||||
height: 159px;
|
||||
border-radius: 2px;
|
||||
background-color: #fff;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
padding-top: 3rem;
|
||||
text-align: center;
|
||||
font-family: Lato;
|
||||
font-size: 1.3rem;
|
||||
> img {
|
||||
width: 2.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
> div {
|
||||
padding-top: 9px;
|
||||
line-height: 18px;
|
||||
> span:first-child {
|
||||
color: #333740;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
> span {
|
||||
color: #787e8f;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export { Content, Wrapper };
|
||||
@ -1,27 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* DownloadInfo
|
||||
*
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Icon from '../../assets/icons/icon_success.svg';
|
||||
import { Content, Wrapper } from './components';
|
||||
|
||||
function DownloadInfo() {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Content>
|
||||
<img src={Icon} alt="info" />
|
||||
<div>
|
||||
<FormattedMessage id="app.components.DownloadInfo.download" />
|
||||
<br />
|
||||
<FormattedMessage id="app.components.DownloadInfo.text" />
|
||||
</div>
|
||||
</Content>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default DownloadInfo;
|
||||
61
packages/strapi-admin/admin/src/components/FormBloc/index.js
Normal file
61
packages/strapi-admin/admin/src/components/FormBloc/index.js
Normal file
@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { Flex, Padded, Text } from '@buffetjs/core';
|
||||
import { LoadingIndicator, Row } from 'strapi-helper-plugin';
|
||||
import PropTypes from 'prop-types';
|
||||
import BaselineAlignement from '../BaselineAlignement';
|
||||
import Bloc from '../Bloc';
|
||||
|
||||
const FormBloc = ({ children, actions, isLoading, title, subtitle }) => (
|
||||
<Bloc>
|
||||
<BaselineAlignement top size={title ? '18px' : '22px'} />
|
||||
<Padded left right size="sm">
|
||||
{isLoading ? (
|
||||
<>
|
||||
<LoadingIndicator />
|
||||
<BaselineAlignement bottom size="22px" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{title && (
|
||||
<>
|
||||
<Padded left right size="xs">
|
||||
<Flex justifyContent="space-between">
|
||||
<Padded left right size="sm">
|
||||
<Text fontSize="lg" fontWeight="bold">
|
||||
{title}
|
||||
</Text>
|
||||
{subtitle && (
|
||||
<Text color="grey" lineHeight="1.8rem">
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</Padded>
|
||||
{actions}
|
||||
</Flex>
|
||||
</Padded>
|
||||
<BaselineAlignement top size="18px" />
|
||||
</>
|
||||
)}
|
||||
<Row>{children}</Row>
|
||||
</>
|
||||
)}
|
||||
</Padded>
|
||||
</Bloc>
|
||||
);
|
||||
|
||||
FormBloc.defaultProps = {
|
||||
actions: null,
|
||||
isLoading: false,
|
||||
subtitle: null,
|
||||
title: null,
|
||||
};
|
||||
|
||||
FormBloc.propTypes = {
|
||||
actions: PropTypes.any,
|
||||
children: PropTypes.node.isRequired,
|
||||
isLoading: PropTypes.bool,
|
||||
subtitle: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
};
|
||||
|
||||
export default FormBloc;
|
||||
@ -0,0 +1,15 @@
|
||||
import styled from 'styled-components';
|
||||
import { Button as Base } from '@buffetjs/core';
|
||||
|
||||
const Button = styled(Base)`
|
||||
width: 100%;
|
||||
text-transform: ${({ textTransform }) => textTransform};
|
||||
`;
|
||||
|
||||
Button.defaultProps = {
|
||||
color: 'primary',
|
||||
type: 'button',
|
||||
textTransform: 'none',
|
||||
};
|
||||
|
||||
export default Button;
|
||||
@ -135,6 +135,7 @@ const GlobalStyle = createGlobalStyle`
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 9px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
import { HeaderSearch as Base } from 'strapi-helper-plugin';
|
||||
|
||||
const HeaderSearch = styled(Base)`
|
||||
left: 32rem;
|
||||
`;
|
||||
|
||||
export default HeaderSearch;
|
||||
@ -0,0 +1,99 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { upperFirst } from 'lodash';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useQuery } from 'strapi-helper-plugin';
|
||||
import StyledHeaderSearch from './HeaderSearch';
|
||||
|
||||
const HeaderSearch = ({ label, queryParameter }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const query = useQuery();
|
||||
const searchValue = query.get(queryParameter) || '';
|
||||
const [value, setValue] = useState(searchValue);
|
||||
const { push } = useHistory();
|
||||
const displayedLabel =
|
||||
typeof label === 'object'
|
||||
? formatMessage({ ...label, defaultMessage: label.defaultMessage || label.id })
|
||||
: label;
|
||||
const capitalizedLabel = upperFirst(displayedLabel);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchValue === '') {
|
||||
// Synchronise the search
|
||||
handleClear();
|
||||
}
|
||||
}, [searchValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
let currentSearch = query;
|
||||
|
||||
if (value) {
|
||||
// Create a new search in order to remove the filters
|
||||
currentSearch = new URLSearchParams('');
|
||||
|
||||
// Keep the previous params _sort, pageSize, page
|
||||
const pageSize = query.get('pageSize');
|
||||
const page = query.get('page');
|
||||
const _sort = query.get('_sort');
|
||||
|
||||
if (page) {
|
||||
currentSearch.set('page', page);
|
||||
}
|
||||
|
||||
if (pageSize) {
|
||||
currentSearch.set('pageSize', pageSize);
|
||||
}
|
||||
|
||||
if (_sort) {
|
||||
currentSearch.set('_sort', _sort);
|
||||
}
|
||||
|
||||
currentSearch.set(queryParameter, encodeURIComponent(value));
|
||||
} else {
|
||||
currentSearch.delete(queryParameter);
|
||||
}
|
||||
|
||||
push({ search: currentSearch.toString() });
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(handler);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value]);
|
||||
|
||||
const handleChange = ({ target: { value } }) => {
|
||||
setValue(value);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setValue('');
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledHeaderSearch
|
||||
label={capitalizedLabel}
|
||||
name={queryParameter}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
HeaderSearch.defaultProps = {
|
||||
queryParameter: '_q',
|
||||
};
|
||||
|
||||
HeaderSearch.propTypes = {
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string,
|
||||
}),
|
||||
]).isRequired,
|
||||
queryParameter: PropTypes.string,
|
||||
};
|
||||
|
||||
export default HeaderSearch;
|
||||
@ -0,0 +1,38 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { translatedErrors } from 'strapi-helper-plugin';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Inputs } from '@buffetjs/custom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const IntlInput = ({ label: labelId, defaultMessage, error, ...rest }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const label = formatMessage({ id: labelId, defaultMessage: defaultMessage || labelId });
|
||||
const translatedError = error ? formatMessage(error) : null;
|
||||
const formattedErrors = useMemo(() => {
|
||||
return Object.keys(translatedErrors).reduce((acc, current) => {
|
||||
acc[current] = formatMessage({ id: translatedErrors[current] });
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Inputs {...rest} label={label} error={translatedError} translatedErrors={formattedErrors} />
|
||||
);
|
||||
};
|
||||
|
||||
IntlInput.defaultProps = {
|
||||
defaultMessage: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
IntlInput.propTypes = {
|
||||
defaultMessage: PropTypes.string,
|
||||
error: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
}),
|
||||
label: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default IntlInput;
|
||||
@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Logo from '../../assets/images/logo-strapi.png';
|
||||
import Logo from '../../../assets/images/logo-strapi.png';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
background-color: #007eff;
|
||||
@ -11,7 +11,7 @@ import { FormattedMessage } from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
|
||||
import en from '../../translations/en.json';
|
||||
import en from '../../../translations/en.json';
|
||||
import LeftMenuIcon from './LeftMenuIcon';
|
||||
import A from './A';
|
||||
|
||||
@ -22,34 +22,29 @@ const LinkLabel = styled.span`
|
||||
padding-left: 2.5rem;
|
||||
`;
|
||||
|
||||
const LeftMenuLinkContent = ({
|
||||
destination,
|
||||
iconName,
|
||||
label,
|
||||
location,
|
||||
source,
|
||||
suffixUrlToReplaceForLeftMenuHighlight,
|
||||
}) => {
|
||||
// TODO: refacto this file
|
||||
const LeftMenuLinkContent = ({ destination, iconName, label, location }) => {
|
||||
const isLinkActive = startsWith(
|
||||
location.pathname.replace('/admin', '').concat('/'),
|
||||
|
||||
destination.replace(suffixUrlToReplaceForLeftMenuHighlight, '').concat('/')
|
||||
destination.concat('/')
|
||||
);
|
||||
|
||||
// Check if messageId exists in en locale to prevent warning messages
|
||||
const content = en[label] ? (
|
||||
<FormattedMessage
|
||||
id={label}
|
||||
defaultMessage="{label}"
|
||||
values={{
|
||||
label: `${label}`,
|
||||
}}
|
||||
>
|
||||
{message => <LinkLabel>{message}</LinkLabel>}
|
||||
</FormattedMessage>
|
||||
) : (
|
||||
<LinkLabel>{label}</LinkLabel>
|
||||
);
|
||||
const labelId = label.id || label;
|
||||
const content =
|
||||
en[labelId] || label.defaultMessage ? (
|
||||
<FormattedMessage
|
||||
id={labelId}
|
||||
defaultMessage={label.defaultMessage || '{label}'}
|
||||
values={{
|
||||
label: `${label.id || label}`,
|
||||
}}
|
||||
>
|
||||
{message => <LinkLabel>{message}</LinkLabel>}
|
||||
</FormattedMessage>
|
||||
) : (
|
||||
<LinkLabel>{labelId}</LinkLabel>
|
||||
);
|
||||
|
||||
// Create external or internal link.
|
||||
return destination.includes('http') ? (
|
||||
@ -68,7 +63,6 @@ const LeftMenuLinkContent = ({
|
||||
className={isLinkActive ? 'linkActive' : ''}
|
||||
to={{
|
||||
pathname: destination,
|
||||
search: source ? `?source=${source}` : '',
|
||||
}}
|
||||
>
|
||||
<LeftMenuIcon icon={iconName} />
|
||||
@ -80,17 +74,10 @@ const LeftMenuLinkContent = ({
|
||||
LeftMenuLinkContent.propTypes = {
|
||||
destination: PropTypes.string.isRequired,
|
||||
iconName: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired,
|
||||
location: PropTypes.shape({
|
||||
pathname: PropTypes.string,
|
||||
}).isRequired,
|
||||
source: PropTypes.string,
|
||||
suffixUrlToReplaceForLeftMenuHighlight: PropTypes.string,
|
||||
};
|
||||
|
||||
LeftMenuLinkContent.defaultProps = {
|
||||
source: '',
|
||||
suffixUrlToReplaceForLeftMenuHighlight: '',
|
||||
};
|
||||
|
||||
export default withRouter(LeftMenuLinkContent);
|
||||
@ -0,0 +1,36 @@
|
||||
/**
|
||||
*
|
||||
* LeftMenuLink
|
||||
*
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import LeftMenuLinkContent from './LeftMenuLinkContent';
|
||||
|
||||
const LeftMenuLink = ({ destination, iconName, label, location }) => {
|
||||
return (
|
||||
<LeftMenuLinkContent
|
||||
destination={destination}
|
||||
iconName={iconName}
|
||||
label={label}
|
||||
location={location}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
LeftMenuLink.propTypes = {
|
||||
destination: PropTypes.string.isRequired,
|
||||
iconName: PropTypes.string,
|
||||
label: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired,
|
||||
location: PropTypes.shape({
|
||||
pathname: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
LeftMenuLink.defaultProps = {
|
||||
iconName: 'circle',
|
||||
};
|
||||
|
||||
export default LeftMenuLink;
|
||||
@ -4,7 +4,8 @@ import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import messages from '../LeftMenuLinkContainer/messages.json';
|
||||
// TODO remove this
|
||||
import messages from './messages.json';
|
||||
import Search from './Search';
|
||||
import Title from './Title';
|
||||
import SearchButton from './SearchButton';
|
||||
@ -27,17 +27,6 @@ const LeftMenuLinksSection = ({
|
||||
'label'
|
||||
);
|
||||
|
||||
const getLinkDestination = link => {
|
||||
if (['plugins', 'general'].includes(section)) {
|
||||
return link.destination;
|
||||
}
|
||||
if (link.schema && link.schema.kind) {
|
||||
return `/plugins/${link.plugin}/${link.schema.kind}/${link.destination || link.uid}`;
|
||||
}
|
||||
|
||||
return `/plugins/${link.plugin}/${link.destination || link.uid}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<LeftMenuLinkHeader
|
||||
@ -56,7 +45,7 @@ const LeftMenuLinksSection = ({
|
||||
key={index}
|
||||
iconName={link.icon}
|
||||
label={link.label}
|
||||
destination={getLinkDestination(link)}
|
||||
destination={link.destination}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
@ -0,0 +1,31 @@
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const LinksContainer = styled.div`
|
||||
padding-top: 0.7rem;
|
||||
position: absolute;
|
||||
top: ${props => props.theme.main.sizes.leftMenu.height};
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - (${props => props.theme.main.sizes.leftMenu.height} + 3rem));
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
LinksContainer.defaultProps = {
|
||||
theme: {
|
||||
main: {
|
||||
sizes: {
|
||||
header: {},
|
||||
leftMenu: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
LinksContainer.propTypes = {
|
||||
theme: PropTypes.object,
|
||||
};
|
||||
|
||||
export default LinksContainer;
|
||||
@ -0,0 +1,4 @@
|
||||
export { default as LeftMenuFooter } from './LeftMenuFooter';
|
||||
export { default as LeftMenuHeader } from './LeftMenuHeader';
|
||||
export { default as LinksContainer } from './LinksContainer';
|
||||
export { default as LeftMenuLinksSection } from './LeftMenuLinkSection';
|
||||
@ -1,33 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Plugin = styled.div`
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: calc(100% - 4px);
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
height: 20px;
|
||||
transition: right 1s ease-in-out;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
width: auto;
|
||||
height: 20px;
|
||||
padding: 0 14px 0 10px;
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
background: #0097f7;
|
||||
border-radius: 3px;
|
||||
transition: transform 0.3s ease-in-out;
|
||||
white-space: pre;
|
||||
|
||||
&:hover {
|
||||
transform: translateX(calc(-100% + 9px));
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Plugin;
|
||||
@ -1,63 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* LeftMenuLink
|
||||
*
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { upperFirst } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import LeftMenuLinkContent from './LeftMenuLinkContent';
|
||||
import Plugin from './Plugin';
|
||||
|
||||
const LeftMenuLink = ({
|
||||
destination,
|
||||
iconName,
|
||||
label,
|
||||
location,
|
||||
source,
|
||||
suffixUrlToReplaceForLeftMenuHighlight,
|
||||
}) => {
|
||||
const plugin =
|
||||
source !== 'content-manager' && source !== '' ? (
|
||||
<Plugin>
|
||||
<span>{upperFirst(source.split('-').join(' '))}</span>
|
||||
</Plugin>
|
||||
) : (
|
||||
''
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LeftMenuLinkContent
|
||||
destination={destination}
|
||||
iconName={iconName}
|
||||
label={label}
|
||||
location={location}
|
||||
source={source}
|
||||
suffixUrlToReplaceForLeftMenuHighlight={suffixUrlToReplaceForLeftMenuHighlight}
|
||||
/>
|
||||
{plugin}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
LeftMenuLink.propTypes = {
|
||||
destination: PropTypes.string.isRequired,
|
||||
iconName: PropTypes.string,
|
||||
label: PropTypes.string.isRequired,
|
||||
location: PropTypes.shape({
|
||||
pathname: PropTypes.string,
|
||||
}).isRequired,
|
||||
source: PropTypes.string,
|
||||
suffixUrlToReplaceForLeftMenuHighlight: PropTypes.string,
|
||||
};
|
||||
|
||||
LeftMenuLink.defaultProps = {
|
||||
iconName: 'circle',
|
||||
source: '',
|
||||
suffixUrlToReplaceForLeftMenuHighlight: '',
|
||||
};
|
||||
|
||||
export default LeftMenuLink;
|
||||
@ -1,18 +0,0 @@
|
||||
// I am keeping this file if we want to join the scrollbars again
|
||||
import styled from 'styled-components';
|
||||
|
||||
const LeftMenuSection = styled.div`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
&:first-child {
|
||||
overflow: hidden;
|
||||
max-height: 180px;
|
||||
height: auto;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
`;
|
||||
|
||||
export default LeftMenuSection;
|
||||
@ -1,67 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
padding-top: 0.7rem;
|
||||
position: absolute;
|
||||
top: ${props => props.theme.main.sizes.leftMenu.height};
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - (${props => props.theme.main.sizes.leftMenu.height} + 3rem));
|
||||
box-sizing: border-box;
|
||||
|
||||
.title {
|
||||
padding-left: 2rem;
|
||||
padding-right: 1.6rem;
|
||||
padding-top: 1rem;
|
||||
margin-bottom: 0.8rem;
|
||||
color: ${props => props.theme.main.colors.leftMenu['title-color']};
|
||||
text-transform: uppercase;
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: 0.1rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin-bottom: 2rem;
|
||||
&.models-list {
|
||||
li a svg {
|
||||
font-size: 0.74rem;
|
||||
top: calc(50% - 0.35rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.noPluginsInstalled {
|
||||
color: ${props => props.theme.main.colors.white};
|
||||
padding-left: 1.6rem;
|
||||
padding-right: 1.6rem;
|
||||
font-weight: 300;
|
||||
min-height: 3.6rem;
|
||||
padding-top: 0.9rem;
|
||||
}
|
||||
`;
|
||||
|
||||
Wrapper.defaultProps = {
|
||||
theme: {
|
||||
main: {
|
||||
colors: {
|
||||
leftMenu: {},
|
||||
},
|
||||
sizes: {
|
||||
header: {},
|
||||
leftMenu: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Wrapper.propTypes = {
|
||||
theme: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Wrapper;
|
||||
@ -1,105 +0,0 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { get, snakeCase, isEmpty } from 'lodash';
|
||||
|
||||
import { SETTINGS_BASE_URL } from '../../config';
|
||||
import Wrapper from './Wrapper';
|
||||
import messages from './messages.json';
|
||||
|
||||
import LeftMenuLinkSection from '../LeftMenuLinkSection';
|
||||
|
||||
const LeftMenuLinkContainer = ({ plugins }) => {
|
||||
const location = useLocation();
|
||||
|
||||
// Generate the list of content types sections
|
||||
const contentTypesSections = Object.keys(plugins).reduce((acc, current) => {
|
||||
plugins[current].leftMenuSections.forEach((section = {}) => {
|
||||
if (!isEmpty(section.links)) {
|
||||
acc[snakeCase(section.name)] = {
|
||||
name: section.name,
|
||||
searchable: true,
|
||||
links: get(acc[snakeCase(section.name)], 'links', []).concat(
|
||||
section.links
|
||||
.filter(link => link.isDisplayed !== false)
|
||||
.map(link => {
|
||||
link.plugin = !isEmpty(plugins[link.plugin]) ? link.plugin : plugins[current].id;
|
||||
|
||||
return link;
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Generate the list of plugin links (plugins without a mainComponent should not appear in the left menu)
|
||||
const pluginsLinks = Object.values(plugins)
|
||||
.filter(
|
||||
plugin => plugin.id !== 'email' && plugin.id !== 'content-manager' && !!plugin.mainComponent
|
||||
)
|
||||
.map(plugin => {
|
||||
const pluginSuffixUrl = plugin.suffixUrl ? plugin.suffixUrl(plugins) : '';
|
||||
|
||||
return {
|
||||
icon: get(plugin, 'icon') || 'plug',
|
||||
label: get(plugin, 'name'),
|
||||
destination: `/plugins/${get(plugin, 'id')}${pluginSuffixUrl}`,
|
||||
};
|
||||
});
|
||||
|
||||
const menu = {
|
||||
...contentTypesSections,
|
||||
plugins: {
|
||||
searchable: false,
|
||||
name: 'plugins',
|
||||
emptyLinksListMessage: messages.noPluginsInstalled.id,
|
||||
links: pluginsLinks,
|
||||
},
|
||||
general: {
|
||||
searchable: false,
|
||||
name: 'general',
|
||||
links: [
|
||||
{
|
||||
icon: 'list',
|
||||
label: messages.listPlugins.id,
|
||||
destination: '/list-plugins',
|
||||
},
|
||||
{
|
||||
icon: 'shopping-basket',
|
||||
label: messages.installNewPlugin.id,
|
||||
destination: '/marketplace',
|
||||
},
|
||||
{
|
||||
icon: 'cog',
|
||||
label: messages.settings.id,
|
||||
destination: SETTINGS_BASE_URL,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{Object.keys(menu).map(current => (
|
||||
<LeftMenuLinkSection
|
||||
key={current}
|
||||
links={menu[current].links}
|
||||
section={current}
|
||||
location={location}
|
||||
searchable={menu[current].searchable}
|
||||
emptyLinksListMessage={menu[current].emptyLinksListMessage}
|
||||
/>
|
||||
))}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
LeftMenuLinkContainer.propTypes = {
|
||||
plugins: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default LeftMenuLinkContainer;
|
||||
@ -1,47 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* StyledListRow
|
||||
*
|
||||
*/
|
||||
|
||||
import styled from 'styled-components';
|
||||
import { CustomRow as Row } from '@buffetjs/styles';
|
||||
import { sizes } from 'strapi-helper-plugin';
|
||||
|
||||
const StyledListRow = styled(Row)`
|
||||
td {
|
||||
p {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
&:first-of-type {
|
||||
width: 65px;
|
||||
padding-left: 30px;
|
||||
> div {
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
&:nth-of-type(2) {
|
||||
max-width: 158px;
|
||||
}
|
||||
&:nth-of-type(3) {
|
||||
max-width: 300px;
|
||||
}
|
||||
&:nth-of-type(4) {
|
||||
min-width: 125px;
|
||||
}
|
||||
&:nth-of-type(5) {
|
||||
.popup-wrapper {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
@media (min-width: ${sizes.wide}) {
|
||||
&:nth-of-type(3) {
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledListRow;
|
||||
@ -11,6 +11,7 @@ import { isObject } from 'lodash';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Remove } from '@buffetjs/icons';
|
||||
import Li, { GlobalNotification } from './Li';
|
||||
|
||||
class Notification extends React.Component {
|
||||
// eslint-disable-line react/prefer-stateless-function
|
||||
handleCloseClicked = () => {
|
||||
@ -0,0 +1,2 @@
|
||||
export { default as Notification } from './Notification';
|
||||
export { default as NotificationsContainer } from './NotificationsContainer';
|
||||
@ -1,33 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Button = styled.button`
|
||||
display: flex;
|
||||
height: 20px !important;
|
||||
width: 88px;
|
||||
padding: 0 10px;
|
||||
border-radius: 2px;
|
||||
background-color: #ee8948;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
> span {
|
||||
height: 20px;
|
||||
padding: 0 !important;
|
||||
color: #fff;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
> i,
|
||||
> svg {
|
||||
margin-top: 1px;
|
||||
margin-right: 6px;
|
||||
vertical-align: -webkit-baseline-middle;
|
||||
|
||||
color: #ffdc00;
|
||||
font-size: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Button;
|
||||
@ -1,29 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* Official
|
||||
*
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import Button from './Button';
|
||||
|
||||
function Official(props) {
|
||||
return (
|
||||
<Button style={props.style}>
|
||||
<i className="fa fa-star" />
|
||||
<FormattedMessage id="app.components.Official" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
Official.defaultProps = {
|
||||
style: {},
|
||||
};
|
||||
|
||||
Official.propTypes = {
|
||||
style: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Official;
|
||||
@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const NumberCard = styled.div`
|
||||
width: 1.9rem;
|
||||
height: 1.4rem;
|
||||
background-color: ${({ theme }) => theme.main.colors.white};
|
||||
border-radius: ${({ theme }) => theme.main.sizes.borderRadius};
|
||||
`;
|
||||
|
||||
export default NumberCard;
|
||||
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Button, Flex, Text, Padded } from '@buffetjs/core';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import NumberCard from './NumberCard';
|
||||
|
||||
const ButtonWithNumber = ({ number, onClick }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<Button disabled color="primary" onClick={onClick}>
|
||||
<Flex style={{ minWidth: '17rem' }} justifyContent="space-between" alignItems="center">
|
||||
<FontAwesomeIcon icon="users" />
|
||||
<Padded left size="sm" />
|
||||
<Text color="grey" fontWeight="semiBold">
|
||||
{formatMessage({
|
||||
id: 'Settings.roles.form.button.users-with-role',
|
||||
defaultMessage: 'Users with this role',
|
||||
})}
|
||||
</Text>
|
||||
<Padded left size="sm" />
|
||||
<NumberCard>
|
||||
<Text fontSize="xs" fontWeight="bold" color="grey">
|
||||
{number}
|
||||
</Text>
|
||||
</NumberCard>
|
||||
</Flex>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonWithNumber.defaultProps = {
|
||||
number: 0,
|
||||
};
|
||||
ButtonWithNumber.propTypes = {
|
||||
number: PropTypes.number,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ButtonWithNumber;
|
||||
@ -0,0 +1,28 @@
|
||||
/* eslint-disable indent */
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
color: ${({ theme }) => theme.main.colors.mediumBlue};
|
||||
${({ isRight }) =>
|
||||
isRight &&
|
||||
`
|
||||
position: absolute;
|
||||
right: 5rem;
|
||||
`}
|
||||
${({ hasConditions, disabled, theme }) =>
|
||||
hasConditions &&
|
||||
`
|
||||
&:before {
|
||||
content: '•';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: -15px;
|
||||
font-size: 18px;
|
||||
color: ${disabled ? theme.main.colors.grey : theme.main.colors.mediumBlue};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Flex, Text, Padded } from '@buffetjs/core';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Wrapper from './Wrapper';
|
||||
|
||||
const ConditionsButton = ({ onClick, className, hasConditions, isRight }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
isRight={isRight}
|
||||
hasConditions={hasConditions}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Padded right size="smd">
|
||||
<Flex alignItems="center">
|
||||
<Text color="mediumBlue">
|
||||
{formatMessage({ id: 'app.components.LeftMenuLinkContainer.settings' })}
|
||||
</Text>
|
||||
<Padded style={{ height: '18px', lineHeight: 'normal' }} left size="xs">
|
||||
<FontAwesomeIcon style={{ fontSize: '11px' }} icon="cog" />
|
||||
</Padded>
|
||||
</Flex>
|
||||
</Padded>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
ConditionsButton.defaultProps = {
|
||||
className: null,
|
||||
hasConditions: false,
|
||||
isRight: false,
|
||||
};
|
||||
ConditionsButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
hasConditions: PropTypes.bool,
|
||||
isRight: PropTypes.bool,
|
||||
};
|
||||
|
||||
// This is a styled component advanced usage :
|
||||
// Used to make a ref to a non styled component.
|
||||
// https://styled-components.com/docs/advanced#caveat
|
||||
export default styled(ConditionsButton)``;
|
||||
@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Text, Padded, Flex } from '@buffetjs/core';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import ConditionsSelect from '../ConditionsSelect';
|
||||
import ActionRowWrapper from './ActionRowWrapper';
|
||||
|
||||
const ActionRow = ({ value, onChange, isGrey, action }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<ActionRowWrapper isGrey={isGrey}>
|
||||
<Padded style={{ width: 200 }} top left right bottom size="sm">
|
||||
<Flex>
|
||||
<Text
|
||||
lineHeight="19px"
|
||||
color="grey"
|
||||
fontSize="xs"
|
||||
fontWeight="bold"
|
||||
textTransform="uppercase"
|
||||
>
|
||||
{formatMessage({
|
||||
id: 'Settings.permissions.conditions.can',
|
||||
})}
|
||||
|
||||
</Text>
|
||||
<Text
|
||||
title={action.displayName}
|
||||
lineHeight="19px"
|
||||
fontWeight="bold"
|
||||
fontSize="xs"
|
||||
textTransform="uppercase"
|
||||
color="mediumBlue"
|
||||
style={{ maxWidth: '60%' }}
|
||||
ellipsis
|
||||
>
|
||||
{action.displayName}
|
||||
</Text>
|
||||
<Text
|
||||
lineHeight="19px"
|
||||
color="grey"
|
||||
fontSize="xs"
|
||||
fontWeight="bold"
|
||||
textTransform="uppercase"
|
||||
>
|
||||
|
||||
{formatMessage({
|
||||
id: 'Settings.permissions.conditions.when',
|
||||
})}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Padded>
|
||||
<ConditionsSelect onChange={onChange} value={value} />
|
||||
</ActionRowWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
ActionRow.defaultProps = {
|
||||
value: [],
|
||||
};
|
||||
ActionRow.propTypes = {
|
||||
action: PropTypes.object.isRequired,
|
||||
isGrey: PropTypes.bool.isRequired,
|
||||
value: PropTypes.array,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
export default ActionRow;
|
||||
@ -0,0 +1,11 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const ActionRowWrapper = styled.div`
|
||||
display: flex;
|
||||
height: 36px;
|
||||
border-radius: 2px;
|
||||
margin-bottom: 18px;
|
||||
background-color: ${({ theme, isGrey }) => (isGrey ? '#fafafb' : theme.main.colors.white)};
|
||||
`;
|
||||
|
||||
export default ActionRowWrapper;
|
||||
@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Separator = styled.div`
|
||||
padding-top: 1.4rem;
|
||||
margin-bottom: 2.8rem;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.main.colors.brightGrey};
|
||||
`;
|
||||
|
||||
export default Separator;
|
||||
@ -0,0 +1,85 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Modal, ModalHeader, ModalFooter } from 'strapi-helper-plugin';
|
||||
import { Button, Text, Padded } from '@buffetjs/core';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import Separator from './Separator';
|
||||
import ActionRow from './ActionRow';
|
||||
|
||||
const ConditionsModal = ({ isOpen, onToggle, actions, onClosed, initialConditions, onSubmit }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const [conditions, setConditions] = useState(initialConditions);
|
||||
|
||||
const handleSelectChange = (action, conditions) => {
|
||||
setConditions(prev => ({
|
||||
...prev,
|
||||
[action]: conditions,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit(conditions);
|
||||
onToggle();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal withoverflow="true" onClosed={onClosed} isOpen={isOpen} onToggle={onToggle}>
|
||||
<ModalHeader
|
||||
headerBreadcrumbs={[
|
||||
'Settings.permissions.conditions.links',
|
||||
'app.components.LeftMenuLinkContainer.settings',
|
||||
]}
|
||||
/>
|
||||
<Padded top left right bottom size="md">
|
||||
<Text fontSize="lg" fontWeight="bold">
|
||||
{formatMessage({
|
||||
id: 'Settings.permissions.conditions.define-conditions',
|
||||
})}
|
||||
</Text>
|
||||
<Separator />
|
||||
{actions.length === 0 && (
|
||||
<Text fontSize="md" color="grey">
|
||||
{formatMessage({ id: 'Settings.permissions.conditions.no-actions' })}
|
||||
</Text>
|
||||
)}
|
||||
{actions.map((action, index) => (
|
||||
<ActionRow
|
||||
key={action.id}
|
||||
action={action}
|
||||
isGrey={index % 2 === 0}
|
||||
value={conditions[action.id]}
|
||||
onChange={val => handleSelectChange(action.id, val)}
|
||||
/>
|
||||
))}
|
||||
</Padded>
|
||||
<ModalFooter>
|
||||
<section>
|
||||
<Button type="button" color="cancel" onClick={onToggle}>
|
||||
{formatMessage({ id: 'app.components.Button.cancel' })}
|
||||
</Button>
|
||||
|
||||
<Button type="button" color="success" onClick={handleSubmit}>
|
||||
{formatMessage({
|
||||
id: 'Settings.permissions.conditions.apply',
|
||||
})}
|
||||
</Button>
|
||||
</section>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
ConditionsModal.defaultProps = {
|
||||
initialConditions: {},
|
||||
};
|
||||
|
||||
ConditionsModal.propTypes = {
|
||||
actions: PropTypes.array.isRequired,
|
||||
initialConditions: PropTypes.object,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
onClosed: PropTypes.func.isRequired,
|
||||
};
|
||||
export default ConditionsModal;
|
||||
@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Remove } from '@buffetjs/icons';
|
||||
import { components } from 'react-select';
|
||||
|
||||
const ClearIndicator = props => {
|
||||
const Component = components.ClearIndicator;
|
||||
|
||||
return (
|
||||
<Component {...props}>
|
||||
<Remove width="11px" height="11px" fill="#9EA7B8" />
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClearIndicator;
|
||||
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Flex } from '@buffetjs/core';
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Wrapper = styled(Flex)`
|
||||
height: 100%;
|
||||
width: 32px;
|
||||
background: #fafafb;
|
||||
> svg {
|
||||
align-self: center;
|
||||
font-size: 11px;
|
||||
color: #b3b5b9;
|
||||
}
|
||||
`;
|
||||
|
||||
const DropdownIndicator = ({ selectProps: { menuIsOpen } }) => {
|
||||
const icon = menuIsOpen ? 'caret-up' : 'caret-down';
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<FontAwesomeIcon icon={icon} />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
DropdownIndicator.propTypes = {
|
||||
selectProps: PropTypes.shape({
|
||||
menuIsOpen: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
Wrapper.defaultProps = {
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
};
|
||||
|
||||
export default DropdownIndicator;
|
||||
@ -0,0 +1,3 @@
|
||||
const IndicatorSeparator = () => null;
|
||||
|
||||
export default IndicatorSeparator;
|
||||
@ -0,0 +1,48 @@
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { Collapse } from 'reactstrap';
|
||||
|
||||
const ToggleUl = styled(Collapse)`
|
||||
font-size: 13px;
|
||||
padding: 12px 15px 0 15px;
|
||||
list-style: none;
|
||||
background-color: #fff;
|
||||
> li {
|
||||
padding-top: 5px;
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.check-wrapper {
|
||||
z-index: 9;
|
||||
> input {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
> li:not(:last-child) {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
const SubUl = ({ children, isOpen }) => {
|
||||
return (
|
||||
<ToggleUl tag="ul" isOpen={isOpen}>
|
||||
{children}
|
||||
</ToggleUl>
|
||||
);
|
||||
};
|
||||
|
||||
SubUl.defaultProps = {
|
||||
children: null,
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
SubUl.propTypes = {
|
||||
children: PropTypes.node,
|
||||
isOpen: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default SubUl;
|
||||
@ -0,0 +1,79 @@
|
||||
/* eslint-disable indent */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Ul = styled.ul`
|
||||
max-height: 150px;
|
||||
font-size: 13px;
|
||||
padding: 0 15px;
|
||||
margin-bottom: 0px;
|
||||
list-style: none;
|
||||
background-color: #fff;
|
||||
> li {
|
||||
label {
|
||||
flex-shrink: 1;
|
||||
width: fit-content !important;
|
||||
cursor: pointer;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.check-wrapper {
|
||||
z-index: 9;
|
||||
> input {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
.chevron {
|
||||
margin: auto;
|
||||
|
||||
font-size: 11px;
|
||||
color: #919bae;
|
||||
}
|
||||
}
|
||||
.li-multi-menu {
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
.li {
|
||||
line-height: 27px;
|
||||
position: relative;
|
||||
> p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
> p::after {
|
||||
content: attr(datadescr);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #007eff;
|
||||
font-weight: 700;
|
||||
z-index: 100;
|
||||
}
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: -30px;
|
||||
right: -30px;
|
||||
bottom: 0;
|
||||
background-color: #e6f0fb;
|
||||
}
|
||||
}
|
||||
}
|
||||
${({ disabled, theme }) =>
|
||||
disabled &&
|
||||
`
|
||||
label {
|
||||
cursor: default !important;
|
||||
}
|
||||
input[type='checkbox'] {
|
||||
&:after {
|
||||
cursor: default;
|
||||
color: ${theme.main.colors.grey};
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export default Ul;
|
||||
@ -0,0 +1,6 @@
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import { upperFirst } from 'lodash';
|
||||
|
||||
const UpperFirst = ({ content }) => upperFirst(content);
|
||||
|
||||
export default UpperFirst;
|
||||
@ -0,0 +1,124 @@
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { components } from 'react-select';
|
||||
import { groupBy, intersectionWith } from 'lodash';
|
||||
import { Checkbox, Flex } from '@buffetjs/core';
|
||||
import { Label } from '@buffetjs/styles';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import SubUl from './SubUl';
|
||||
import Ul from './Ul';
|
||||
import UpperFirst from './UpperFirst';
|
||||
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
|
||||
const MenuList = ({ selectProps, ...rest }) => {
|
||||
const Component = components.MenuList;
|
||||
const [collapses, setCollapses] = useState({});
|
||||
const optionsGroupByCategory = groupBy(selectProps.options, 'category');
|
||||
const toggleCollapse = collapseName => {
|
||||
setCollapses(prevState => ({ ...prevState, [collapseName]: !collapses[collapseName] }));
|
||||
};
|
||||
|
||||
const hasAction = useCallback(
|
||||
condition => {
|
||||
return selectProps.value.includes(condition.id);
|
||||
},
|
||||
[selectProps.value]
|
||||
);
|
||||
|
||||
const hasSomeCategoryAction = useCallback(
|
||||
category => {
|
||||
const formattedCategories = category[1].map(condition => condition.id);
|
||||
|
||||
const categoryActions = intersectionWith(formattedCategories, selectProps.value).length;
|
||||
|
||||
return categoryActions > 0 && categoryActions < formattedCategories.length;
|
||||
},
|
||||
[selectProps.value]
|
||||
);
|
||||
|
||||
const hasAllCategoryAction = useCallback(
|
||||
category => {
|
||||
const formattedCategories = category[1].map(condition => condition.id);
|
||||
|
||||
const categoryActions = intersectionWith(formattedCategories, selectProps.value).length;
|
||||
|
||||
return categoryActions === formattedCategories.length;
|
||||
},
|
||||
[selectProps.value]
|
||||
);
|
||||
|
||||
return (
|
||||
<Component {...rest}>
|
||||
<Ul disabled>
|
||||
{Object.entries(optionsGroupByCategory).map((category, index) => {
|
||||
return (
|
||||
<li key={category[0]}>
|
||||
<div>
|
||||
<Flex justifyContent="space-between">
|
||||
<Label htmlFor="overrideReactSelectBehavior">
|
||||
<Flex>
|
||||
<Checkbox
|
||||
id="checkCategory"
|
||||
name={category[0]}
|
||||
onChange={() => {}}
|
||||
value={hasAllCategoryAction(category)}
|
||||
someChecked={hasSomeCategoryAction(category)}
|
||||
/>
|
||||
<UpperFirst content={category[0]} />
|
||||
</Flex>
|
||||
</Label>
|
||||
<div
|
||||
style={{ flex: 1, textAlign: 'end', cursor: 'pointer' }}
|
||||
onClick={() => toggleCollapse(category[0])}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
style={{
|
||||
margin: 'auto',
|
||||
fontSize: '11px',
|
||||
color: '#919bae',
|
||||
}}
|
||||
icon={collapses[category[0]] ? 'chevron-up' : 'chevron-down'}
|
||||
/>
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
<SubUl tag="ul" isOpen={collapses[category[0]]}>
|
||||
{category[1].map(action => {
|
||||
return (
|
||||
<li key={action.id}>
|
||||
<Flex>
|
||||
<Label htmlFor={action.id} message={action.displayName}>
|
||||
<Flex>
|
||||
<Checkbox
|
||||
id="check"
|
||||
name={action.id}
|
||||
// Remove the handler
|
||||
onChange={() => {}}
|
||||
value={hasAction(action)}
|
||||
/>
|
||||
{action.displayName}
|
||||
</Flex>
|
||||
</Label>
|
||||
</Flex>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</SubUl>
|
||||
{index + 1 < Object.entries(optionsGroupByCategory).length && (
|
||||
<div style={{ paddingTop: '17px' }} />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</Ul>
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
MenuList.propTypes = {
|
||||
selectProps: PropTypes.object.isRequired,
|
||||
};
|
||||
export default MenuList;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user