Merge branch 'master' into docs/clean_local_plugin

This commit is contained in:
Jim LAURIE 2020-04-20 16:01:28 +02:00 committed by GitHub
commit f22c595e49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
670 changed files with 21818 additions and 10189 deletions

View File

@ -1,6 +1,7 @@
**/node_modules/**
**/build/**
**/dist/**
**/OLD/**
testApp/**
examples/**
packages/strapi-generate-plugin/files/admin/src/**

View File

@ -47,7 +47,7 @@ module.exports = {
'no-console': 0,
'require-atomic-updates': 0,
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'react-hooks/exhaustive-deps': 'error',
'arrow-body-style': 0,
'arrow-parens': 0,
camelcase: 0,
@ -64,10 +64,7 @@ module.exports = {
{
flatTernaryExpressions: false,
SwitchCase: 1,
ignoredNodes: [
'ConditionalExpression',
"VariableDeclarator[kind='const']",
],
ignoredNodes: ['ConditionalExpression', "VariableDeclarator[kind='const']"],
},
],
'func-names': ['error', 'never'],
@ -97,10 +94,7 @@ module.exports = {
'no-plusplus': 0,
'no-shadow': 0,
'no-underscore-dangle': 0,
'no-use-before-define': [
'error',
{ functions: false, classes: false, variables: false },
],
'no-use-before-define': ['error', { functions: false, classes: false, variables: false }],
'object-curly-newline': [2, { multiline: true, consistent: true }],
'operator-linebreak': 0,
'padding-line-between-statements': [

View File

@ -16,6 +16,7 @@ https://guides.github.com/features/mastering-markdown/
A clear and concise description of what the bug is.
**Steps to reproduce the behavior**
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@ -31,6 +32,7 @@ If applicable, add screenshots to help explain your problem.
If applicable, add code samples to help explain your problem.
**System**
- Node.js version: <!-- Please ensure you are using the Node LTS version (v12) -->
- NPM version:
- Strapi version: <!-- Please make sure you are on the latest version -->

View File

@ -3,17 +3,13 @@ services:
- mysql
addons:
postgresql: '10'
apt:
packages:
- postgresql-10
- postgresql-client-10
postgresql: '11.2'
env:
global:
- PGPORT=5433
sudo: required
dist: trusty
dist: xenial
language: node_js
@ -33,14 +29,6 @@ e2e_tests: &e2e_tests
- yarn run -s test:start-app & wait-on http://localhost:1337
- yarn run -s test:e2e
install_mongo: &install_mongo
before_install:
- wget http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.6.6.tgz
- tar -zxvf mongodb-linux-x86_64-3.6.6.tgz
- mkdir -p ./data/db/27017
- mkdir -p ./data/db/27000
- ./mongodb-linux-x86_64-3.6.6/bin/mongod --fork --dbpath ./data/db/27017 --syslog --port 27017
before_script:
- yarn build
- yarn global add -g wait-on
@ -50,11 +38,17 @@ jobs:
include:
- stage: test
name: Snyk
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash
- export PATH="$HOME/.yarn/bin:$PATH"
script: yarn run -s test:snyk
if: fork = false
- stage: test
name: 'Lint / Unit Tests '
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash
- export PATH="$HOME/.yarn/bin:$PATH"
script:
- yarn run -s lint
- yarn run -s test:unit --coverage && codecov -C -F unit
@ -63,8 +57,13 @@ jobs:
- <<: *e2e_tests
name: 'E2E Postgresql'
before_install:
- sudo cp /etc/postgresql/{9.6,10}/main/pg_hba.conf
- sudo /etc/init.d/postgresql restart
- curl -o- -L https://yarnpkg.com/install.sh | bash
- export PATH="$HOME/.yarn/bin:$PATH"
- sudo apt-get update
- sudo apt-get --yes remove postgresql-*
- sudo apt-get install -y postgresql-11 postgresql-client-11
- sudo cp /etc/postgresql/{9.6,11}/main/pg_hba.conf
- sudo service postgresql restart 11
- psql -c 'create database strapi_test;' -U postgres
env:
- DB_STRING='--dbclient=postgres --dbhost=localhost --dbport=5433 --dbname=strapi_test --dbusername=postgres --dbpassword='
@ -72,6 +71,8 @@ jobs:
- <<: *e2e_tests
name: 'E2E Mysql'
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash
- export PATH="$HOME/.yarn/bin:$PATH"
- sudo cp $TRAVIS_BUILD_DIR/_travis/mysql.cnf /etc/mysql/conf.d/
- sudo service mysql restart
- mysql -e 'CREATE DATABASE strapi_test;'
@ -80,21 +81,21 @@ jobs:
- <<: *e2e_tests
name: 'E2E Sqlite'
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash
- export PATH="$HOME/.yarn/bin:$PATH"
env:
- DB_STRING='--dbclient=sqlite --dbfile=./tmp/data.db'
- <<: *e2e_tests
name: 'E2E MongoDB'
<<: *install_mongo
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash
- export PATH="$HOME/.yarn/bin:$PATH"
- wget http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.6.6.tgz
- tar -zxvf mongodb-linux-x86_64-3.6.6.tgz
- mkdir -p ./data/db/27017
- mkdir -p ./data/db/27000
- ./mongodb-linux-x86_64-3.6.6/bin/mongod --fork --dbpath ./data/db/27017 --syslog --port 27017
env:
- DB_STRING='--dbclient=mongo --dbhost=localhost --dbport=27017 --dbname=strapi_test --dbusername= --dbpassword='
# - name: 'Cypress tests'
# <<: *install_mongo
# install:
# - yarn run -s bootstrap
# - yarn global add -g wait-on
# - cypress install
# script:
# - yarn run -s test:generate-app -- --dbclient=mongo --dbhost=localhost --dbport=27017 --dbname=strapi_test --dbusername= --dbpassword=
# - yarn run -s test:start-app & wait-on http://localhost:1337
# - node test/cypress.js

View File

@ -213,6 +213,8 @@ module.exports = {
'/3.0.0-beta.x/guides/scheduled-publication',
'/3.0.0-beta.x/guides/secure-your-app',
'/3.0.0-beta.x/guides/send-email',
'/3.0.0-beta.x/guides/registering-a-field-in-admin',
'/3.0.0-beta.x/guides/count-graphql',
'/3.0.0-beta.x/guides/client',
'/3.0.0-beta.x/guides/update-version',
],
@ -220,7 +222,11 @@ module.exports = {
{
collapsable: true,
title: '⚙️️ Admin Panel',
children: ['/3.0.0-beta.x/admin-panel/customization', '/3.0.0-beta.x/admin-panel/deploy'],
children: [
'/3.0.0-beta.x/admin-panel/customization',
'/3.0.0-beta.x/admin-panel/custom-webpack-config',
'/3.0.0-beta.x/admin-panel/deploy',
],
},
{
collapsable: true,
@ -241,6 +247,7 @@ module.exports = {
'/3.0.0-beta.x/plugin-development/plugin-architecture',
'/3.0.0-beta.x/plugin-development/backend-development',
'/3.0.0-beta.x/plugin-development/frontend-development',
'/3.0.0-beta.x/plugin-development/frontend-field-api',
'/3.0.0-beta.x/plugin-development/frontend-settings-api',
],
},

View File

@ -0,0 +1,16 @@
# Custom Webpack Config
In order to extend the usage of webpack, you can define a function that extends its config inside `admin/admin.config.js`, like so:
```js
module.exports = {
webpack: (config, webpack) => {
// Note: we provide webpack above so you should not `require` it
// Perform customizations to webpack config
// Important: return the modified config
config.plugins.push(new webpack.IgnorePlugin(/\/__tests__\//));
return config;
},
};
```

View File

@ -78,6 +78,8 @@ options: [--no-optimization]
- **strapi build**<br/>
Builds the administration panel and minimizing the assets
- **strapi build --clean**<br/>
Builds the administration panel and delete the previous build and .cache folders
- **strapi build --no-optimization**<br/>
Builds the administration panel without minimizing the assets. The build duration is faster.

View File

@ -132,6 +132,12 @@ Additional settings can be set on models:
In this example, the model `Restaurant` will be accessible through the `Restaurants` global variable. The data will be stored in the `Restaurants_v1` collection or table and the model will use the `mongo` connection defined in `./config/environments/**/database.json`
::: warning
If not set manually in the JSON file, Strapi will adopt the filename as `globalId`.
The `globalId` serves as a reference to your model within relations and Strapi APIs. If you chose to rename it (either by renaming your file or by changing the value of the `globalId`), you'd have to migrate your tables manually and update the references.
Please note that you should not alter Strapi's models `globalId` (plugins and core ones) since it is used directly within Strapi APIs and other models' relations.
:::
::: tip
The `connection` value can be changed whenever you want, but you should be aware that there is no automatic data migration process. Also if the new connection doesn't use the same ORM you will have to rewrite your queries.
:::
@ -159,10 +165,7 @@ The info key on the model-json states information about the model. This informat
The options key on the model-json states.
- `idAttribute`: This tells the model which attribute to expect as the unique identifier for each database row (typically an auto-incrementing primary key named 'id'). _Only valid for bookshelf._
- `idAttributeType`: Data type of `idAttribute`, accepted list of value below. _Only valid for bookshelf._
- `timestamps`: This tells the model which attributes to use for timestamps. Accepts either `boolean` or `Array` of strings where first element is create date and second element is update date. Default value when set to `true` for Bookshelf is `["created_at", "updated_at"]` and for MongoDB is `["createdAt", "updatedAt"]`.
- `uuid` : Boolean to enable UUID support on MySQL, you will need to set the `idAttributeType` to `uuid` as well and install the `bookshelf-uuid` package. To load the package you can see [this example](./configurations.md#bookshelf-mongoose).
**Path —** `User.settings.json`.

View File

@ -137,12 +137,13 @@ module.exports = {
*/
async create(data, { files } = {}) {
const entry = await strapi.query(model).create(data);
const entry = await strapi.query('restaurant').create(data);
if (files) {
// automatically uploads the files based on the entry and the model
await strapi.entityService.uploadFiles(entry, files, {
model: strapi.models.restaurant,
model: 'restaurant',
// if you are using a plugin's model you will have to add the `plugin` key (plugin: 'users-permissions')
});
return this.findOne({ id: entry.id });
}
@ -167,12 +168,13 @@ module.exports = {
*/
async update(params, data, { files } = {}) {
const entry = await strapi.query(model).update(params, data);
const entry = await strapi.query('restaurant').update(params, data);
if (files) {
// automatically uploads the files based on the entry and the model
await strapi.entityService.uploadFiles(entry, files, {
model: strapi.models.restaurant,
model: 'restaurant',
// if you are using a plugin's model you will have to add the `plugin` key (plugin: 'users-permissions')
});
return this.findOne({ id: entry.id });
}

View File

@ -0,0 +1,307 @@
# Google App Engine
In this guide we are going to:
- Create a new Strapi project
- Configure PostgreSQL for the production enviroment
- Deploy the app to Google App Engine
- Add the [Google Cloud Storage file uploading plugin](https://github.com/Lith/strapi-provider-upload-google-cloud-storage) by [@Lith](https://github.com/Lith)
### New Strapi project
:::: tabs
::: tab yarn
Use **yarn** to install the Strapi project (**recommended**). [Install yarn with these docs](https://yarnpkg.com/lang/en/docs/install/)
```bash
yarn create strapi-app my-project --quickstart
```
:::
::: tab npx
Use **npm/npx** to install the Strapi project
```bash
npx create-strapi-app my-project --quickstart
```
:::
::::
When the setup completes, register an admin user using the form which opens in the browser. This user will be only relevant in local development.
The `sqlite` database is created at `.tmp/data.db`.
Login, but don't add content types yet. Close the browser. Quit the running app.
### Initial commit
This may be a good point to commit the files in their initial state.
```bash
cd my-project
git init
git add .
git commit -m first
```
### Install the Cloud SDK CLI tool
[Cloud SDK: Command Line Interface](https://cloud.google.com/sdk/)
### New App Engine project
Create a new [App Engine](https://console.cloud.google.com/appengine/) project.
Select the region, such as `europe-west`.
- Language: Node JS
- Environment: Standard (or Flexible)
(_A note on performance and cost_: the `Standard Environment` is sufficient for development, but it may not be for production. Review the resources your application will need to determine the cost. When you sign up for Google App Engine, it offers a certain amount of free credits which will not be billed.)
Create the project. Take note of the instance identifier, which is in the form of `<instance_id>:<region>:<instance_name>`.
Check if `gcloud` lists the project:
```bash
gcloud projects list
```
Run `init` to authenticate the cli, and select current cloud project.
```bash
gcloud init
```
### Create the database (PostgreSQL)
Create the [Cloud SQL database](https://cloud.google.com/sql/docs/postgres/create-manage-databases) which the app is going to use.
Take note of the user name (default is `postgres`) and password.
The first database will be created with the name `postgres`. This cannot be deleted.
Create another database, named `strapi` for example. It may be useful to delete and and re-create this while you are experimenting with the application setup.
### Create app.yaml and .gcloudignore
Create the `app.yaml` file in the project root.
Add `app.yaml` to `.gitignore`.
The instance identifier looks like `myapi-123456:europe-west1:myapi`.
The `myapi-123456` part is the project identifier. (The number is automatically added to short project names).
The following is an example config for `Standard Environment` or `Flexible Environment`.
:::: tabs
::: tab Standard Environment
```yaml
runtime: nodejs10
instance_class: F2
env_variables:
HOST: '<project_id>.appspot.com'
NODE_ENV: 'production'
DATABASE_NAME: 'strapi'
DATABASE_USERNAME: 'postgres'
DATABASE_PASSWORD: '<password>'
INSTANCE_CONNECTION_NAME: '<instance_identifier>'
beta_settings:
cloud_sql_instances: '<instance_identifier>'
```
:::
::: tab Flexible Environment
```yaml
runtime: nodejs10
env: flex
env_variables:
HOST: '<project_id>.appspot.com'
NODE_ENV: 'production'
DATABASE_NAME: 'strapi'
DATABASE_USERNAME: 'postgres'
DATABASE_PASSWORD: '<password>'
INSTANCE_CONNECTION_NAME: '<instance_identifier>'
beta_settings:
cloud_sql_instances: '<instance_identifier>'
```
:::
::::
Create `.gcloudignore` in the project root, include `app.yaml` here as well.
```
app.yaml
.gcloudignore
.git
.gitignore
node_modules/
#!include:.gitignore
```
In the case of Strapi, the admin UI will have to be re-built after every deploy,
and so we don't deploy local build artifacts, cache files and so on by including
the `.gitignore` entries.
### Configure the database
The `PostgreSQL` database will need the `pg` package.
```bash
yarn add pg
```
[Google App Engine requires](https://cloud.google.com/sql/docs/postgres/connect-app-engine) to connect to the database using the unix socket path, not an IP and port.
Edit `database.json`, and use the socket path as `host`.
```
config/environments/production/database.json
```
```json
{
"defaultConnection": "default",
"connections": {
"default": {
"connector": "bookshelf",
"settings": {
"client": "postgres",
"host": "/cloudsql/${process.env.INSTANCE_CONNECTION_NAME}",
"database": "${process.env.DATABASE_NAME}",
"username": "${process.env.DATABASE_USERNAME}",
"password": "${process.env.DATABASE_PASSWORD}"
},
"options": {}
}
}
}
```
Edit `server.json` to pick up the deployed hostname from the `HOST` variable in `app.yaml`.
```
config/environments/production/server.json
```
```json
{
"host": "${process.env.HOST}",
"port": "${process.env.PORT || 1337}",
"production": true,
"proxy": {
"enabled": false
},
"cron": {
"enabled": false
},
"admin": {
"autoOpen": false
}
}
```
### Auto-build after deploy
After deployment, the admin UI has to be re-built. This generates the contents of the `build` folder on the server.
In `package.json`, add the `gcp-build` command to `scripts`:
```json
{
"scripts": {
"gcp-build": "strapi build"
}
}
```
### Deploy
```bash
gcloud app deploy app.yaml --project myapi-123456
```
Watch the logs:
```bash
gcloud app logs tail --project=myapi-123456 -s default
```
Open the admin page and register and admin user.
```
https://myapp-123456.appspot.com/admin/
```
### File uploading to Google Cloud Storage
[Lith/strapi-provider-upload-google-cloud-storage](https://github.com/Lith/strapi-provider-upload-google-cloud-storage)
```bash
yarn add strapi-provider-upload-google-cloud-storage
```
Deploy so that the server app includes the dependency from `package.json`.
Create a Google service account key.
<https://console.cloud.google.com/apis/credentials/serviceaccountkey>
Save the JSON credentials file.
Plugins > File Upload > Settings > Production tab
By default `localhost` is selected. Select the `Google Cloud Storage` plugin.
Copy the JSON key and set the regions.
Open the `Cloud Console > Storage > Browser` menu.
Copy the bucket name to the plugin settings, the default is the app ID, such as `myapi-123456.appspot.com`.
(Note that the `Access control` setting of the bucket has to be `Fine-grained`, which is the default.)
Click `Save`, and it's ready to go!
### Post-setup configuration
**CORS**
CORS is enabled by default, allowing `*` origin. You may want to limit the allowed origins.
```
config/environments/production/security.json
```
**Changing the admin url**
```
config/environments/production/server.json
```
```json
{
"admin": {
"path": "/dashboard"
}
}
```

View File

@ -46,6 +46,18 @@ Manual guides for deployment on various platforms, for One-click and docker plea
</InstallLink>
</div>
<div>
<InstallLink link="../deployment/google-app-engine">
<template #icon>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24" version="1.1"><path d="M6.969 3L4.094 8.188l1.468 2.624L8.438 6h10.25L17 3zm8.75 4l2.969 4.906L13.625 21H17l5-9-2.781-5zM12 8c-2.207 0-4 1.793-4 4s1.793 4 4 4 4-1.793 4-4-1.793-4-4-4zM3.531 9.219L2 12l4.969 9H12.5l1.656-3h-5.75zM12 10c1.102 0 2 .898 2 2 0 1.102-.898 2-2 2-1.102 0-2-.898-2-2 0-1.102.898-2 2-2z" fill="#fff"/></svg>
</template>
<template #title>Google App Engine</template>
<template #description>
Manual step by step guide for deploying on GCP's App Engine
</template>
</InstallLink>
</div>
<div>
<InstallLink link="../deployment/heroku">
<template #icon>

View File

@ -74,15 +74,15 @@ To be able to see the update, you will need to have a Content Type that has a `d
Then you will have to investigate into the [`strapi-plugin-content-manager`](https://github.com/strapi/strapi/tree/master/packages/strapi-plugin-content-manager) package to find the file that is used to format the date for the list view.
Here is the [Row component](https://github.com/strapi/strapi/blob/master/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/Row.js) you will have to update.
Here is the [Row component](https://github.com/strapi/strapi/blob/master/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/Row.js) which requires a [dedicated file](https://github.com/strapi/strapi/blob/master/packages/strapi-plugin-content-manager/admin/src/utils/dateFormats.js) to modify the date display.
### Eject the file
Let's eject the file to be able to customize it.
**Path —** `./extensions/content-manager/admin/src/components/CustomTable/Row.js`
**Path —** `./extensions/content-manager/admin/src/utils/dateFormats.js`
In this new file, paste the current [Row component](https://github.com/strapi/strapi/blob/master/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/Row.js) code.
In this new file, paste the current [dateFormats](https://github.com/strapi/strapi/blob/master/packages/strapi-plugin-content-manager/admin/src/utils/dateFormats.js) code.
To run your application, you will have to run the `yarn develop --watch-admin` command.
@ -95,13 +95,17 @@ In our example, we want to change the format of the date. We have to find in thi
Here is the code you have to find:
```js
return moment
.parseZone(date)
.utc()
.format('dddd, MMMM Do YYYY');
const dateFormats = {
...defaultDateFormats,
// Customise the format by uncommenting the one you wan to override it corresponds to the type of your field
// date: 'dddd, MMMM Do YYYY',
// datetime: 'dddd, MMMM Do YYYY HH:mm',
// time: 'HH:mm A',
// timestamp: 'dddd, MMMM Do YYYY HH:mm',
};
```
Now let's replace `.format('dddd, MMMM Do YYYY');` by `.format('YYYY/MM/DD');`
Now let's replace `date: 'dddd, MMMM Do YYYY'` by `date: 'YYYY/MM/DD';`
And tada, the date will now display with the new format.

View File

@ -72,9 +72,7 @@ module.exports = {
entities = await strapi.services.article.find(ctx.query);
}
return entities.map(entity =>
sanitizeEntity(entity, { model: strapi.models.article })
);
return entities.map(entity => sanitizeEntity(entity, { model: strapi.models.article }));
},
};
```
@ -99,7 +97,7 @@ module.exports = {
ctx.query = {
...ctx.query,
status: 'published'
status: 'published',
};
if (ctx.query._q) {
@ -108,9 +106,7 @@ module.exports = {
entities = await strapi.services.article.find(ctx.query);
}
return entities.map(entity =>
sanitizeEntity(entity, { model: strapi.models.article })
);
return entities.map(entity => sanitizeEntity(entity, { model: strapi.models.article }));
},
};
```
@ -120,4 +116,3 @@ And tada! Draft and archived articles disappeared.
::: tip
This guide can be applied to any other controller action.
:::

View File

@ -0,0 +1,335 @@
# Creating a new Field in the administration panel
In this guide we will see how you can create a new Field for your administration panel.
## Introduction
For this example, we will see how to change the WYSIWYG with [CKEditor](https://ckeditor.com/ckeditor-5/) in the **`Content Manager`** plugin by creating a new plugin which will add a new **Field** in your application.
## Setup
1. Create a new project:
```bash
# Create an application using SQLite and prevent the server from starting automatically as we will create a plugin
# right after the project generation
yarn create strapi-app my-app --quickstart --no-run
```
2. Generate a plugin:
```bash
yarn run strapi generate:plugin wysiwyg
```
3. Install the needed dependencies:
```bash
cd my-app/plugins/wysiwyg
yarn add @ckeditor/ckeditor5-react @ckeditor/ckeditor5-build-classic
```
4. Start your application with the front-end development mode:
```bash
cd my-app
yarn develop --watch-admin
```
Once this step is over all we need to do is to create our new WYSIWYG which will replace the default one in the **Content Manager** plugin.
### Creating the WYSIWYG
In this part we will create three components:
- MediaLib which will be used to insert media in the editor
- Wysiwyg which will wrap the CKEditor with a label and the errors
- CKEditor which will be the implementation of the new WYSIWYG
### Creating the MediaLib
**Path —** `./plugins/wysiwyg/admin/src/components/MediaLib/index.js`
```js
import React, { useEffect, useState } from 'react';
import { useStrapi, prefixFileUrlWithBackendUrl } from 'strapi-helper-plugin';
import PropTypes from 'prop-types';
const MediaLib = ({ isOpen, onChange, onToggle }) => {
const {
strapi: {
componentApi: { getComponent },
},
} = useStrapi();
const [data, setData] = useState(null);
const [isDisplayed, setIsDisplayed] = useState(false);
useEffect(() => {
if (isOpen) {
setIsDisplayed(true);
}
}, [isOpen]);
const Component = getComponent('media-library').Component;
const handleInputChange = data => {
if (data) {
const { url } = data;
setData({ ...data, url: prefixFileUrlWithBackendUrl(url) });
}
};
const handleClosed = () => {
if (data) {
onChange(data);
}
setData(null);
setIsDisplayed(false);
};
if (Component && isDisplayed) {
return (
<Component
allowedTypes={['images', 'videos', 'files']}
isOpen={isOpen}
multiple={false}
noNavigation
onClosed={handleClosed}
onInputMediaChange={handleInputChange}
onToggle={onToggle}
/>
);
}
return null;
};
MediaLib.defaultProps = {
isOpen: false,
onChange: () => {},
onToggle: () => {},
};
MediaLib.propTypes = {
isOpen: PropTypes.bool,
onChange: PropTypes.func,
onToggle: PropTypes.func,
};
export default MediaLib;
```
#### Creating the WYSIWYG Wrapper
**Path —** `./plugins/wysiwyg/admin/src/components/Wysiwyg/index.js`
```js
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { isEmpty } from 'lodash';
import { Button } from '@buffetjs/core';
import { Label, InputDescription, InputErrors } from 'strapi-helper-plugin';
import Editor from '../CKEditor';
import MediaLib from '../MediaLib';
const Wysiwyg = ({
inputDescription,
errors,
label,
name,
noErrorsDescription,
onChange,
value,
}) => {
const [isOpen, setIsOpen] = useState(false);
let spacer = !isEmpty(inputDescription) ? <div style={{ height: '.4rem' }} /> : <div />;
if (!noErrorsDescription && !isEmpty(errors)) {
spacer = <div />;
}
const handleChange = data => {
if (data.mime.includes('image')) {
const imgTag = `<p><img src="${data.url}" caption="${data.caption}" alt="${data.alternativeText}"></img></p>`;
const newValue = value ? `${value}${imgTag}` : imgTag;
onChange({ target: { name, value: newValue } });
}
// Handle videos and other type of files by adding some code
};
const handleToggle = () => setIsOpen(prev => !prev);
return (
<div
style={{
marginBottom: '1.6rem',
fontSize: '1.3rem',
fontFamily: 'Lato',
}}
>
<Label htmlFor={name} message={label} style={{ marginBottom: 10 }} />
<div>
<Button color="primary" onClick={handleToggle}>
MediaLib
</Button>
</div>
<Editor name={name} onChange={onChange} value={value} />
<InputDescription
message={inputDescription}
style={!isEmpty(inputDescription) ? { marginTop: '1.4rem' } : {}}
/>
<InputErrors errors={(!noErrorsDescription && errors) || []} name={name} />
{spacer}
<MediaLib onToggle={handleToggle} isOpen={isOpen} onChange={handleChange} />
</div>
);
};
Wysiwyg.defaultProps = {
errors: [],
inputDescription: null,
label: '',
noErrorsDescription: false,
value: '',
};
Wysiwyg.propTypes = {
errors: PropTypes.array,
inputDescription: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
PropTypes.shape({
id: PropTypes.string,
params: PropTypes.object,
}),
]),
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func,
PropTypes.shape({
id: PropTypes.string,
params: PropTypes.object,
}),
]),
name: PropTypes.string.isRequired,
noErrorsDescription: PropTypes.bool,
onChange: PropTypes.func.isRequired,
value: PropTypes.string,
};
export default Wysiwyg;
```
#### Implementing CKEditor
**Path —** `./plugins/wysiwyg/admin/src/components/CKEditor/index.js`
```js
import React from 'react';
import PropTypes from 'prop-types';
import CKEditor from '@ckeditor/ckeditor5-react';
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
import styled from 'styled-components';
const Wrapper = styled.div`
.ck-editor__main {
min-height: 200px;
> div {
min-height: 200px;
}
}
`;
const configuration = {
toolbar: [
'heading',
'|',
'bold',
'italic',
'link',
'bulletedList',
'numberedList',
'|',
'indent',
'outdent',
'|',
'blockQuote',
'insertTable',
'mediaEmbed',
'undo',
'redo',
],
};
const Editor = ({ onChange, name, value }) => {
return (
<Wrapper>
<CKEditor
editor={ClassicEditor}
config={configuration}
data={value}
onChange={(event, editor) => {
const data = editor.getData();
onChange({ target: { name, value: data } });
}}
/>
</Wrapper>
);
};
Editor.propTypes = {
onChange: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string,
};
export default Editor;
```
At this point we have simply created a new plugin which is mounted in our project but our custom **Field** has not been registered yet.
### Registering a our new Field
Since the goal of our plugin is to override the current WYSIWYG we don't want it to be displayed in the administration panel but we need it to register our new **Field**. In order to do so, we will simply modify the front-end entry point of our plugin:
**Path —** `./plugins/wysiwyg/admin/src/index.js`
```js
import pluginPkg from '../../package.json';
import Wysiwyg from './components/Wysiwyg';
import pluginId from './pluginId';
export default strapi => {
const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
const plugin = {
blockerComponent: null,
blockerComponentProps: {},
description: pluginDescription,
icon: pluginPkg.strapi.icon,
id: pluginId,
initializer: () => null,
injectedComponents: [],
isReady: true,
isRequired: pluginPkg.strapi.required || false,
leftMenuLinks: [],
leftMenuSections: [],
mainComponent: null,
name: pluginPkg.strapi.name,
preventComponentRendering: false,
settings: null,
trads: {},
};
strapi.registerField({ type: 'wysiwyg', Component: Wysiwyg });
return strapi.registerPlugin(plugin);
};
```
And VOILA, if you create a new `collectionType` or a `singleType` with a `richtext` field you will see the implementation of [CKEditor](https://ckeditor.com/ckeditor-5/) instead of the default WYSIWYG.

View File

@ -46,7 +46,7 @@ Now you can run `node server.js` and it will start your application.
By following their Node.js onboarding, we need to require the Sqreen node_module where the server is started.
Also, Sqreen has to be required just before Strapi to work!
*This is the reason why we have created a `server.js` file.*
_This is the reason why we have created a `server.js` file._
To do so, you will have to update this file.

View File

@ -11,6 +11,7 @@ Read the [Migration guide from alpha.26 to beta](migration-guide-alpha.26-to-bet
- [Migration guide from beta.17+ to beta.18](migration-guide-beta.17-to-beta.18.md)
- [Migration guide from beta.18 to beta.19](migration-guide-beta.18-to-beta.19.md)
- [Migration guide from beta.19+ to beta.19.4](migration-guide-beta.19-to-beta.19.4.md)
- [Migration guide from beta.19.4+ to beta.20](migration-guide-beta.19-to-beta.20.md)
## Alpha guides

View File

@ -74,7 +74,7 @@ In some cases (with Heroku, Docker...), listening to `localhost` won't work. In
### Example
**After -** `./config/environments/**/server.js`
**Before -** `./config/environments/**/server.js`
```json
{

View File

@ -0,0 +1,217 @@
# Migration guide from beta.19.x to beta.20
Upgrading your Strapi application to `v3.0.0-beta.20`.
**Make sure your server is not running until the end of the migration**
## Upgrading your dependencies
Start by upgrading your dependencies. Make sure to use exact versions.
Update your package.json accordingly:
```json
{
//...
"dependencies": {
"strapi": "3.0.0-beta.20",
"strapi-admin": "3.0.0-beta.20",
"strapi-connector-bookshelf": "3.0.0-beta.20",
"strapi-plugin-content-manager": "3.0.0-beta.20",
"strapi-plugin-content-type-builder": "3.0.0-beta.20",
"strapi-plugin-email": "3.0.0-beta.20",
"strapi-plugin-graphql": "3.0.0-beta.20",
"strapi-plugin-upload": "3.0.0-beta.20",
"strapi-plugin-users-permissions": "3.0.0-beta.20",
"strapi-utils": "3.0.0-beta.20"
}
}
```
Then run either `yarn install` or `npm install`.
## Index page
Some users have been asking to make the `/` route customizable and to be able to disable it.
To allow customizations, the server will now serve the files in your `./public` folder as is. To migrate, you must delete the `index.html` and `production.html` files in the `./public` directory.
From now on, if you don't have any `index.html` file in your `./public` folder, the server will render the default Strapi homepage.
You can now also disable this behavior with the `public.defaultIndex` option. Read the documentation [here](../concepts/configurations.md#application).
## Upload plugin settings
A lot of our users have been requesting that we move some back-end specific configurations to files. While implementing the media library feature, we decided to move the upload plugin settings to files.
This means that you now have to configure your provider directly in the files. You can read the documentation [here](../plugins/upload.md#using-a-provider) to update.
## MongoDB Media relation changes
::: tip
The guide below only applies if you are using MongoDB/Mongoose, if you are using a Bookshelf database you can simply skip to the [rebuilding step](#rebuilding-your-administration-panel)
:::
In the media library features, We wanted to make sure media would keep their ordering. To implement this in mongo we had to change the way the media relation was built.
Previously, the `upload_file` collection was the one keeping track of the relations and the entity related to the file had not reference to it.
Implementing ordering without changes the relations proved unfeasible. Finally we decided to add the reverse reference in the entities so it would make accessing the files really easy.
You will hence need to migrate your `mongo` database to avoid losing references to your files.
### Backup your database
When running in production, you should always backup your database before running migrations. To backup a `mongo` database, look at the documentation [here](https://docs.mongodb.com/manual/core/backups/)
### Export model metadatas
First create a `export.js` file at the root of your project with the following content:
```js
const fs = require('fs');
require('strapi')()
.load()
.then(() => {
const models = {};
Object.keys(strapi.api).forEach(apiName => {
Object.values(strapi.api[apiName].models || {}).forEach(model => {
models[model.globalId] = formatModel(model);
});
});
Object.keys(strapi.plugins).forEach(pluginName => {
Object.values(strapi.plugins[pluginName].models || {}).forEach(model => {
models[model.globalId] = formatModel(model);
});
});
Object.values(strapi.components).forEach(model => {
models[model.globalId] = formatModel(model);
});
fs.writeFileSync('models.json', JSON.stringify(models, null, 2));
process.exit(0);
});
function formatModel(model) {
return {
collection: model.collectionName,
files: Object.keys(model.attributes).reduce((acc, key) => {
const attr = model.attributes[key];
if (attr.model === 'file' && attr.plugin === 'upload') {
acc[key] = 'single';
}
if (attr.collection === 'file' && attr.plugin === 'upload') {
acc[key] = 'multiple';
}
return acc;
}, {}),
};
}
```
then run the script from the root of your project
```sh
node export.js
```
This script will create a models.json file at the root of your project that looks like:
```json
{
"ModelName": {
"collection": "collectionName",
"files": {
"image": "single",
"images": "multiple"
}
}
}
```
### Create migration script
You then need to create the mongo shell script to run to migrate your data strucutre. First create a `migration.js` file with the following code:
```js
var models = {
/* paste the object model.json here */
};
for (var i in models) {
var model = models[i];
var update = {};
var keyCount = 0;
for (var key in model.files) {
keyCount += 1;
update[key] = '';
}
if (keyCount > 0) {
db.getCollection(model.collection).update({}, { $unset: update }, { multi: true });
}
}
var fileCursor = db.getCollection('upload_file').find({});
while (fileCursor.hasNext()) {
var el = fileCursor.next();
el.related.forEach(function(fileRef) {
var model = models[fileRef.kind];
if (!model) {
return;
}
var fieldType = model.files && model.files[fileRef.field];
// stop if the file points to a field the user didn't specify
if (!fieldType) {
return;
}
if (fieldType === 'single') {
db.getCollection(model.collection).updateOne(
{ _id: fileRef.ref },
{ $set: { [fileRef.field]: el._id } }
);
} else if (fieldType === 'multiple') {
db.getCollection(model.collection).updateOne(
{ _id: fileRef.ref },
{ $push: { [fileRef.field]: el._id } }
);
}
});
}
```
Then you will need to copy the content of `models.json` to the `models` variable at the top of this script:
```js
var models = {
ModelName: {
collection: 'collectionName',
files: {
image: 'single',
images: 'multiple',
},
},
};
// rest of the script
```
Finally you can load this script in your mongo shell and run it.
Once your migration is done you can delete the `export.js` and `models.json` files from your project. You are all set !
## Rebuilding your administration panel
You can run `yarn build --clean` or `npm run build -- --clean` to rebuild your admin panel with the newly installed version of strapi.
Finally restart your server: `yarn develop` or `npm run develop`.

View File

@ -0,0 +1,160 @@
# Plugin's front-end Component API
As plugins developer you may need to add custom components in your application so other plugin may use them. To do so, a **Component API** is available in order for a plugin to register a component which will be available for all plugins.
## Registering a new component
Registering a component can be made in two different ways:
1. During the load phase of a plugin
2. Using the provided `react-hook` in a component.
### Registering a component during the load of a plugin
Registering a component during the load phase of a plugin can be done as follows:
1. Create a new Field type (in this example a **`media`** field type):
**Path —** `plugins/my-plugin/admin/src/components/MyComponent/index.js`.
```js
import React from 'react';
const MyComponent = () => {
return <div>MyComponent</div>;
};
export default MyComponent;
```
2. Register the field into the application:
**Path —** `plugins/my-plugin/admin/src/index.js`.
```js
import pluginPkg from '../../package.json';
import MyComponent from './components/MyComponent';
import pluginId from './pluginId';
export default strapi => {
const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
const plugin = {
blockerComponent: null,
blockerComponentProps: {},
description: pluginDescription,
icon: pluginPkg.strapi.icon,
id: pluginId,
initializer: () => null,
injectedComponents: [],
isReady: true,
leftMenuLinks: [],
leftMenuSections: [],
mainComponent: null,
name: pluginPkg.strapi.name,
preventComponentRendering: false,
trads: {},
};
strapi.registerComponent({ name: 'my-component', Component: MyComponent });
return strapi.registerPlugin(plugin);
};
```
By doing so, all the plugins from your project will be able to use the newly registered **Component**.
### Registering a component inside a React Component
The other way to register a **Component** is to use the provided `react-hook`: **`useStrapi`** it can be done in the `Initializer` Component so it is accessible directly when the user is logged in, if you decide to register your plugin in another component than the `Initializer` the **Component** will only be registered in the administration panel once the component is mounted (the user has navigated to the view where the **Component** is registered).
1. Register the **Component** in the `Initializer` Component:
**Path —** `plugins/my-plugin/admin/src/containers/Initializer/index.js`.
```js
/**
*
* Initializer
*
*/
import { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useStrapi } from 'strapi-helper-plugin';
import pluginId from '../../pluginId';
import MyComponent from './components/MyComponent';
const Initializer = ({ updatePlugin }) => {
const {
strapi: { componentApi },
} = useStrapi();
const ref = useRef();
ref.current = updatePlugin;
useEffect(() => {
// Register the new component
strapi.componentApi.registerComponent({ name: 'my-component', Component: MyComponent });
ref.current(pluginId, 'isReady', true);
}, []);
return null;
};
Initializer.propTypes = {
updatePlugin: PropTypes.func.isRequired,
};
export default Initializer;
```
2. Add the `Initializer` component to your plugin so it is mounted in the administration panel once the user is logged in:
```js
import pluginPkg from '../../package.json';
import pluginLogo from './assets/images/logo.svg';
import App from './containers/App';
import Initializer from './containers/Initializer';
import lifecycles from './lifecycles';
import trads from './translations';
import pluginId from './pluginId';
export default strapi => {
const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
const plugin = {
blockerComponent: null,
blockerComponentProps: {},
description: pluginDescription,
icon: pluginPkg.strapi.icon,
id: pluginId,
initializer: Initializer,
injectedComponents: [],
isRequired: pluginPkg.strapi.required || false,
layout: null,
lifecycles,
leftMenuLinks: [],
leftMenuSections: [],
mainComponent: App,
name: pluginPkg.strapi.name,
pluginLogo,
preventComponentRendering: false,
trads,
};
return strapi.registerPlugin(plugin);
};
```
## Consuming the Component API
Consuming the **Component** API can only be done by using the provided `react-hook` **`useStrapi`**.
## Component API definition
| Method | Param | Description |
| :---------------- | :------------ | :----------------------------------------- |
| getComponent | {String} name | Retrieve a Component depending on the name |
| getComponents | | Retrieve all the Components |
| registerComponent | {Object} | Register a Component |
| removeComponent | | Remove a Component |

View File

@ -0,0 +1,234 @@
# Plugin's front-end Field API
As plugins developer you may need to add custom fields in your application. To do so, a **Field API** is available in order for a plugin to register a field which will be available for all plugins.
::: warning NOTE
Currently, only the content manager uses this API to extend its current fields.
:::
## Registering a new field
Registering a field can be made in two different ways:
1. During the load phase of a plugin
2. Using the provided `react-hook` in a component.
### Registering a field during the load of a plugin
Registering a field during the load phase of a plugin can be done as follows:
1. Create a new Field type (in this example a **`media`** field type):
**Path —** `plugins/my-plugin/admin/src/components/InputMedia/index.js`.
```js
import React from 'react';
const InputMedia = props => {
// Check out the provided props
console.log(props);
return <div>InputMedia</div>;
};
export default InputMedia;
```
2. Register the field into the application:
**Path —** `plugins/my-plugin/admin/src/index.js`.
```js
import pluginPkg from '../../package.json';
import InputMedia from './components/InputMedia';
import pluginId from './pluginId';
export default strapi => {
const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
const plugin = {
blockerComponent: null,
blockerComponentProps: {},
description: pluginDescription,
icon: pluginPkg.strapi.icon,
id: pluginId,
initializer: () => null,
injectedComponents: [],
isReady: true,
leftMenuLinks: [],
leftMenuSections: [],
mainComponent: null,
name: pluginPkg.strapi.name,
preventComponentRendering: false,
trads: {},
};
strapi.registerField({ type: 'media', Component: InputMedia });
return strapi.registerPlugin(plugin);
};
```
By doing so, all the plugins from your project will be able to use the newly registered **Field** type.
### Registering a field inside a React Component
The other way to register a **Field** is to use the provided `react-hook`: **`useStrapi`** it can be done in the `Initializer` Component so it is accessible directly when the user is logged in, if you decide to register your plugin in another component than the `Initializer` the **Field** will only be registered in the administration panel once the component is mounted (the user has navigated to the view where the **Field** is registered).
1. Register the **Field** in the `Initializer` Component:
**Path —** `plugins/my-plugin/admin/src/containers/Initializer/index.js`.
```js
/**
*
* Initializer
*
*/
import { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useStrapi } from 'strapi-helper-plugin';
import pluginId from '../../pluginId';
import InputMedia from './components/InputMedia';
const Initializer = ({ updatePlugin }) => {
const {
strapi: { fieldApi },
} = useStrapi();
const ref = useRef();
ref.current = updatePlugin;
useEffect(() => {
// Register the new field
strapi.fieldApi.registerField({ type: 'media', Component: InputMedia });
ref.current(pluginId, 'isReady', true);
}, []);
return null;
};
Initializer.propTypes = {
updatePlugin: PropTypes.func.isRequired,
};
export default Initializer;
```
2. Add the `Initializer` component to your plugin so it is mounted in the administration panel once the user is logged in:
```js
import pluginPkg from '../../package.json';
import pluginLogo from './assets/images/logo.svg';
import App from './containers/App';
import Initializer from './containers/Initializer';
import lifecycles from './lifecycles';
import trads from './translations';
import pluginId from './pluginId';
export default strapi => {
const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
const plugin = {
blockerComponent: null,
blockerComponentProps: {},
description: pluginDescription,
icon: pluginPkg.strapi.icon,
id: pluginId,
initializer: Initializer,
injectedComponents: [],
isRequired: pluginPkg.strapi.required || false,
layout: null,
lifecycles,
leftMenuLinks: [],
leftMenuSections: [],
mainComponent: App,
name: pluginPkg.strapi.name,
pluginLogo,
preventComponentRendering: false,
trads,
};
return strapi.registerPlugin(plugin);
};
```
## Consuming the Field API
Consuming the **Field** API can only be done by using the provided `react-hook` **`useStrapi`**. Here's an example from the **content-manager** plugin:
**Path —** `~/strapi-plugin-content-manager/admin/src/components/Inputs/index.js`.
```js
import React, { memo, useMemo } from 'react';
// Other imports
// ...
// Import the Inputs component from our component library Buffet.js
import { Inputs as InputsIndex } from '@buffetjs/custom';
// Import the Hook with which you can access the Field API
import { useStrapi } from 'strapi-helper-plugin';
function Inputs({ autoFocus, keys, layout, name, onBlur }) {
// This is where you will access the field API
const {
strapi: { fieldApi },
} = useStrapi();
// Other boilerplate code
// ...
return (
<FormattedMessage id={errorId}>
{error => {
return (
<InputsIndex
{...metadatas}
autoComplete="new-password"
autoFocus={autoFocus}
didCheckErrors={didCheckErrors}
disabled={disabled}
error={
isEmpty(error) || errorId === temporaryErrorIdUntilBuffetjsSupportsFormattedMessage
? null
: error
}
inputDescription={description}
description={description}
contentTypeUID={layout.uid}
customInputs={{
json: InputJSONWithErrors,
wysiwyg: WysiwygWithErrors,
uid: InputUID,
// Retrieve all the fields that other plugins have registered
...fieldApi.getFields(),
}}
multiple={get(attribute, 'multiple', false)}
attribute={attribute}
name={keys}
onBlur={onBlur}
onChange={onChange}
options={enumOptions}
step={step}
type={getInputType(type)}
validations={validations}
value={inputValue}
withDefaultValue={false}
/>
);
}}
</FormattedMessage>
);
}
```
## Field API definition
| Method | Param | Description |
| :------------ | :------------ | :------------------------------------- |
| getField | {String} type | Retrieve a Field depending on the type |
| getFields | | Retrieve all the Fields |
| registerField | {Object} | Register a Field |
| removeField | | Remove a Field |

View File

@ -19,13 +19,12 @@ The menu section can be declared as follows:
**Path —** `plugins/my-plugin/admin/src/index.js`.
```
```js
import pluginPkg from '../../package.json';
import pluginId from './pluginId';
export default strapi => {
const pluginDescription =
pluginPkg.strapi.description || pluginPkg.description;
const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
// Declare the links that will be injected into the settings menu
const menuSection = {
@ -90,7 +89,7 @@ With the configuration from above we could easily create our plugin Settings vie
**Path —** `plugins/my-plugin/admin/src/containers/Settings/index.js`.
```
```js
import React from 'react';
import PropTypes from 'prop-types';
import { Switch, Route } from 'react-router-dom';
@ -110,14 +109,8 @@ const SettingPage2 = () => (
const Settings = ({ settingsBaseURL }) => {
return (
<Switch>
<Route
component={SettingPage1}
path={`${settingsBaseURL}/${pluginId}/setting1`}
/>
<Route
component={SettingPage2}
path={`${settingsBaseURL}/${pluginId}/setting2`}
/>
<Route component={SettingPage1} path={`${settingsBaseURL}/${pluginId}/setting1`} />
<Route component={SettingPage2} path={`${settingsBaseURL}/${pluginId}/setting2`} />
</Switch>
);
};
@ -133,15 +126,14 @@ Now that the `Settings` component is declared in your plugin the only thing left
**Path —** `plugins/my-plugin/admin/src/index.js`.
```
```js
import pluginPkg from '../../package.json';
// Import the component
import Settings from './containers/Settings';
import pluginId from './pluginId';
export default strapi => {
const pluginDescription =
pluginPkg.strapi.description || pluginPkg.description;
const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
// Declare the links that will be injected into the settings menu
const menuSection = {
@ -191,3 +183,83 @@ export default strapi => {
return strapi.registerPlugin(plugin);
};
```
## Adding a setting into the global section
In order to add a link into the global section of the settings view you need to create a global array containing the links you want to add:
**Path —** `plugins/my-plugin/admin/src/index.js`.
```js
import pluginPkg from '../../package.json';
// Import the component
import Settings from './containers/Settings';
import SettingLink from './components/SettingLink';
import pluginId from './pluginId';
export default strapi => {
const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
// Declare the links that will be injected into the settings menu
const menuSection = {
id: pluginId,
title: {
id: `${pluginId}.foo`,
defaultMessage: 'Super cool setting',
},
links: [
{
title: 'Setting page 1',
to: `${strapi.settingsBaseURL}/${pluginId}/setting1`,
name: 'setting1',
},
{
title: {
id: `${pluginId}.bar`,
defaultMessage: 'Setting page 2',
},
to: `${strapi.settingsBaseURL}/${pluginId}/setting2`,
name: 'setting2',
},
],
};
const plugin = {
blockerComponent: null,
blockerComponentProps: {},
description: pluginDescription,
icon: pluginPkg.strapi.icon,
id: pluginId,
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,
},
],
mainComponent: Settings,
menuSection,
},
trads: {},
};
return strapi.registerPlugin(plugin);
};
```
::: danger
It is currently not possible to add a link into another plugin's setting section
:::

View File

@ -2,6 +2,26 @@
Thanks to the plugin `Upload`, you can upload any kind of file on your server or external providers such as **AWS S3**.
## Configuration
Currently the strapi middleware in charge of parsing request needs to be configured to support bigger file sizes if you need to upload file with a size greater than 200MB.
The library we use is [`koa-body`](https://github.com/dlau/koa-body), and itself uses the [`node-formidable`](https://github.com/felixge/node-formidable) library to process files.
You can pass configuration to the middleware directly by setting it in the `parser` middleware configuration:
```json
{
"parser": {
"enabled": true,
"multipart": true,
"formidable": {
"maxFileSize": 20000000 // defaults to 200mb
}
}
}
```
## Endpoints
<style lang="stylus">
@ -272,23 +292,41 @@ In our second example, you can upload and attach multiple pictures to the restau
}
```
## Install providers
## Using a provider
By default Strapi provides a local file upload system. You might want to upload your files on AWS S3 or another provider.
By default Strapi provides a provider that uploads files to a local directory. You might want to upload your files to another provider like AWS S3.
You can check all the available providers developed by the community on npmjs.org - [Providers list](https://www.npmjs.com/search?q=strapi-provider-upload-)
To install a new provider run:
```
$ npm install strapi-provider-upload-aws-s3@beta --save
$ npm install strapi-provider-upload-aws-s3 --save
```
::: tip
If the provider is not in the mono repo, you probably don't need `@beta` depending if the creator published it with this tag or not.
:::
or
Then, visit [http://localhost:1337/admin/plugins/upload/configurations/development](http://localhost:1337/admin/plugins/upload/configurations/development) on your web browser and configure the provider.
```
$ yarn add strapi-provider-upload-aws-s3
```
To enable the provider, create or edit the file at `./extensions/upload/config/settings.json`
```json
{
"provider": "aws-s3",
"providerOptions": {
"accessKeyId": "dev-key",
"secretAccessKey": "dev-secret",
"region": "aws-region",
"params": {
"Bucket": "my-bucket"
}
}
}
```
Make sure to read the provider's `README` to know what are the possible parameters.
::: tip
Some providers may have additional settings such as the AWS S3 needs an API endpoint URL. You can find a list of these for AWS [here](https://docs.aws.amazon.com/general/latest/gr/ses.html)
@ -296,24 +334,45 @@ Some providers may have additional settings such as the AWS S3 needs an API endp
## Create providers
If you want to create your own, make sure the name starts with `strapi-provider-upload-` (duplicating an existing one will be easier to create), modify the `auth` config object and customize the `upload` and `delete` functions.
You can create a Node.js module to implement a custom provider. Read the official documentation [here](https://docs.npmjs.com/creating-node-js-modules).
To use it you will have to publish it on **npm**.
To work with strapi, your provider name must match the pattern `strapi-provider-upload-{provider-name}`.
Your provider need to export the following interface:
```js
module.exports = {
init(providerOptions) {
// init your provider if necessary
return {
upload(file) {
// upload the file in the provider
},
delete(file) {
// delete the file in the provider
},
};
},
};
```
You can then publish it to make it available to the community.
### Create a local provider
If you want to create your own provider without publishing it on **npm** you can follow these steps:
- Create a `providers` folder in your application.
- Create your provider as explained in the documentation eg. `./providers/strapi-provider-upload-[...]/...`
- Then update your `package.json` to link your `strapi-provider-upload-[...]` dependency to the [local path](https://docs.npmjs.com/files/package.json#local-paths) of your new provider.
- Create a `./providers/strapi-provider-upload-{provider-name}` folder in your root application folder.
- Create your provider as explained in the [documentation](#create-providers) above.
- Then update your `package.json` to link your `strapi-provider-upload-{provider-name}` dependency to point to the [local path](https://docs.npmjs.com/files/package.json#local-paths) of your provider.
```json
{
...
"dependencies": {
...
"strapi-provider-upload-[...]": "file:providers/strapi-provider-upload-[...]",
"strapi-provider-upload-{provider-name}": "file:providers/strapi-provider-upload-{provider-name}"
...
}
}

View File

@ -1,4 +1,5 @@
{
"kind": "collectionType",
"collectionName": "addresses",
"info": {
"name": "address",
@ -13,10 +14,6 @@
"comment": ""
},
"attributes": {
"geolocation": {
"type": "json",
"required": true
},
"city": {
"type": "string",
"required": true
@ -32,12 +29,20 @@
"cover": {
"model": "file",
"via": "related",
"allowedTypes": [
"files",
"images",
"videos"
],
"plugin": "upload",
"required": false
},
"images": {
"collection": "file",
"via": "related",
"allowedTypes": [
"images"
],
"plugin": "upload",
"required": false
},

View File

@ -1,4 +1,5 @@
{
"kind": "collectionType",
"collectionName": "categories",
"info": {
"name": "category",

View File

@ -15,6 +15,27 @@
"slug": {
"type": "uid",
"targetField": "title"
},
"single": {
"model": "file",
"via": "related",
"allowedTypes": [
"images",
"files",
"videos"
],
"plugin": "upload",
"required": false
},
"multiple": {
"collection": "file",
"via": "related",
"allowedTypes": [
"images",
"videos"
],
"plugin": "upload",
"required": false
}
}
}

View File

@ -43,7 +43,6 @@
"max": 35.12
},
"address": {
"required": true,
"model": "address"
},
"cover": {

View File

@ -0,0 +1,8 @@
module.exports = {
// provider: 'cloudinary',
// providerOptions: {
// cloud_name: 'cloud-name',
// api_key: 'api-key',
// api_secret: 'api-secret',
// },
};

View File

@ -1,7 +1,7 @@
{
"name": "getstarted",
"private": true,
"version": "3.0.0-beta.19.5",
"version": "3.0.0-beta.20",
"description": "A Strapi application.",
"scripts": {
"develop": "strapi develop",
@ -15,21 +15,22 @@
"mysql": "^2.17.1",
"pg": "^7.10.0",
"sqlite3": "^4.0.6",
"strapi": "3.0.0-beta.19.5",
"strapi-admin": "3.0.0-beta.19.5",
"strapi-connector-bookshelf": "3.0.0-beta.19.5",
"strapi-connector-mongoose": "3.0.0-beta.19.5",
"strapi-middleware-views": "3.0.0-beta.19.5",
"strapi-plugin-content-manager": "3.0.0-beta.19.5",
"strapi-plugin-content-type-builder": "3.0.0-beta.19.5",
"strapi-plugin-documentation": "3.0.0-beta.19.5",
"strapi-plugin-email": "3.0.0-beta.19.5",
"strapi-plugin-graphql": "3.0.0-beta.19.5",
"strapi-plugin-upload": "3.0.0-beta.19.5",
"strapi-plugin-users-permissions": "3.0.0-beta.19.5",
"strapi-provider-email-mailgun": "3.0.0-beta.19.5",
"strapi-provider-upload-aws-s3": "3.0.0-beta.19.5",
"strapi-utils": "3.0.0-beta.19.5"
"strapi": "3.0.0-beta.20",
"strapi-admin": "3.0.0-beta.20",
"strapi-connector-bookshelf": "3.0.0-beta.20",
"strapi-connector-mongoose": "3.0.0-beta.20",
"strapi-middleware-views": "3.0.0-beta.20",
"strapi-plugin-content-manager": "3.0.0-beta.20",
"strapi-plugin-content-type-builder": "3.0.0-beta.20",
"strapi-plugin-documentation": "3.0.0-beta.20",
"strapi-plugin-email": "3.0.0-beta.20",
"strapi-plugin-graphql": "3.0.0-beta.20",
"strapi-plugin-upload": "3.0.0-beta.20",
"strapi-plugin-users-permissions": "3.0.0-beta.20",
"strapi-provider-email-mailgun": "3.0.0-beta.20",
"strapi-provider-upload-aws-s3": "3.0.0-beta.20",
"strapi-provider-upload-cloudinary": "3.0.0-beta.20",
"strapi-utils": "3.0.0-beta.20"
},
"strapi": {
"uuid": "getstarted"

View File

@ -2,8 +2,7 @@ import pluginPkg from '../../package.json';
import pluginId from './pluginId';
export default strapi => {
const pluginDescription =
pluginPkg.strapi.description || pluginPkg.description;
const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
const plugin = {
blockerComponent: null,

View File

@ -13,7 +13,9 @@ module.exports = {
],
globals: {
__webpack_public_path__: 'http://localhost:4000',
strapi: {},
strapi: {
backendURL: 'http://localhost:1337',
},
BACKEND_URL: 'http://localhost:1337',
MODE: 'host',
PUBLIC_PATH: '/admin',
@ -26,8 +28,7 @@ module.exports = {
'<rootDir>/test/config/front',
],
moduleNameMapper: {
'.*\\.(css|less|styl|scss|sass)$':
'<rootDir>/test/config/front/mocks/cssModule.js',
'.*\\.(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',
},
@ -37,6 +38,7 @@ module.exports = {
'/node_modules/',
'<rootDir>/examples/getstarted/',
'<rootDir>/packages/strapi-helper-plugin/dist/',
'/OLD/',
],
setupFilesAfterEnv: [
'<rootDir>/test/config/front/enzyme-setup.js',
@ -48,8 +50,6 @@ module.exports = {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/fileTransformer.js',
},
transformIgnorePatterns: [
'node_modules/(?!(react-dnd|dnd-core|react-dnd-html5-backend)/)',
],
transformIgnorePatterns: ['node_modules/(?!(react-dnd|dnd-core|react-dnd-html5-backend)/)'],
testURL: 'http://localhost:4000/admin',
};

View File

@ -1,5 +1,5 @@
{
"version": "3.0.0-beta.19.5",
"version": "3.0.0-beta.20",
"packages": [
"packages/*",
"examples/*"

View File

@ -1,6 +1,6 @@
{
"name": "create-strapi-app",
"version": "3.0.0-beta.19.5",
"version": "3.0.0-beta.20",
"description": "Generate a new Strapi application.",
"license": "MIT",
"homepage": "http://strapi.io",
@ -21,7 +21,7 @@
],
"dependencies": {
"commander": "^2.20.0",
"strapi-generate-new": "3.0.0-beta.19.5"
"strapi-generate-new": "3.0.0-beta.20"
},
"scripts": {
"test": "echo \"no tests yet\""

View File

@ -1,9 +1,16 @@
// /**
// *
// * app.js
// *
// * Entry point of the application
// */
/**
*
* app.js
*
* Entry point of the application
*/
// NOTE TO PLUGINS DEVELOPERS:
// If you modify this file by adding new options to a plugin entry point
// Here's the file: strapi/docs/3.0.0-beta.x/plugin-development/frontend-field-api.md
// Here's the file: strapi/docs/3.0.0-beta.x/guides/registering-a-field-in-admin.md
// Also the strapi-generate-plugins/files/admin/src/index.js needs to be updated
// IF THE DOC IS NOT UPDATED THE PULL REQUEST WILL NOT BE MERGED
/* eslint-disable */
@ -21,7 +28,8 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
// Strapi provider with the internal APIs
import { StrapiProvider } from 'strapi-helper-plugin';
import { merge } from 'lodash';
import { Fonts } from '@buffetjs/styles';
import { freezeApp, pluginLoaded, unfreezeApp, updatePlugin } from './containers/App/actions';
@ -30,6 +38,7 @@ import { showNotification } from './containers/NotificationProvider/actions';
import basename from './utils/basename';
import injectReducer from './utils/injectReducer';
import injectSaga from './utils/injectSaga';
import Strapi from './utils/Strapi';
// Import root component
import App from './containers/App';
@ -47,6 +56,8 @@ import history from './utils/history';
import plugins from './plugins';
const strapi = Strapi();
const initialState = {};
const store = configureStore(initialState, history);
const { dispatch } = store;
@ -57,7 +68,13 @@ Object.keys(plugins).forEach(current => {
return plugin;
};
const currentPluginFn = plugins[current];
// By updating this by adding required methods
// to load a plugin you need to update this file
// strapi-generate-plugins/files/admin/src/index.js needs to be updated
const plugin = currentPluginFn({
registerComponent: strapi.componentApi.registerComponent,
registerField: strapi.fieldApi.registerField,
registerPlugin,
settingsBaseURL: SETTINGS_BASE_URL || '/settings',
});
@ -150,12 +167,14 @@ window.strapi = Object.assign(window.strapi || {}, {
const render = messages => {
ReactDOM.render(
<Provider store={store}>
<Fonts />
<LanguageProvider messages={messages}>
<BrowserRouter basename={basename}>
<App store={store} />
</BrowserRouter>
</LanguageProvider>
<StrapiProvider strapi={strapi}>
<Fonts />
<LanguageProvider messages={messages}>
<BrowserRouter basename={basename}>
<App store={store} />
</BrowserRouter>
</LanguageProvider>
</StrapiProvider>
</Provider>,
MOUNT_NODE
);

View File

@ -18,8 +18,7 @@ const EventInput = ({ onChange, name: inputName, value: inputValue }) => {
media: ['media.create', 'media.update', 'media.delete'],
};
// Media update disabled for now - until the media libray is ready
const disabledEvents = ['media.update'];
const disabledEvents = [];
const formattedValue = formatValue(inputValue);

View File

@ -60,6 +60,72 @@ exports[`<EventInput /> should match the snapshot 1`] = `
font-size: 1.3rem;
}
.c4 {
cursor: pointer;
margin: 0;
position: relative;
width: 14px;
font-weight: 100;
}
.c4:focus,
.c4:active {
outline: 0;
}
.c4:before {
content: '';
position: absolute;
left: 0;
top: 50%;
margin-top: calc(-14px / 2);
width: 14px;
height: 14px;
border: 1px solid rgba(16,22,34,0.15);
background-color: #fdfdfd;
border-radius: 3px;
box-sizing: border-box;
}
.c4:after {
display: none;
content: '\\f00c';
font-family: 'FontAwesome';
position: absolute;
left: 0px;
top: 55%;
margin-top: calc(-14px / 2);
width: 14px;
height: 14px;
text-align: center;
font-size: 9px;
font-weight: 400;
color: #1C5DE7;
line-height: 14px;
}
.c4:checked:after {
display: block;
}
.c4:after {
content: '\\f068';
display: block;
top: 50%;
font-size: 10px;
}
.c4:disabled {
background-color: #FAFAFB;
cursor: initial;
}
.c4 + label {
display: inline-block;
font-weight: 400;
font-size: 1.3rem;
}
.c1 {
display: -webkit-box;
display: -webkit-flex;
@ -299,8 +365,8 @@ exports[`<EventInput /> should match the snapshot 1`] = `
<input
autoComplete="off"
autoFocus={false}
checked={true}
className="c2"
checked={false}
className="c4"
id="media"
name="media"
onChange={[Function]}
@ -348,7 +414,7 @@ exports[`<EventInput /> should match the snapshot 1`] = `
autoFocus={false}
checked={false}
className="c2"
disabled={true}
disabled={false}
id="media.update"
name="media.update"
onChange={[Function]}

View File

@ -15,7 +15,7 @@ const Wrapper = styled.div`
margin-bottom: 1rem;
}
> p {
width 100%;
width: 100%;
margin-bottom: -8px;
padding-top: 10px;
font-size: 13px;

View File

@ -448,7 +448,7 @@ exports[`<Inputs /> should match the snapshot if type is events 1`] = `
autoFocus={false}
checked={false}
className="c4"
disabled={true}
disabled={false}
id="media.update"
name="media.update"
onChange={[Function]}

View File

@ -48,12 +48,7 @@ const LeftMenuFooter = ({ version }) => {
defaultMessage={messages.poweredBy.defaultMessage}
key="poweredBy"
/>
<a
key="website"
href="https://strapi.io"
target="_blank"
rel="noopener noreferrer"
>
<a key="website" href="https://strapi.io" target="_blank" rel="noopener noreferrer">
Strapi
</a>
&nbsp;

View File

@ -5,14 +5,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
const FaIcon = styled(({ small, ...props }) => <FontAwesomeIcon {...props} />)`
position: absolute;
top: calc(50% - 0.9rem + 0.3rem);
left: 1.6rem;
margin-right: 1.2rem;
margin-top: ${({ small }) => (small ? '.3rem' : null)};
font-size: ${({ small }) => (small ? '.9rem' : '1.4rem')};
width: 1.4rem;
padding-bottom: 0.2rem;
text-align: center;
top: ${({ small }) => (small ? 'calc(50% - 0.3rem)' : 'calc(50% - 0.9rem + 0.3rem)')};
left: ${({ small }) => (small ? '2.2rem' : '1.6rem')};
font-size: ${({ small }) => (small ? '.5rem' : '1.2rem')};
`;
const LeftMenuIcon = ({ icon }) => <FaIcon small={icon === 'circle'} icon={icon} />;

View File

@ -19,7 +19,7 @@ const LinkLabel = styled.span`
display: inline-block;
width: 100%;
padding-right: 1rem;
padding-left: 2.6rem;
padding-left: 2.1rem;
`;
const LeftMenuLinkContent = ({

View File

@ -36,9 +36,7 @@ const LeftMenuLink = ({
label={label}
location={location}
source={source}
suffixUrlToReplaceForLeftMenuHighlight={
suffixUrlToReplaceForLeftMenuHighlight
}
suffixUrlToReplaceForLeftMenuHighlight={suffixUrlToReplaceForLeftMenuHighlight}
/>
{plugin}
</>

View File

@ -1,3 +1,4 @@
/* eslint-disable react/no-array-index-key */
import React from 'react';
import { useLocation } from 'react-router-dom';
import PropTypes from 'prop-types';

View File

@ -22,8 +22,7 @@ const Logout = ({ history: { push } }) => {
push({
pathname: `/plugins/content-manager/collectionType/strapi::administrator/${id}`,
search:
'?redirectUrl=/plugins/content-manager/collectionType/strapi::administrator/&_page=0&_limit=0&_sort=id',
search: '?redirectUrl=/plugins/content-manager/collectionType/strapi::administrator',
});
};
const handleGoToAdministrator = () => {

View File

@ -11,7 +11,7 @@ import { Button, PopUpWarning } from 'strapi-helper-plugin';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Wrapper from './Wrapper';
const PLUGINS_WITH_CONFIG = ['email', 'upload'];
const PLUGINS_WITH_CONFIG = ['email'];
/* eslint-disable react/no-unused-state */
class PluginCard extends React.Component {
@ -87,17 +87,13 @@ class PluginCard extends React.Component {
};
render() {
const buttonClass = !this.props.isAlreadyInstalled
? 'primary'
: 'secondary';
const buttonClass = !this.props.isAlreadyInstalled ? 'primary' : 'secondary';
const buttonLabel = this.props.isAlreadyInstalled
? 'app.components.PluginCard.Button.label.install'
: 'app.components.PluginCard.Button.label.download';
// Display settings link for a selection of plugins.
const settingsComponent = PLUGINS_WITH_CONFIG.includes(
this.props.plugin.id
) && (
const settingsComponent = PLUGINS_WITH_CONFIG.includes(this.props.plugin.id) && (
<div className="settings" onClick={this.handleClickSettings}>
<FontAwesomeIcon icon="cog" />
<FormattedMessage id="app.components.PluginCard.settings" />
@ -114,14 +110,10 @@ class PluginCard extends React.Component {
long:
this.props.plugin.id === 'support-us' ? (
<FormattedMessage
id={
this.props.plugin.description.long ||
this.props.plugin.description.short
}
id={this.props.plugin.description.long || this.props.plugin.description.short}
/>
) : (
this.props.plugin.description.long ||
this.props.plugin.description.short
this.props.plugin.description.long || this.props.plugin.description.short
),
};
@ -170,11 +162,7 @@ class PluginCard extends React.Component {
settingsComponent
) : (
<div className="compatible">
<i
className={`fa fa-${
this.props.plugin.isCompatible ? 'check' : 'times'
}`}
/>
<i className={`fa fa-${this.props.plugin.isCompatible ? 'check' : 'times'}`} />
<FormattedMessage
id={`app.components.PluginCard.compatible${
this.props.plugin.id === 'support-us' ? 'Community' : ''
@ -187,12 +175,9 @@ class PluginCard extends React.Component {
<PopUpWarning
content={{
message:
'app.components.PluginCard.PopUpWarning.install.impossible.autoReload.needed',
title:
'app.components.PluginCard.PopUpWarning.install.impossible.title',
confirm:
'app.components.PluginCard.PopUpWarning.install.impossible.confirm',
message: 'app.components.PluginCard.PopUpWarning.install.impossible.autoReload.needed',
title: 'app.components.PluginCard.PopUpWarning.install.impossible.title',
confirm: 'app.components.PluginCard.PopUpWarning.install.impossible.confirm',
}}
isOpen={this.state.showModalAutoReload}
onlyConfirmButton
@ -201,12 +186,9 @@ class PluginCard extends React.Component {
/>
<PopUpWarning
content={{
message:
'app.components.PluginCard.PopUpWarning.install.impossible.environment',
title:
'app.components.PluginCard.PopUpWarning.install.impossible.title',
confirm:
'app.components.PluginCard.PopUpWarning.install.impossible.confirm',
message: 'app.components.PluginCard.PopUpWarning.install.impossible.environment',
title: 'app.components.PluginCard.PopUpWarning.install.impossible.title',
confirm: 'app.components.PluginCard.PopUpWarning.install.impossible.confirm',
}}
isOpen={this.state.showModalEnv}
onlyConfirmButton

View File

@ -1,4 +1,12 @@
// DO NOT MODIFY THESE OPTIONS
// NOTE TO PLUGINS DEVELOPERS:
// If you modify this file you also need to update the documentation accordingly
// Here's the file: strapi/docs/3.0.0-beta.x/admin-panel/customization.md#tutorial-videos
// IF THE DOC IS NOT UPDATED THE PULL REQUEST WILL NOT BE MERGED
// IMPORTANT: It also needs to be added to the migration guide.
export const LOGIN_LOGO = null;
export const SHOW_TUTORIALS = true;
export const SETTINGS_BASE_URL = '/settings';

View File

@ -4,10 +4,7 @@
*
*/
import {
GET_PLUGINS_FROM_MARKETPLACE_SUCCEEDED,
SET_APP_ERROR,
} from './constants';
import { GET_PLUGINS_FROM_MARKETPLACE_SUCCEEDED, SET_APP_ERROR } from './constants';
export function getPluginsFromMarketPlaceSucceeded(plugins) {
return {

View File

@ -117,9 +117,12 @@ export class Admin extends React.Component {
return Object.keys(plugins).reduce((acc, current) => {
const InitializerComponent = plugins[current].initializer;
const key = plugins[current].id;
acc.push(<InitializerComponent key={key} {...this.props} {...this.helpers} />);
if (InitializerComponent) {
const key = plugins[current].id;
acc.push(<InitializerComponent key={key} {...this.props} {...this.helpers} />);
}
return acc;
}, []);

View File

@ -42,7 +42,9 @@ function App(props) {
try {
const requestURL = '/users-permissions/init';
const { hasAdmin } = await request(requestURL, { method: 'GET' });
const { hasAdmin } = await request(requestURL, { method: 'GET' }, false, false, {
noAuth: true,
});
const { data } = await request('/admin/init', { method: 'GET' });
const { uuid } = data;
@ -86,9 +88,7 @@ function App(props) {
<Switch>
<Route
path="/auth/:authType"
render={routerProps => (
<AuthPage {...routerProps} hasAdminUser={state.hasAdmin} />
)}
render={routerProps => <AuthPage {...routerProps} hasAdminUser={state.hasAdmin} />}
exact
/>
<PrivateRoute path="/" component={Admin} />
@ -108,10 +108,7 @@ export function mapDispatchToProps(dispatch) {
return bindActionCreators({ getDataSucceeded }, dispatch);
}
const withConnect = connect(
null,
mapDispatchToProps
);
const withConnect = connect(null, mapDispatchToProps);
export default compose(withConnect)(App);
export { App };

View File

@ -86,8 +86,7 @@ const Wrapper = styled.div`
}
.bordered {
border-top: 2px solid
${({ withSucessBorder }) => (withSucessBorder ? '#5a9e06' : '#1c5de7')};
border-top: 2px solid ${({ withSuccessBorder }) => (withSuccessBorder ? '#5a9e06' : '#1c5de7')};
}
.borderedSuccess {

View File

@ -3,13 +3,7 @@ import PropTypes from 'prop-types';
import { get, isEmpty, omit, set, upperFirst } from 'lodash';
import { FormattedMessage } from 'react-intl';
import { Link, Redirect } from 'react-router-dom';
import {
auth,
Button,
getQueryParameters,
getYupInnerErrors,
request,
} from 'strapi-helper-plugin';
import { auth, Button, getQueryParameters, getYupInnerErrors, request } from 'strapi-helper-plugin';
import NavTopRightWrapper from '../../components/NavTopRightWrapper';
import LogoStrapi from '../../assets/images/logo_strapi.png';
import PageTitle from '../../components/PageTitle';
@ -29,9 +23,9 @@ const AuthPage = ({
}) => {
const [reducerState, dispatch] = useReducer(reducer, initialState);
const codeRef = useRef();
const aborController = new AbortController();
const abortController = new AbortController();
const { signal } = aborController;
const { signal } = abortController;
codeRef.current = getQueryParameters(search, 'code');
useEffect(() => {
// Set the reset code provided by the url
@ -49,17 +43,11 @@ const AuthPage = ({
}
return () => {
aborController.abort();
abortController.abort();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [authType, codeRef]);
const {
didCheckErrors,
errors,
modifiedData,
submitSuccess,
userEmail,
} = reducerState.toJS();
const { didCheckErrors, errors, modifiedData, submitSuccess, userEmail } = reducerState.toJS();
const handleChange = ({ target: { name, value } }) => {
dispatch({
type: 'ON_CHANGE',
@ -124,9 +112,7 @@ const AuthPage = ({
} else if (authType === 'forgot-password') {
formErrors = { email: formattedError[0] };
} else {
strapi.notification.error(
get(formattedError, '0.id', 'notification.error')
);
strapi.notification.error(get(formattedError, '0.id', 'notification.error'));
}
}
} catch (err) {
@ -164,7 +150,7 @@ const AuthPage = ({
return (
<>
<PageTitle title={upperFirst(authType)} />
<Wrapper authType={authType} withSucessBorder={submitSuccess}>
<Wrapper authType={authType} withSuccessBorder={submitSuccess}>
<NavTopRightWrapper>
<LocaleToggle isLogged className="localeDropdownMenuNotLogged" />
</NavTopRightWrapper>
@ -177,9 +163,7 @@ const AuthPage = ({
)}
</div>
<div className="headerDescription">
{authType === 'register' && (
<FormattedMessage id="Auth.header.register.description" />
)}
{authType === 'register' && <FormattedMessage id="Auth.header.register.description" />}
</div>
{/* TODO Forgot success style */}
<div className="formContainer bordered">
@ -217,9 +201,7 @@ const AuthPage = ({
})}
<div
className={`${
authType === 'login'
? 'col-6 loginButton'
: 'col-12 buttonContainer'
authType === 'login' ? 'col-6 loginButton' : 'col-12 buttonContainer'
}`}
>
<Button
@ -238,15 +220,9 @@ const AuthPage = ({
</div>
<div className="linkContainer">
{authType !== 'register' && authType !== 'reset-password' && (
<Link
to={`/auth/${
authType === 'login' ? 'forgot-password' : 'login'
}`}
>
<Link to={`/auth/${authType === 'login' ? 'forgot-password' : 'login'}`}>
<FormattedMessage
id={`Auth.link.${
authType === 'login' ? 'forgot-password' : 'ready'
}`}
id={`Auth.link.${authType === 'login' ? 'forgot-password' : 'ready'}`}
/>
</Link>
)}

View File

@ -9,7 +9,7 @@ const Block = styled.div`
padding: 19px 30px 30px 30px;
box-shadow: 0 2px 4px 0 #e3e9f3;
border-radius: 3px;
line-heigth: 18px;
line-height: 18px;
a {
position: relative;
@ -213,12 +213,12 @@ const LinkWrapper = styled.a`
&:first-child {
font-size: 16px;
}
color: #919BAE;
color: #919bae;
text-overflow: ellipsis;
overflow: hidden;
}
.bold {
color: #333740
color: #333740;
font-weight: 600;
}
`;
@ -257,13 +257,4 @@ const SocialLinkWrapper = styled.div`
}
`;
export {
ALink,
Block,
Container,
LinkWrapper,
P,
Separator,
SocialLinkWrapper,
Wave,
};
export { ALink, Block, Container, LinkWrapper, P, Separator, SocialLinkWrapper, Wave };

View File

@ -9,8 +9,7 @@ import Text from '../../components/Text';
import CustomRow from './CustomRow';
import LogoContainer from './Logo';
// TODO: remove the upload plugin when the media lib feature is merged.
const PLUGINS_WITH_CONFIG = ['email', 'upload'];
const PLUGINS_WITH_CONFIG = ['email'];
const Row = ({ logo, name, description, isRequired, id, icon, onConfirm }) => {
const { currentEnvironment, formatMessage } = useGlobalContext();

View File

@ -1,19 +1,17 @@
import { sortBy } from 'lodash';
const generateRows = (obj, onConfirm) => {
const rows = Object.values(obj).map(
({ name, pluginLogo, id, description, isRequired, icon }) => {
return {
name,
logo: pluginLogo,
id,
description,
isRequired,
icon,
onConfirm,
};
}
);
const rows = Object.values(obj).map(({ name, pluginLogo, id, description, isRequired, icon }) => {
return {
name,
logo: pluginLogo,
id,
description,
isRequired,
icon,
onConfirm,
};
});
return sortBy(rows, [obj => obj.name.toLowerCase()]);
};

View File

@ -5,7 +5,7 @@
*
* NOTE: while this component should technically be a stateless functional
* component (SFC), hot reloading does not currently support SFCs. If hot
* reloading is not a neccessity for you then you can refactor it and remove
* reloading is not a necessity for you then you can refactor it and remove
* the linting exception.
*/

View File

@ -4,31 +4,37 @@
*
*/
// NOTE TO PLUGINS DEVELOPERS:
// If you modify this file you also need to update the documentation accordingly
// Here's the file: strapi/docs/3.0.0-beta.x/plugin-development/frontend-settings-api.md
// IF THE DOC IS NOT UPDATED THE PULL REQUEST WILL NOT BE MERGED
import React, { memo } from 'react';
import { useGlobalContext, LeftMenu, LeftMenuList } from 'strapi-helper-plugin';
import { get } from 'lodash';
import { Switch, Redirect, Route, useParams } from 'react-router-dom';
import EditView from '../Webhooks/EditView';
import ListView from '../Webhooks/ListView';
import SettingDispatcher from './SettingDispatcher';
import Wrapper from './Wrapper';
import retrieveGlobalLinks from './utils/retrieveGlobalLinks';
import retrievePluginsMenu from './utils/retrievePluginsMenu';
function SettingsPage() {
const { settingId } = useParams();
const { formatMessage, plugins, settingsBaseURL } = useGlobalContext();
// Retrieve the links that will be injected into the global section
const globalLinks = retrieveGlobalLinks(plugins);
// Create the plugins settings section
// Note it is currently not possible to add a link into a plugin section
const pluginsMenu = retrievePluginsMenu(plugins);
const pluginsMenu = Object.keys(plugins).reduce((acc, current) => {
const pluginMenu = get(plugins, [current, 'settings', 'menuSection'], null);
if (!pluginMenu) {
return acc;
}
acc.push(pluginMenu);
return acc;
}, []);
const createdRoutes = globalLinks
.map(({ to, Component, exact }) => (
<Route path={to} key={to} component={Component} exact={exact || false} />
))
.filter((route, index, refArray) => {
return refArray.findIndex(obj => obj.key === route.key) === index;
});
const menuItems = [
{
@ -40,6 +46,7 @@ function SettingsPage() {
to: `${settingsBaseURL}/webhooks`,
name: 'webhooks',
},
...globalLinks,
],
},
...pluginsMenu,
@ -64,20 +71,10 @@ function SettingsPage() {
</div>
<div className="col-md-9">
<Switch>
<Route
exact
path={`${settingsBaseURL}/webhooks`}
component={ListView}
/>
<Route
exact
path={`${settingsBaseURL}/webhooks/:id`}
component={EditView}
/>
<Route
path={`${settingsBaseURL}/:pluginId`}
component={SettingDispatcher}
/>
<Route exact path={`${settingsBaseURL}/webhooks`} component={ListView} />
<Route exact path={`${settingsBaseURL}/webhooks/:id`} component={EditView} />
{createdRoutes}
<Route path={`${settingsBaseURL}/:pluginId`} component={SettingDispatcher} />
</Switch>
</div>
</div>

View File

@ -0,0 +1,17 @@
import { get } from 'lodash';
const retrieveGlobalLinks = pluginsObj => {
return Object.values(pluginsObj).reduce((acc, current) => {
const links = get(current, ['settings', 'global'], null);
if (links) {
for (let i = 0; i < links.length; i++) {
acc.push(links[i]);
}
}
return acc;
}, []);
};
export default retrieveGlobalLinks;

View File

@ -0,0 +1,17 @@
import { get } from 'lodash';
const retrievePluginsMenu = pluginsObj => {
return Object.values(pluginsObj).reduce((acc, current) => {
const pluginMenu = get(current, ['settings', 'menuSection'], null);
if (!pluginMenu) {
return acc;
}
acc.push(pluginMenu);
return acc;
}, []);
};
export default retrievePluginsMenu;

View File

@ -0,0 +1,33 @@
import retrieveGlobalLinks from '../retrieveGlobalLinks';
describe('ADMIN | containers | SettingsPage | utils', () => {
describe('retrieveGlobalLinks', () => {
it('should return an empty array if there is no plugins', () => {
expect(retrieveGlobalLinks({})).toHaveLength(0);
});
it('should return an array of links', () => {
const plugins = {
test: {
settings: {
global: [],
},
},
noSettings: {},
foo: {
settings: {
global: ['test'],
},
},
bar: {
settings: {
global: ['test2'],
},
},
};
const expected = ['test', 'test2'];
expect(retrieveGlobalLinks(plugins)).toEqual(expected);
});
});
});

View File

@ -0,0 +1,33 @@
import retrievePluginsMenu from '../retrievePluginsMenu';
describe('ADMIN | containers | SettingsPage | utils', () => {
describe('retrievePluginsMenu', () => {
it('should return an empty array if there is no plugins', () => {
expect(retrievePluginsMenu({})).toHaveLength(0);
});
it('should return an array of menu sections', () => {
const plugins = {
test: {
settings: {
menuSection: null,
},
},
noSettings: {},
foo: {
settings: {
menuSection: { label: 'test' },
},
},
bar: {
settings: {
menuSection: { label: 'test2' },
},
},
};
const expected = [{ label: 'test' }, { label: 'test2' }];
expect(retrievePluginsMenu(plugins)).toEqual(expected);
});
});
});

View File

@ -107,6 +107,9 @@ exports[`Admin | containers | EditView should match the snapshot 1`] = `
background-color: transparent;
border: 1px solid #9ea7b8;
color: #9ea7b8;
background-color: #E9EAEB;
border: 1px solid #E9EAEB;
color: #B4B6BA;
}
.c7:hover {
@ -148,6 +151,9 @@ exports[`Admin | containers | EditView should match the snapshot 1`] = `
background-color: #6DBB1A;
border: 1px solid #6DBB1A;
color: #ffffff;
background-color: #E9EAEB;
border: 1px solid #E9EAEB;
color: #B4B6BA;
min-width: 140px;
}
@ -177,12 +183,6 @@ exports[`Admin | containers | EditView should match the snapshot 1`] = `
cursor: initial;
}
.c8:disabled {
background-color: #E9EAEB;
border: 1px solid #B4B6BA;
color: #B4B6BA;
}
.c18 {
cursor: pointer;
margin: 0;
@ -1258,7 +1258,6 @@ exports[`Admin | containers | EditView should match the snapshot 1`] = `
<input
autocomplete="off"
class="c18"
disabled=""
id="media.update"
name="media.update"
tabindex="0"

View File

@ -5,6 +5,7 @@ exports[`Admin | containers | ListView should match the snapshot 1`] = `
.c6 button {
width: 100%;
height: 54px;
border: 0;
border-top: 1px solid #aed4fb;
color: #007eff;
font-weight: 500;

View File

@ -16,13 +16,10 @@ const useFetchPluginsFromMarketPlace = () => {
const getData = async () => {
try {
const { data } = await axios.get(
'https://marketplace.strapi.io/plugins',
{
cancelToken: source.token,
params: { lang: currentLocale },
}
);
const { data } = await axios.get('https://marketplace.strapi.io/plugins', {
cancelToken: source.token,
params: { lang: currentLocale },
});
setState({
isLoading: false,

View File

@ -1,5 +1,6 @@
import { renderHook } from '@testing-library/react-hooks';
import axios from 'axios';
// eslint-disable-next-line import/no-unresolved
import MockAdapter from 'axios-mock-adapter';
import useFetch from '../index';

View File

@ -5,6 +5,11 @@
*
*/
// NOTE TO PLUGINS DEVELOPERS:
// If you modify this file you also need to update the documentation accordingly
// Here's the file: strapi/docs/3.0.0-beta.x/admin-panel/customization.md#customize-the-strapi-admin-package
// IF THE DOC IS NOT UPDATED THE PULL REQUEST WILL NOT BE MERGED
import { addLocaleData } from 'react-intl';
import { reduce } from 'lodash';

View File

@ -29,12 +29,8 @@ module.exports = {
.default,
'strapi-plugin-content-type-builder': require('../../../strapi-plugin-content-type-builder/admin/src')
.default,
'strapi-plugin-documentation': require('../../../strapi-plugin-documentation/admin/src')
.default,
'strapi-plugin-email': require('../../../strapi-plugin-email/admin/src')
.default,
'strapi-plugin-upload': require('../../../strapi-plugin-upload/admin/src')
.default,
'strapi-plugin-graphql': require('../../../strapi-plugin-graphql/admin/src')
.default,
'strapi-plugin-documentation': require('../../../strapi-plugin-documentation/admin/src').default,
'strapi-plugin-email': require('../../../strapi-plugin-email/admin/src').default,
'strapi-plugin-upload': require('../../../strapi-plugin-upload/admin/src').default,
'strapi-plugin-graphql': require('../../../strapi-plugin-graphql/admin/src').default,
};

View File

@ -1,8 +1,9 @@
const colors = {
black: '#000000',
black: '#333740',
white: '#ffffff',
red: '#ff203c',
orange: '#ff5d00',
lightOrange: '#f64d0a',
yellow: '#ffd500',
green: '#27b70f',
blue: '#0097f7',
@ -15,7 +16,17 @@ const colors = {
grayLight: '#636c72',
'gray-lighter': '#eceeef',
'gray-lightest': '#f7f7f9',
brightGrey: '#f0f3f8',
darkGrey: '#e3e9f3',
lightGrey: '#fafafa',
lightestGrey: '#fbfbfb',
mediumGrey: '#F2F3F4',
grey: '#9ea7b8',
greyDark: '#292b2c',
greyAlpha: 'rgba(227, 233, 243, 0.5)',
lightBlue: '#E6F0FB',
mediumBlue: '#007EFF',
darkBlue: '#AED4FB',
content: {
background: '#fafafb',
'background-alpha': 'rgba(14, 22, 34, 0.02)',

View File

@ -0,0 +1,8 @@
const fontWeights = {
regular: 400,
semiBold: 500,
bold: 600,
black: 900,
};
export default fontWeights;

View File

@ -1,9 +1,11 @@
import colors from './colors';
import fontWeights from './fontWeights';
import sizes from './sizes';
const theme = {
main: {
colors,
fontWeights,
sizes,
},
};

View File

@ -1,4 +1,5 @@
const sizes = {
borderRadius: '2px',
header: {
height: '6rem',
},
@ -6,6 +7,23 @@ const sizes = {
height: '6rem',
width: '24rem',
},
margins: {
// TODO:
sm: '10px',
},
paddings: {
// TODO
xs: '5px',
sm: '10px',
md: '30px',
lg: '40px',
},
fonts: {
xs: '11px',
sm: '12px',
md: '13px',
lg: '18px',
},
};
export default sizes;

View File

@ -95,6 +95,14 @@
"app.utils.placeholder.defaultMessage": " ",
"components.AutoReloadBlocker.header": "مطلوب ميزة إعادة التحميل لهذه الإضافة.",
"components.ErrorBoundary.title": "هناك خطأ ما...",
"components.FilterOptions.FILTER_TYPES.=": "هو",
"components.FilterOptions.FILTER_TYPES._contains": "يحتوي",
"components.FilterOptions.FILTER_TYPES._containss": "يحتوي (حساس لحالة الأحرف)",
"components.FilterOptions.FILTER_TYPES._gt": "اكبر من",
"components.FilterOptions.FILTER_TYPES._gte": "اكبر من او يساوي",
"components.FilterOptions.FILTER_TYPES._lt": "اقل من",
"components.FilterOptions.FILTER_TYPES._lte": "اقل من او يساوي",
"components.FilterOptions.FILTER_TYPES._ne": "ليس",
"components.Input.error.attribute.key.taken": "هذه القيمة موجودة مسبقًا",
"components.Input.error.attribute.sameKeyAndName": "لا تتطابق",
"components.Input.error.attribute.taken": "اسم الحقل هذا مستخدم مسبقًا",

View File

@ -112,6 +112,16 @@
"components.AutoReloadBlocker.description": "Spusťte Strapi jedním z následujících příkazů:",
"components.AutoReloadBlocker.header": "Pro tento zásuvný modul musí být zapnuta funkce znovu načítání.",
"components.ErrorBoundary.title": "Něco se pokazilo...",
"components.FilterOptions.FILTER_TYPES.=": "je",
"components.FilterOptions.FILTER_TYPES._contains": "obsahuje",
"components.FilterOptions.FILTER_TYPES._containss": "obsahuje (citlivé na velká písmena)",
"components.FilterOptions.FILTER_TYPES._gt": "je větší než",
"components.FilterOptions.FILTER_TYPES._gte": "je větší nebo rovno než",
"components.FilterOptions.FILTER_TYPES._lt": "je menší než",
"components.FilterOptions.FILTER_TYPES._lte": "je menší nebo rovno než",
"components.FilterOptions.FILTER_TYPES._ne": "není",
"components.FilterOptions.FILTER_TYPES._in": "odpovídá jakékoliv hodnotě v poli hodnot",
"components.FilterOptions.FILTER_TYPES._nin": "neodpovídá ani jedné hodnotě v poli hodnot",
"components.Input.error.attribute.key.taken": "Tato hodnota již existuje",
"components.Input.error.attribute.sameKeyAndName": "Hodnoty nesmí být stejné",
"components.Input.error.attribute.taken": "Název tohoto pole již existuje",

View File

@ -109,6 +109,14 @@
"components.AutoReloadBlocker.description": "Führen Sie Strapi mit einem der folgenden Befehle aus:",
"components.AutoReloadBlocker.header": "Dieses Plugin benötigt das Neuladen-Feature.",
"components.ErrorBoundary.title": "Etwas ist falsch gelaufen...",
"components.FilterOptions.FILTER_TYPES.=": "ist",
"components.FilterOptions.FILTER_TYPES._contains": "enthält",
"components.FilterOptions.FILTER_TYPES._containss": "enthält (Groß-/Kleinschreibung beachten)",
"components.FilterOptions.FILTER_TYPES._gt": "ist größer als",
"components.FilterOptions.FILTER_TYPES._gte": "ist größer oder gleich als",
"components.FilterOptions.FILTER_TYPES._lt": "ist kleiner als",
"components.FilterOptions.FILTER_TYPES._lte": "ist kleiner oder gleich als",
"components.FilterOptions.FILTER_TYPES._ne": "ist nicht",
"components.Input.error.attribute.key.taken": "Dieser Wert existiert bereits",
"components.Input.error.attribute.sameKeyAndName": "Darf nicht gleich sein",
"components.Input.error.attribute.taken": "Dieser Feldname ist bereits vergeben",

View File

@ -118,6 +118,17 @@
"components.AutoReloadBlocker.description": "Run Strapi with one of the following commands:",
"components.AutoReloadBlocker.header": "Reload feature is required for this plugin.",
"components.ErrorBoundary.title": "Something went wrong...",
"components.FilterOptions.FILTER_TYPES.=": "is",
"components.FilterOptions.FILTER_TYPES._contains": "contains",
"components.FilterOptions.FILTER_TYPES._containss": "contains (case sensitive)",
"components.FilterOptions.FILTER_TYPES._gt": "is greater than",
"components.FilterOptions.FILTER_TYPES._gte": "is greater than or equal to",
"components.FilterOptions.FILTER_TYPES._lt": "is lower than",
"components.FilterOptions.FILTER_TYPES._lte": "is lower than or equal to",
"components.FilterOptions.FILTER_TYPES._ne": "is not",
"components.FilterOptions.FILTER_TYPES._ncontains": "does not contain",
"components.FilterOptions.FILTER_TYPES._in": "matches any value in the array of values",
"components.FilterOptions.FILTER_TYPES._nin": "doesn't match any value in the array of values",
"components.Input.error.attribute.key.taken": "This value already exists",
"components.Input.error.attribute.sameKeyAndName": "Can't be equal",
"components.Input.error.attribute.taken": "This field name already exists",
@ -166,6 +177,7 @@
"notification.error.layout": "Couldn't retrieve the layout",
"request.error.model.unknown": "This model doesn't exist",
"app.utils.delete": "Delete",
"app.utils.filters": "Filters",
"HomePage.helmet.title": "Homepage",
"HomePage.welcome.congrats": "Congrats!",
"HomePage.welcome.congrats.content": "You are logged as the first administrator. To discover the powerful features provided by Strapi,",
@ -261,5 +273,6 @@
"notification.contentType.relations.conflict": "Content type has conflicting relations",
"notification.form.error.fields": "The form contains some errors",
"notification.form.success.fields": "Changes saved",
"notification.success.delete": "The item has been deleted",
"global.prompt.unsaved": "Are you sure you want to leave this page? All your modifications will be lost"
}

View File

@ -108,6 +108,16 @@
"components.AutoReloadBlocker.description": "Inicia Strapi con uno de los siguientes comandos:",
"components.AutoReloadBlocker.header": "Es necesario recargar para este plugin.",
"components.ErrorBoundary.title": "Algo salió mal...",
"components.FilterOptions.FILTER_TYPES.=": "es",
"components.FilterOptions.FILTER_TYPES._contains": "contiene",
"components.FilterOptions.FILTER_TYPES._containss": "contiene (distinguiendo mayúsculas y minúsculas)",
"components.FilterOptions.FILTER_TYPES._gt": "es mayor que",
"components.FilterOptions.FILTER_TYPES._gte": "es mayor o igual que",
"components.FilterOptions.FILTER_TYPES._lt": "es menor que",
"components.FilterOptions.FILTER_TYPES._lte": "es menor o igual que",
"components.FilterOptions.FILTER_TYPES._ne": "no es",
"components.FilterOptions.FILTER_TYPES._in": "coincide con cualquier valor de la lista de registros",
"components.FilterOptions.FILTER_TYPES._nin": "no coincide con ningún valor de la lista de registros",
"components.Input.error.attribute.key.taken": "Este valor ya existe",
"components.Input.error.attribute.sameKeyAndName": "No puede ser igual",
"components.Input.error.attribute.taken": "Este nombre de campo ya existe",

View File

@ -116,6 +116,15 @@
"components.AutoReloadBlocker.description": "Démarrez Strapi avec l'une des commandes suivantes:",
"components.AutoReloadBlocker.header": "L'autoReload doit être activé pour ce plugin.",
"components.ErrorBoundary.title": "Une erreur est survenue...",
"components.FilterOptions.FILTER_TYPES.=": "est",
"components.FilterOptions.FILTER_TYPES._contains": "contient",
"components.FilterOptions.FILTER_TYPES._containss": "contient (sensible à la casse)",
"components.FilterOptions.FILTER_TYPES._gt": "supérieur à",
"components.FilterOptions.FILTER_TYPES._gte": "supérieur ou égal à",
"components.FilterOptions.FILTER_TYPES._lt": "inférieur à",
"components.FilterOptions.FILTER_TYPES._lte": "inférieur ou égal à",
"components.FilterOptions.FILTER_TYPES._ne": "n'est pas",
"components.FilterOptions.FILTER_TYPES._ncontains": "ne contient pas",
"components.Input.error.attribute.key.taken": "Cette valeur existe déjà",
"components.Input.error.attribute.sameKeyAndName": "Ne peuvent pas être égaux",
"components.Input.error.attribute.taken": "Ce champ existe déjà",

View File

@ -109,6 +109,14 @@
"components.AutoReloadBlocker.description": "Avvia Strapi con uno dei seguenti comandi:",
"components.AutoReloadBlocker.header": "Ricarica funzionalità è richiesto per questo plugin.",
"components.ErrorBoundary.title": "Qualcosa è andato storto...",
"components.FilterOptions.FILTER_TYPES.=": "si",
"components.FilterOptions.FILTER_TYPES._contains": "contiene",
"components.FilterOptions.FILTER_TYPES._containss": "contiene (maiuscole e minuscole)",
"components.FilterOptions.FILTER_TYPES._gt": "è maggiore di",
"components.FilterOptions.FILTER_TYPES._gte": "è maggiore o uguale a",
"components.FilterOptions.FILTER_TYPES._lt": "è inferiore",
"components.FilterOptions.FILTER_TYPES._lte": "è inferiore o uguale a",
"components.FilterOptions.FILTER_TYPES._ne": "non è",
"components.Input.error.attribute.key.taken": "Valore già esistente",
"components.Input.error.attribute.sameKeyAndName": "Non può essere uguale",
"components.Input.error.attribute.taken": "Nome campo già esistente",

View File

@ -96,6 +96,14 @@
"app.utils.SelectOption.defaultMessage": " ",
"components.AutoReloadBlocker.header": "プラグインを有効化するにはリロードが必要です",
"components.ErrorBoundary.title": "なにかが間違っています...",
"components.FilterOptions.FILTER_TYPES.=": "は",
"components.FilterOptions.FILTER_TYPES._contains": "含まれる",
"components.FilterOptions.FILTER_TYPES._containss": "含まれる(大文字と小文字を区別)",
"components.FilterOptions.FILTER_TYPES._gt": "より大きい",
"components.FilterOptions.FILTER_TYPES._gte": "より大きい、もしくは、等しい",
"components.FilterOptions.FILTER_TYPES._lt": "より低い",
"components.FilterOptions.FILTER_TYPES._lte": "より低い、もしくは、等しい",
"components.FilterOptions.FILTER_TYPES._ne": "ではない",
"components.Input.error.attribute.key.taken": "この値はすでに存在しています",
"components.Input.error.attribute.sameKeyAndName": "等しくありません",
"components.Input.error.attribute.taken": "このフィールド名はすでに存在します",

View File

@ -109,6 +109,16 @@
"components.AutoReloadBlocker.description": "다음 명령어 중 하나를 사용하여 Strapi를 실행합니다:",
"components.AutoReloadBlocker.header": "이 플러그인은 리로드 기능이 필요합니다.",
"components.ErrorBoundary.title": "에러가 발생했습니다.",
"components.FilterOptions.FILTER_TYPES.=": "같음",
"components.FilterOptions.FILTER_TYPES._contains": "포함",
"components.FilterOptions.FILTER_TYPES._containss": "포함(대소문자 구분)",
"components.FilterOptions.FILTER_TYPES._gt": "큼",
"components.FilterOptions.FILTER_TYPES._gte": "크거나 같음",
"components.FilterOptions.FILTER_TYPES._lt": "작음",
"components.FilterOptions.FILTER_TYPES._lte": "작거나 같음",
"components.FilterOptions.FILTER_TYPES._ne": "같지 않음",
"components.FilterOptions.FILTER_TYPES._in": "배열중에 일치하는 값이 있음",
"components.FilterOptions.FILTER_TYPES._nin": "배열중에 일치하는 값이 없음",
"components.Input.error.attribute.key.taken": "이미 사용중인 키입니다.",
"components.Input.error.attribute.sameKeyAndName": "같은 값을 사용할 수 없습니다.",
"components.Input.error.attribute.taken": "이미 사용중인 이름입니다.",
@ -213,4 +223,4 @@
"components.Input.error.password.noMatch": "패스워드가 일치하지 않습니다.",
"form.button.done": "확인",
"notification.form.error.fields": "잘못 입력된 필드가 존재합니다."
}
}

View File

@ -206,6 +206,14 @@
"components.AutoReloadBlocker.description": "Start Strapi met een van de volgende commands:",
"components.AutoReloadBlocker.header": "De herlaad feature is nodig voor deze extensie",
"components.ErrorBoundary.title": "Er is iets fout gegaan...",
"components.FilterOptions.FILTER_TYPES.=": "is",
"components.FilterOptions.FILTER_TYPES._contains": "bevat",
"components.FilterOptions.FILTER_TYPES._containss": "bevat (hoofdletter gevoelig)",
"components.FilterOptions.FILTER_TYPES._gt": "is groter dan",
"components.FilterOptions.FILTER_TYPES._gte": "is groter dan of gelijk aan",
"components.FilterOptions.FILTER_TYPES._lt": "is lager dan",
"components.FilterOptions.FILTER_TYPES._lte": "is lager dan of gelijk aan",
"components.FilterOptions.FILTER_TYPES._ne": "is niet",
"components.Input.error.attribute.key.taken": "Deze waarde bestaat al.",
"components.Input.error.attribute.sameKeyAndName": "Mag niet gelijk zijn.",
"components.Input.error.attribute.taken": "Deze veld naam bestaat al",

View File

@ -114,6 +114,16 @@
"components.AutoReloadBlocker.description": "Uruchom Strapi z jedną z poniższych komend:",
"components.AutoReloadBlocker.header": "Do tej wtyczki wymagana jest funkcja przeładowania.",
"components.ErrorBoundary.title": "Coś poszło nie tak...",
"components.FilterOptions.FILTER_TYPES.=": "jest identyczne z",
"components.FilterOptions.FILTER_TYPES._contains": "zawiera",
"components.FilterOptions.FILTER_TYPES._containss": "zawiera (rozróżnianie wielkości liter)",
"components.FilterOptions.FILTER_TYPES._gt": "jest większe od",
"components.FilterOptions.FILTER_TYPES._gte": "jest większe od lub równe",
"components.FilterOptions.FILTER_TYPES._in": "dopasuj jakąkolwiek wartość z tablicy wartości",
"components.FilterOptions.FILTER_TYPES._lt": "jest mniejsze od",
"components.FilterOptions.FILTER_TYPES._lte": "jest mniejsze od lub równe",
"components.FilterOptions.FILTER_TYPES._ne": "jest różne od",
"components.FilterOptions.FILTER_TYPES._nin": "nie znaleziono pasującej wartości w tablicy wartości",
"components.Input.error.attribute.key.taken": "Ta wartość już istnieje",
"components.Input.error.attribute.sameKeyAndName": "Nie mogą być takie same",
"components.Input.error.attribute.taken": "Ta nazwa pola już istnieje",

View File

@ -109,6 +109,14 @@
"components.AutoReloadBlocker.description": "Execute o Strapi com um dos seguintes comandos:",
"components.AutoReloadBlocker.header": "Auto recarregamento é necessário para esta extensão.",
"components.ErrorBoundary.title": "Algo deu errado...",
"components.FilterOptions.FILTER_TYPES.=": "é",
"components.FilterOptions.FILTER_TYPES._contains": "contém",
"components.FilterOptions.FILTER_TYPES._containss": "contém (case sensitive)",
"components.FilterOptions.FILTER_TYPES._gt": "é maior que",
"components.FilterOptions.FILTER_TYPES._gte": "é maior que ou igual à",
"components.FilterOptions.FILTER_TYPES._lt": "é menor que",
"components.FilterOptions.FILTER_TYPES._lte": "é menor que ou igual à",
"components.FilterOptions.FILTER_TYPES._ne": "não é",
"components.Input.error.attribute.key.taken": "Este valor já existe",
"components.Input.error.attribute.sameKeyAndName": "Não pode ser igual",
"components.Input.error.attribute.taken": "O nome deste campo já existe",

View File

@ -95,6 +95,14 @@
"app.utils.placeholder.defaultMessage": " ",
"components.AutoReloadBlocker.header": "Recurso de recarga é necessário para esta extensão.",
"components.ErrorBoundary.title": "Algo deu errado...",
"components.FilterOptions.FILTER_TYPES.=": "é",
"components.FilterOptions.FILTER_TYPES._contains": "contém",
"components.FilterOptions.FILTER_TYPES._containss": "contém (case sensitive)",
"components.FilterOptions.FILTER_TYPES._gt": "é maior que",
"components.FilterOptions.FILTER_TYPES._gte": "é maior que ou igual à",
"components.FilterOptions.FILTER_TYPES._lt": "é menor que",
"components.FilterOptions.FILTER_TYPES._lte": "é menor que ou igual à",
"components.FilterOptions.FILTER_TYPES._ne": "não é",
"components.Input.error.attribute.key.taken": "Este valor já existe",
"components.Input.error.attribute.sameKeyAndName": "Não pode ser igual",
"components.Input.error.attribute.taken": "O nome deste campo já existe",

View File

@ -113,6 +113,14 @@
"components.AutoReloadBlocker.description": "Запустите Strapi с помощью одной из следующих команд:",
"components.AutoReloadBlocker.header": "Функционал перезапуска необходим для этого плагина.",
"components.ErrorBoundary.title": "Что-то пошло не так...",
"components.FilterOptions.FILTER_TYPES.=": "равно",
"components.FilterOptions.FILTER_TYPES._contains": "содержит",
"components.FilterOptions.FILTER_TYPES._containss": "содержит (с учетом регистра)",
"components.FilterOptions.FILTER_TYPES._gt": "больше чем",
"components.FilterOptions.FILTER_TYPES._gte": "больше или равно",
"components.FilterOptions.FILTER_TYPES._lt": "меньше чем",
"components.FilterOptions.FILTER_TYPES._lte": "меньше или равно",
"components.FilterOptions.FILTER_TYPES._ne": "не равно",
"components.Input.error.attribute.key.taken": "Это значение уже существует",
"components.Input.error.attribute.sameKeyAndName": "Не может быть одинаковым",
"components.Input.error.attribute.taken": "Поле с таким названием уже существует",

View File

@ -112,6 +112,16 @@
"components.AutoReloadBlocker.description": "Spustite Strapi s jedným z nasledujúcich príkazov:",
"components.AutoReloadBlocker.header": "Pre tento plugin je požadované opätovné načítanie stránky.",
"components.ErrorBoundary.title": "Niečo sa pokazilo...",
"components.FilterOptions.FILTER_TYPES.=": "je",
"components.FilterOptions.FILTER_TYPES._contains": "obsahuje",
"components.FilterOptions.FILTER_TYPES._containss": "obsahuje (záleží na veľkosti písmen)",
"components.FilterOptions.FILTER_TYPES._gt": "je väčšie ako",
"components.FilterOptions.FILTER_TYPES._gte": "je väčšie ako alebo rovné",
"components.FilterOptions.FILTER_TYPES._lt": "je menšie ako",
"components.FilterOptions.FILTER_TYPES._lte": "je menšie ako alebo rovné",
"components.FilterOptions.FILTER_TYPES._ne": "nie je",
"components.FilterOptions.FILTER_TYPES._in": "obsahuje hodnotu z poľa",
"components.FilterOptions.FILTER_TYPES._nin": "neobsahuje žiadnu hodnotu z poľa",
"components.Input.error.attribute.key.taken": "Táto hodnota už existuje",
"components.Input.error.attribute.sameKeyAndName": "Nemôže sa rovnať",
"components.Input.error.attribute.taken": "Názov tohto políčka už existuje",

View File

@ -26,13 +26,14 @@
"app.components.DownloadInfo.text": "Det kan ta en minut. Tack för ditt tålamod.",
"app.components.EmptyAttributes.title": "Det finns inga fält än ",
"app.components.HomePage.button.blog": "SE MER PÅ BLOGGEN",
  "app.components.HomePage.button.quickStart": "Snabb introduktion",
  "app.components.HomePage.community": "Hitta gemenskapen på webben",
  "app.components.HomePage.community.content": "Diskutera med teammedlemmar, bidragsgivare och utvecklare på olika kanaler.",
  "app.components.HomePage.createBlock.content.first": "De",
  "app.components.HomePage.createBlock.content.second": "plugin hjälper dig att definiera datastrukturen för dina modeller. Om du är ny här rekommenderar vi dig starkt att följa vår",
  "app.components.HomePage.createBlock.content.tutorial": "snabbguide.",
  "app.components.HomePage.cta": "BEKRÄFTA",
"app.components.HomePage.button.quickStart": "Snabb introduktion",
"app.components.HomePage.community": "Hitta gemenskapen på webben",
"app.components.HomePage.community.content": "Diskutera med teammedlemmar, bidragsgivare och utvecklare på olika kanaler.",
"app.components.HomePage.create": "Skapa din första innehållstyp",
"app.components.HomePage.createBlock.content.first": "De",
"app.components.HomePage.createBlock.content.second": "plugin hjälper dig att definiera datastrukturen för dina modeller. Om du är ny här rekommenderar vi dig starkt att följa vår",
"app.components.HomePage.createBlock.content.tutorial": "snabbguide.",
"app.components.HomePage.cta": "BEKRÄFTA",
"app.components.HomePage.newsLetter": "Prenumerera på nyhetsbrevet för att hålla dig uppdaterad om Strapi",
"app.components.HomePage.support": "STÖD OSS",
"app.components.HomePage.support.content": "Genom att köpa en T-shirt hjälper du oss att fortsätta vårt arbete med projektet för att ge dig bästa möjliga upplevelse! ",

View File

@ -111,6 +111,14 @@
"components.AutoReloadBlocker.description": "Strapi'yi aşağıdaki komutlardan biri ile çalıştırın:",
"components.AutoReloadBlocker.header": "Bu eklenti için tekrar yükleme özelliği gerekiyor.",
"components.ErrorBoundary.title": "Bir şeyler yanlış gitti...",
"components.FilterOptions.FILTER_TYPES.=": "eşit",
"components.FilterOptions.FILTER_TYPES._contains": "içermek",
"components.FilterOptions.FILTER_TYPES._containss": "içermek (büyük-küçük harfe duyarlı)",
"components.FilterOptions.FILTER_TYPES._gt": "daha yüksek",
"components.FilterOptions.FILTER_TYPES._gte": "daha yüksek ya da eşit",
"components.FilterOptions.FILTER_TYPES._lt": "daha düşük",
"components.FilterOptions.FILTER_TYPES._lte": "daha düşük ya da eşit",
"components.FilterOptions.FILTER_TYPES._ne": "eşit değil",
"components.Input.error.attribute.key.taken": "Bu değer zaten var.",
"components.Input.error.attribute.sameKeyAndName": "Eşit olamaz",
"components.Input.error.attribute.taken": "Bu alan ismi zaten var.",

View File

@ -109,6 +109,16 @@
"components.AutoReloadBlocker.description": "Chạy Strapi với một trong các lệnh sau:",
"components.AutoReloadBlocker.header": "Tính năng Tải lại bị bắt buộc cho plugin này.",
"components.ErrorBoundary.title": "Điều gì đó không ổn...",
"components.FilterOptions.FILTER_TYPES.=": "là",
"components.FilterOptions.FILTER_TYPES._contains": "chứa",
"components.FilterOptions.FILTER_TYPES._containss": "chứa (phân biệt chữ hoa thường)",
"components.FilterOptions.FILTER_TYPES._gt": "lớn hơn",
"components.FilterOptions.FILTER_TYPES._gte": "lớn hơn hoặc bằng",
"components.FilterOptions.FILTER_TYPES._lt": "nhỏ hơn",
"components.FilterOptions.FILTER_TYPES._lte": "nhỏ hơn hoặc bằng",
"components.FilterOptions.FILTER_TYPES._ne": "không bằng",
"components.FilterOptions.FILTER_TYPES._in": "khớp với bất cứ giá trị nào có trong mảng giá trị",
"components.FilterOptions.FILTER_TYPES._nin": "không khớp với bất cứ giá trị nào có trong mảng giá trị",
"components.Input.error.attribute.key.taken": "Giá trị này đã tồn tại",
"components.Input.error.attribute.sameKeyAndName": "Không thể bằng nhau",
"components.Input.error.attribute.taken": "Trường nhập liệu này đã tồn tại",

View File

@ -108,7 +108,7 @@
"app.components.listPlugins.title.plural": "{number} 个插件已安装",
"app.components.listPlugins.title.singular": "{number} 个插件已安装",
"app.components.listPluginsPage.deletePlugin.error": "卸载插件时出错",
"app.links.configure-view": "配置视图",
"app.utils.SelectOption.defaultMessage": " ",
@ -117,6 +117,16 @@
"components.AutoReloadBlocker.description": "使用以下命令中的一个来运行 Strapi",
"components.AutoReloadBlocker.header": "这个插件需要重新加载特性。",
"components.ErrorBoundary.title": "哪里出问题了…",
"components.FilterOptions.FILTER_TYPES.=": "等于",
"components.FilterOptions.FILTER_TYPES._contains": "包含",
"components.FilterOptions.FILTER_TYPES._containss": "包含(区分大小写)",
"components.FilterOptions.FILTER_TYPES._gt": "大于",
"components.FilterOptions.FILTER_TYPES._gte": "大于等于",
"components.FilterOptions.FILTER_TYPES._lt": "小于",
"components.FilterOptions.FILTER_TYPES._lte": "小于等于",
"components.FilterOptions.FILTER_TYPES._ne": "不等于",
"components.FilterOptions.FILTER_TYPES._in": "包含在指定数组中",
"components.FilterOptions.FILTER_TYPES._nin": "不包含在指定数组中",
"components.Input.error.attribute.key.taken": "此值已经存在",
"components.Input.error.attribute.sameKeyAndName": "不能相等",
"components.Input.error.attribute.taken": "此字段名称已经存在",

View File

@ -95,6 +95,14 @@
"app.utils.placeholder.defaultMessage": " ",
"components.AutoReloadBlocker.header": "這個擴充功能需要自動重新整理功能才能載入",
"components.ErrorBoundary.title": "有錯誤發生...",
"components.FilterOptions.FILTER_TYPES.=": "等於",
"components.FilterOptions.FILTER_TYPES._contains": "包含",
"components.FilterOptions.FILTER_TYPES._containss": "包含(區分大小寫)",
"components.FilterOptions.FILTER_TYPES._gt": "大於",
"components.FilterOptions.FILTER_TYPES._gte": "大於等於",
"components.FilterOptions.FILTER_TYPES._lt": "小於",
"components.FilterOptions.FILTER_TYPES._lte": "小於等於",
"components.FilterOptions.FILTER_TYPES._ne": "不等於",
"components.Input.error.attribute.key.taken": "這個數值已經存在了",
"components.Input.error.attribute.sameKeyAndName": "不能等於",
"components.Input.error.attribute.taken": "這個欄位名稱已經存在了",

View File

@ -0,0 +1,42 @@
import { cloneDeep } from 'lodash';
import invariant from 'invariant';
class ComponentApi {
components = {};
getComponent = name => {
invariant(name, 'A name must be provided');
return cloneDeep(this.components[name]) || null;
};
getComponents = () => {
const components = cloneDeep(this.components);
return Object.keys(components).reduce((acc, current) => {
acc[current] = components[current].Component;
return acc;
}, {});
};
registerComponent = component => {
const { name, Component } = component;
invariant(Component, 'A Component must be provided');
invariant(name, 'A name must be provided');
invariant(this.components[name] === undefined, 'A similar field already exists');
this.components[name] = { Component };
};
removeComponent = name => {
invariant(name, 'A name must be provided in order to remove a field');
delete this.components[name];
};
}
export default () => {
return new ComponentApi();
};

View File

@ -0,0 +1,48 @@
// NOTE TO PLUGINS DEVELOPERS:
// If you modify this file you also need to update the documentation accordingly
// Here's the file: strapi/docs/3.0.0-beta.x/plugin-development/frontend-field-api.md
// Here's the file: strapi/docs/3.0.0-beta.x/guides/registering-a-field-in-admin.md
// IF THE DOC IS NOT UPDATED THE PULL REQUEST WILL NOT BE MERGED
import { cloneDeep } from 'lodash';
import invariant from 'invariant';
class FieldApi {
fields = {};
getField = type => {
invariant(type, 'A type must be provided');
return cloneDeep(this.fields[type]) || null;
};
getFields = () => {
const fields = cloneDeep(this.fields);
return Object.keys(fields).reduce((acc, current) => {
acc[current] = fields[current].Component;
return acc;
}, {});
};
registerField = field => {
const { type, Component } = field;
invariant(Component, 'A Component must be provided');
invariant(type, 'A type must be provided');
invariant(this.fields[type] === undefined, 'A similar field already exists');
this.fields[type] = { Component };
};
removeField = type => {
invariant(type, 'A type must be provided in order to remove a field');
delete this.fields[type];
};
}
export default () => {
return new FieldApi();
};

View File

@ -0,0 +1,12 @@
import ComponentApi from './ComponentApi';
import FieldApi from './FieldApi';
class Strapi {
componentApi = ComponentApi();
fieldApi = FieldApi();
}
export default () => {
return new Strapi();
};

View File

@ -8,6 +8,7 @@ const ALLOWED_EVENTS = [
'entry.update',
'entry.delete',
'media.create',
'media.update',
'media.delete',
];
@ -165,11 +166,7 @@ module.exports = {
const webhook = await strapi.webhookStore.findWebhook(id);
const response = await strapi.webhookRunner.run(
webhook,
'trigger-test',
{}
);
const response = await strapi.webhookRunner.run(webhook, 'trigger-test', {});
ctx.body = { data: response };
},

View File

@ -1,5 +1,6 @@
/* eslint-disable no-useless-escape */
const path = require('path');
const _ = require('lodash');
const fs = require('fs-extra');
const webpack = require('webpack');
const getWebpackConfig = require('./webpack.config.js');
@ -9,6 +10,71 @@ const chokidar = require('chokidar');
const getPkgPath = name => path.dirname(require.resolve(`${name}/package.json`));
function getCustomWebpackConfig(dir, config) {
const adminConfigPath = path.join(dir, 'admin', 'admin.config.js');
let webpackConfig = getWebpackConfig(config);
if (fs.existsSync(adminConfigPath)) {
const adminConfig = require(path.resolve(adminConfigPath));
if (_.isFunction(adminConfig.webpack)) {
webpackConfig = adminConfig.webpack(webpackConfig, webpack);
if (!webpackConfig) {
console.error(
`${chalk.red('Error:')} Nothing was returned from your custom webpack configuration`
);
process.exit(1);
}
}
}
return webpackConfig;
}
async function build({ dir, env, options, optimize }) {
// Create the cache dir containing the front-end files.
await createCacheDir(dir);
const cacheDir = path.resolve(dir, '.cache');
const entry = path.resolve(cacheDir, 'admin', 'src', 'app.js');
const dest = path.resolve(dir, 'build');
const config = getCustomWebpackConfig(dir, { entry, dest, env, options, optimize });
const compiler = webpack(config);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
let messages;
if (err) {
if (!err.message) {
return reject(err);
}
messages = {
errors: [err.message],
warnings: [],
};
} else {
messages = stats.toJson({ all: false, warnings: true, errors: true });
}
if (messages.errors.length) {
// Only keep the first error. Others are often indicative
// of the same problem, but confuse the reader with noise.
if (messages.errors.length > 1) {
messages.errors.length = 1;
}
return reject(new Error(messages.errors.join('\n\n')));
}
return resolve({
stats,
warnings: messages.warnings,
});
});
});
}
async function createPluginsJs(plugins, localPlugins, dest) {
const content = `
const injectReducer = require('./utils/injectReducer').default;
@ -53,6 +119,14 @@ module.exports = {
return fs.writeFile(path.resolve(dest, 'admin', 'src', 'plugins.js'), content);
}
async function clean({ dir }) {
const buildDir = path.join(dir, 'build');
const cacheDir = path.join(dir, '.cache');
fs.removeSync(buildDir);
fs.removeSync(cacheDir);
}
async function copyPlugin(name, dest) {
const pkgFilePath = getPkgPath(name);
@ -153,49 +227,6 @@ async function createCacheDir(dir) {
);
}
async function build({ dir, env, options, optimize }) {
// Create the cache dir containing the front-end files.
await createCacheDir(dir);
const cacheDir = path.resolve(dir, '.cache');
const entry = path.resolve(cacheDir, 'admin', 'src', 'app.js');
const dest = path.resolve(dir, 'build');
const config = getWebpackConfig({ entry, dest, env, options, optimize });
const compiler = webpack(config);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
let messages;
if (err) {
if (!err.message) {
return reject(err);
}
messages = {
errors: [err.message],
warnings: [],
};
} else {
messages = stats.toJson({ all: false, warnings: true, errors: true });
}
if (messages.errors.length) {
// Only keep the first error. Others are often indicative
// of the same problem, but confuse the reader with noise.
if (messages.errors.length > 1) {
messages.errors.length = 1;
}
return reject(new Error(messages.errors.join('\n\n')));
}
return resolve({
stats,
warnings: messages.warnings,
});
});
});
}
async function watchAdmin({ dir, host, port, options }) {
// Create the cache dir containing the front-end files.
await createCacheDir(dir);
@ -223,7 +254,8 @@ async function watchAdmin({ dir, host, port, options }) {
},
};
const server = new WebpackDevServer(webpack(getWebpackConfig(args)), opts);
const webpackConfig = getCustomWebpackConfig(dir, args);
const server = new WebpackDevServer(webpack(webpackConfig), opts);
server.listen(port, host, function(err) {
if (err) {
@ -328,6 +360,7 @@ async function watchFiles(dir, ignoreFiles = []) {
}
module.exports = {
clean,
build,
watchAdmin,
};

View File

@ -1,6 +1,6 @@
{
"name": "strapi-admin",
"version": "3.0.0-beta.19.5",
"version": "3.0.0-beta.20",
"description": "Strapi Admin",
"repository": {
"type": "git",
@ -22,12 +22,12 @@
"@babel/preset-env": "^7.4.3",
"@babel/preset-react": "^7.0.0",
"@babel/runtime": "^7.4.3",
"@buffetjs/core": "3.0.1",
"@buffetjs/custom": "3.0.1",
"@buffetjs/hooks": "3.0.0",
"@buffetjs/icons": "3.0.0",
"@buffetjs/styles": "3.0.0",
"@buffetjs/utils": "3.0.0",
"@buffetjs/core": "3.0.5",
"@buffetjs/custom": "3.0.5",
"@buffetjs/hooks": "3.0.5",
"@buffetjs/icons": "3.0.5",
"@buffetjs/styles": "3.0.5",
"@buffetjs/utils": "3.0.5",
"@fortawesome/fontawesome-free": "^5.11.2",
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-brands-svg-icons": "^5.11.2",
@ -77,7 +77,7 @@
"react-router-dom": "^5.0.0",
"react-transition-group": "^2.9.0",
"react-virtualized": "^9.21.2",
"reactstrap": "^5.0.0",
"reactstrap": "8.4.1",
"redux": "^4.0.1",
"redux-immutable": "^4.0.0",
"redux-saga": "^0.16.0",
@ -85,8 +85,8 @@
"reselect": "^3.0.1",
"sanitize.css": "^4.1.0",
"shelljs": "^0.7.8",
"strapi-helper-plugin": "3.0.0-beta.19.5",
"strapi-utils": "3.0.0-beta.19.5",
"strapi-helper-plugin": "3.0.0-beta.20",
"strapi-utils": "3.0.0-beta.20",
"style-loader": "^0.23.1",
"styled-components": "^5.0.0",
"terser-webpack-plugin": "^1.2.3",

View File

@ -412,6 +412,9 @@ module.exports = async ({ ORM, loadedModel, definition, connection, model }) =>
[definition.attributes[morphRelation.alias].filter]: {
type: 'text',
},
order: {
type: 'integer',
},
};
if (connection.options && connection.options.autoMigration !== false) {
@ -419,7 +422,7 @@ module.exports = async ({ ORM, loadedModel, definition, connection, model }) =>
}
}
// Equilize many to many releations
// Equilize many to many relations
const manyRelations = definition.associations.filter(({ nature }) =>
['manyToMany', 'manyWay'].includes(nature)
);

View File

@ -8,7 +8,7 @@ const { singular } = require('pluralize');
* @param {Object} options.filters - Filters params (start, limit, sort, where)
*/
const buildQuery = ({ model, filters }) => qb => {
if (_.has(filters, 'where') && Array.isArray(filters.where)) {
if (_.has(filters, 'where') && Array.isArray(filters.where) && filters.where.length > 0) {
qb.distinct();
buildJoinsAndFilter(qb, model, filters.where);
}

View File

@ -81,10 +81,7 @@ module.exports = function(strapi) {
function mountComponents(connectionName, ctx) {
const options = {
models: _.pickBy(
strapi.components,
({ connection }) => connection === connectionName
),
models: _.pickBy(strapi.components, ({ connection }) => connection === connectionName),
target: strapi.components,
};
@ -93,10 +90,7 @@ module.exports = function(strapi) {
function mountApis(connectionName, ctx) {
const options = {
models: _.pickBy(
strapi.models,
({ connection }) => connection === connectionName
),
models: _.pickBy(strapi.models, ({ connection }) => connection === connectionName),
target: strapi.models,
};
@ -105,10 +99,7 @@ module.exports = function(strapi) {
function mountAdmin(connectionName, ctx) {
const options = {
models: _.pickBy(
strapi.admin.models,
({ connection }) => connection === connectionName
),
models: _.pickBy(strapi.admin.models, ({ connection }) => connection === connectionName),
target: strapi.admin.models,
};
@ -121,10 +112,7 @@ module.exports = function(strapi) {
const plugin = strapi.plugins[name];
return mountModels(
{
models: _.pickBy(
plugin.models,
({ connection }) => connection === connectionName
),
models: _.pickBy(plugin.models, ({ connection }) => connection === connectionName),
target: plugin.models,
},
ctx

View File

@ -142,7 +142,11 @@ module.exports = ({ models, target }, ctx) => {
}
const { nature, verbose } =
utilsModels.getNature(details, name, undefined, model.toLowerCase()) || {};
utilsModels.getNature({
attribute: details,
attributeName: name,
modelName: model.toLowerCase(),
}) || {};
// Build associations key
utilsModels.defineAssociations(model.toLowerCase(), definition, details, name);
@ -305,6 +309,7 @@ module.exports = ({ models, target }, ctx) => {
: strapi.models[details.model];
const globalId = `${model.collectionName}_morph`;
const filter = _.get(model, ['attributes', details.via, 'filter'], 'field');
loadedModel[name] = function() {
return this.morphOne(
@ -312,7 +317,7 @@ module.exports = ({ models, target }, ctx) => {
details.via,
`${definition.collectionName}`
).query(qb => {
qb.where(_.get(model, ['attributes', details.via, 'filter'], 'field'), name);
qb.where(filter, name);
});
};
break;
@ -323,6 +328,7 @@ module.exports = ({ models, target }, ctx) => {
: strapi.models[details.collection];
const globalId = `${collection.collectionName}_morph`;
const filter = _.get(model, ['attributes', details.via, 'filter'], 'field');
loadedModel[name] = function() {
return this.morphMany(
@ -330,7 +336,7 @@ module.exports = ({ models, target }, ctx) => {
details.via,
`${definition.collectionName}`
).query(qb => {
qb.where(_.get(model, ['attributes', details.via, 'filter'], 'field'), name);
qb.where(filter, name).orderBy('order');
});
};
break;
@ -652,6 +658,7 @@ module.exports = ({ models, target }, ctx) => {
// Push attributes to be aware of model schema.
target[model]._attributes = definition.attributes;
target[model].updateRelations = relations.update;
target[model].deleteRelations = relations.deleteRelations;
await buildDatabaseSchema({
ORM,

View File

@ -132,26 +132,7 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) {
throw err;
}
const values = {};
model.associations.map(association => {
switch (association.nature) {
case 'oneWay':
case 'oneToOne':
case 'manyToOne':
case 'oneToManyMorph':
values[association.alias] = null;
break;
case 'manyWay':
case 'oneToMany':
case 'manyToMany':
case 'manyToManyMorph':
values[association.alias] = [];
break;
default:
}
});
await model.updateRelations({ [model.primaryKey]: id, values }, { transacting });
await model.deleteRelations(id, { transacting });
const runDelete = async trx => {
await deleteComponents(entry, { transacting: trx });
@ -694,6 +675,10 @@ const buildSearchQuery = (qb, model, params) => {
// Search in columns with text using index.
switch (model.client) {
case 'sqlite3': {
searchText.forEach(attr => qb.orWhereRaw(`${attr} LIKE ?`, `%${query}%`));
break;
}
case 'mysql':
qb.orWhereRaw(`MATCH(${searchText.join(',')}) AGAINST(? IN BOOLEAN MODE)`, `*${query}*`);
break;

Some files were not shown because too many files have changed in this diff Show More