Merge pull request #6965 from strapi/features/rbac

Features/rbac
This commit is contained in:
Alexandre BODIN 2020-07-20 19:07:46 +02:00 committed by GitHub
commit c2fb4904cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1073 changed files with 43479 additions and 12501 deletions

View File

@ -43,6 +43,7 @@ module.exports = {
},
},
rules: {
'import/no-unresolved': 0,
'generator-star-spacing': 0,
'no-console': 0,
'require-atomic-updates': 0,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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,
};
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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!

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -45,10 +45,6 @@
],
"plugin": "upload",
"required": false
},
"full_name": {
"type": "string",
"required": true
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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.

View File

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

View File

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

View File

@ -101,3 +101,4 @@ node_modules
test
testApp
coverage
webpack.config.dev.js

View 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.

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -135,6 +135,7 @@ const GlobalStyle = createGlobalStyle`
::-webkit-scrollbar {
width: 9px;
height: 5px;
}
::-webkit-scrollbar-track {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
export { default as Notification } from './Notification';
export { default as NotificationsContainer } from './NotificationsContainer';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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',
})}
&nbsp;
</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"
>
&nbsp;
{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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
const IndicatorSeparator = () => null;
export default IndicatorSeparator;

View File

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

View File

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

View File

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

View File

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