mirror of
https://github.com/strapi/strapi.git
synced 2025-12-30 00:37:24 +00:00
Merge branch 'releases/v4' into pluginAPI/loadPlugin
This commit is contained in:
commit
5dd7f6c1be
@ -6,7 +6,10 @@ testApp/**
|
||||
examples/**
|
||||
cypress/**
|
||||
packages/generators/plugin/files/admin/src/**
|
||||
packages/core/helper-plugin/**
|
||||
packages/core/helper-plugin/build/**
|
||||
packages/core/helper-plugin/lib/src/components/**
|
||||
packages/core/helper-plugin/lib/src/testUtils/**
|
||||
packages/core/helper-plugin/lib/src/utils/**
|
||||
packages/plugins/users-permissions/admin/OLD/**
|
||||
.eslintrc.js
|
||||
.eslintrc.front.js
|
||||
|
||||
@ -32,5 +32,6 @@ module.exports = {
|
||||
'node/no-path-concat': 'error',
|
||||
'node/no-callback-literal': 'error',
|
||||
'node/handle-callback-err': 'error',
|
||||
'one-var': ['error', 'never'],
|
||||
},
|
||||
};
|
||||
|
||||
@ -91,6 +91,7 @@ module.exports = {
|
||||
'no-underscore-dangle': 0,
|
||||
'no-use-before-define': ['error', { functions: false, classes: false, variables: false }],
|
||||
'object-curly-newline': [2, { multiline: true, consistent: true }],
|
||||
'one-var': ['error', 'never'],
|
||||
'operator-linebreak': 0,
|
||||
'padding-line-between-statements': [
|
||||
'error',
|
||||
|
||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@ -7,3 +7,5 @@ updates:
|
||||
versioning-strategy: increase
|
||||
reviewers:
|
||||
- strapi/maintainers
|
||||
# disable dependency pull requests while we work on v4
|
||||
open-pull-requests-limit: 0
|
||||
|
||||
22
.github/workflows/tests.yml
vendored
22
.github/workflows/tests.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14]
|
||||
node: [12, 14, 16]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2-beta
|
||||
@ -32,7 +32,7 @@ jobs:
|
||||
CODECOV_TOKEN: ${{ secrets.codecov }}
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14]
|
||||
node: [12, 14, 16]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2-beta
|
||||
@ -52,7 +52,7 @@ jobs:
|
||||
CODECOV_TOKEN: ${{ secrets.codecov }}
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14]
|
||||
node: [12, 14, 16]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2-beta
|
||||
@ -72,7 +72,7 @@ jobs:
|
||||
name: '[CE] E2E (postgres, node: ${{ matrix.node }})'
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14]
|
||||
node: [12, 14, 16]
|
||||
max-parallel: 2
|
||||
services:
|
||||
postgres:
|
||||
@ -108,7 +108,7 @@ jobs:
|
||||
name: '[CE] E2E (mysql, node: ${{ matrix.node }})'
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14]
|
||||
node: [12, 14, 16]
|
||||
max-parallel: 2
|
||||
services:
|
||||
mysql:
|
||||
@ -143,7 +143,7 @@ jobs:
|
||||
name: '[CE] E2E (sqlite, node: ${{ matrix.node }})'
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14]
|
||||
node: [12, 14, 16]
|
||||
max-parallel: 2
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@ -161,7 +161,7 @@ jobs:
|
||||
name: '[CE] E2E (mongo, node: ${{ matrix.node }})'
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14]
|
||||
node: [12, 14, 16]
|
||||
max-parallel: 2
|
||||
services:
|
||||
mongo:
|
||||
@ -188,7 +188,7 @@ jobs:
|
||||
STRAPI_LICENSE: ${{ secrets.strapiLicense }}
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14]
|
||||
node: [12, 14, 16]
|
||||
max-parallel: 2
|
||||
services:
|
||||
postgres:
|
||||
@ -228,7 +228,7 @@ jobs:
|
||||
STRAPI_LICENSE: ${{ secrets.strapiLicense }}
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14]
|
||||
node: [12, 14, 16]
|
||||
max-parallel: 2
|
||||
services:
|
||||
mysql:
|
||||
@ -267,7 +267,7 @@ jobs:
|
||||
STRAPI_LICENSE: ${{ secrets.strapiLicense }}
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14]
|
||||
node: [12, 14, 16]
|
||||
max-parallel: 2
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@ -289,7 +289,7 @@ jobs:
|
||||
STRAPI_LICENSE: ${{ secrets.strapiLicense }}
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14]
|
||||
node: [12, 14, 16]
|
||||
max-parallel: 2
|
||||
services:
|
||||
mongo:
|
||||
|
||||
15
README.md
15
README.md
@ -33,11 +33,8 @@
|
||||
<a href="https://www.npmjs.org/package/strapi">
|
||||
<img src="https://img.shields.io/npm/dm/strapi.svg" alt="Monthly download on NPM" />
|
||||
</a>
|
||||
<a href="https://travis-ci.org/strapi/strapi">
|
||||
<img src="https://travis-ci.org/strapi/strapi.svg?branch=master" alt="Travis Build Status" />
|
||||
</a>
|
||||
<a href="https://slack.strapi.io">
|
||||
<img src="https://slack.strapi.io/badge.svg" alt="Strapi on Slack" />
|
||||
<a href="https://discord.strapi.io">
|
||||
<img src="https://img.shields.io/discord/811989166782021633?label=Discord" alt="Strapi on Discord" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@ -100,16 +97,16 @@ Complete installation requirements can be found in the documentation under <a hr
|
||||
|
||||
**Node:**
|
||||
|
||||
- NodeJS >= 10.16 <=14
|
||||
- NodeJS >= 12 <= 16
|
||||
- NPM >= 6.x
|
||||
|
||||
**Database:**
|
||||
|
||||
- MongoDB >= 3.6
|
||||
- MySQL >= 5.6
|
||||
- MariaDB >= 10.1
|
||||
- PostgreSQL >= 10
|
||||
- SQLite >= 3
|
||||
- MongoDB >= 3.6 ([please read this thread before using MongoDB](https://forum.strapi.io/t/mongodb-compatibility-delayed-on-v4/4549/108))
|
||||
|
||||
**We recommend always using the latest version of Strapi to start your new projects**.
|
||||
|
||||
@ -121,7 +118,7 @@ Complete installation requirements can be found in the documentation under <a hr
|
||||
- **Blazing Fast:** Built on top of Node.js, Strapi delivers amazing performance.
|
||||
- **Front-end Agnostic:** Use any front-end framework (React, Vue, Angular, etc.), mobile apps or even IoT.
|
||||
- **Powerful CLI:** Scaffold projects and APIs on the fly.
|
||||
- **SQL & NoSQL databases:** Works with MongoDB, PostgreSQL, MySQL, MariaDB, and SQLite.
|
||||
- **SQL & NoSQL databases:** Works with MongoDB ([please read this thread before using MongoDB](https://forum.strapi.io/t/mongodb-compatibility-delayed-on-v4/4549/108)), PostgreSQL, MySQL, MariaDB, and SQLite.
|
||||
|
||||
**[See more on our website](https://strapi.io/overview)**.
|
||||
|
||||
@ -133,7 +130,7 @@ Please read our [Contributing Guide](./CONTRIBUTING.md) before submitting a Pull
|
||||
|
||||
For general help using Strapi, please refer to [the official Strapi documentation](https://strapi.io/documentation/). For additional help, you can use one of these channels to ask a question:
|
||||
|
||||
- [Slack](https://slack.strapi.io) (For live discussion with the Community and Strapi team)
|
||||
- [Discord](https://discord.strapi.io) (For live discussion with the Community and Strapi team)
|
||||
- [GitHub](https://github.com/strapi/strapi) (Bug reports, Contributions)
|
||||
- [Community Forum](https://forum.strapi.io) (Questions and Discussions)
|
||||
- [Academy](https://academy.strapi.io) (Learn the fundamentals of Strapi)
|
||||
|
||||
11
SECURITY.md
11
SECURITY.md
@ -2,19 +2,12 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
As of May 2020 (and until this document is updated), only the v3.0.0 *stable* release of Strapi is supported for updates. Any previous versions are currently not supported and users are advised to use them "at their own risk".
|
||||
As of May 2020 (and until this document is updated), only the v3.0.0 _stable_ release of Strapi is supported for updates. Any previous versions are currently not supported and users are advised to use them "at their own risk".
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report (suspected) security vulnerabilities to
|
||||
**[security@strapi.io](mailto:security@strapi.io)** or via the [Strapi Slack](https://slack.strapi.io).
|
||||
|
||||
When reporting a (suspected) security vulnerability via slack please reach out to any of the following Strapi employees directly:
|
||||
|
||||
- `@aureliengeorget`
|
||||
- `@alexandre`
|
||||
- `@lauriejim`
|
||||
- `@soupette`
|
||||
**[security@strapi.io](mailto:security@strapi.io)**.
|
||||
|
||||
You will receive a response from us within 72 hours. If the issue is confirmed,
|
||||
we will release a patch as soon as possible depending on complexity
|
||||
|
||||
15
examples/getstarted/admin/admin.config.js
Normal file
15
examples/getstarted/admin/admin.config.js
Normal file
@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
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
|
||||
return config;
|
||||
},
|
||||
app: config => {
|
||||
config.locales = ['fr'];
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
@ -14,9 +14,19 @@
|
||||
],
|
||||
"comment": ""
|
||||
},
|
||||
"pluginOptions": {
|
||||
"i18n": {
|
||||
"localized": true
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
"postal_coder": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"pluginOptions": {
|
||||
"i18n": {
|
||||
"localized": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"categories": {
|
||||
"collection": "category",
|
||||
@ -32,7 +42,12 @@
|
||||
"videos"
|
||||
],
|
||||
"plugin": "upload",
|
||||
"required": false
|
||||
"required": false,
|
||||
"pluginOptions": {
|
||||
"i18n": {
|
||||
"localized": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"images": {
|
||||
"collection": "file",
|
||||
@ -41,22 +56,71 @@
|
||||
"images"
|
||||
],
|
||||
"plugin": "upload",
|
||||
"required": false
|
||||
"required": false,
|
||||
"pluginOptions": {
|
||||
"i18n": {
|
||||
"localized": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"city": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"maxLength": 200
|
||||
"maxLength": 200,
|
||||
"pluginOptions": {
|
||||
"i18n": {
|
||||
"localized": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"likes": {
|
||||
"collection": "like",
|
||||
"via": "address"
|
||||
},
|
||||
"json": {
|
||||
"type": "json"
|
||||
"type": "json",
|
||||
"pluginOptions": {
|
||||
"i18n": {
|
||||
"localized": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"slug": {
|
||||
"type": "uid"
|
||||
},
|
||||
"notrepeat_req": {
|
||||
"type": "component",
|
||||
"repeatable": false,
|
||||
"pluginOptions": {
|
||||
"i18n": {
|
||||
"localized": false
|
||||
}
|
||||
},
|
||||
"component": "blog.test-como",
|
||||
"required": true
|
||||
},
|
||||
"repeat_req": {
|
||||
"type": "component",
|
||||
"repeatable": true,
|
||||
"pluginOptions": {
|
||||
"i18n": {
|
||||
"localized": true
|
||||
}
|
||||
},
|
||||
"component": "blog.test-como",
|
||||
"required": true
|
||||
},
|
||||
"repeat_req_min": {
|
||||
"type": "component",
|
||||
"repeatable": true,
|
||||
"pluginOptions": {
|
||||
"i18n": {
|
||||
"localized": true
|
||||
}
|
||||
},
|
||||
"component": "blog.test-como",
|
||||
"required": false,
|
||||
"min": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "getstarted",
|
||||
"private": true,
|
||||
"version": "3.6.0",
|
||||
"version": "3.6.5",
|
||||
"description": "A Strapi application.",
|
||||
"scripts": {
|
||||
"develop": "strapi develop",
|
||||
@ -17,29 +17,29 @@
|
||||
"mysql": "2.18.1",
|
||||
"pg": "8.6.0",
|
||||
"sqlite3": "5.0.0",
|
||||
"@strapi/strapi": "3.6.0",
|
||||
"@strapi/admin": "3.6.0",
|
||||
"@strapi/connector-bookshelf": "3.6.0",
|
||||
"@strapi/connector-mongoose": "3.6.0",
|
||||
"strapi-middleware-views": "3.6.0",
|
||||
"@strapi/plugin-content-manager": "3.6.0",
|
||||
"@strapi/plugin-content-type-builder": "3.6.0",
|
||||
"@strapi/plugin-documentation": "3.6.0",
|
||||
"@strapi/plugin-email": "3.6.0",
|
||||
"@strapi/plugin-graphql": "3.6.0",
|
||||
"@strapi/plugin-i18n": "3.6.0",
|
||||
"@strapi/plugin-upload": "3.6.0",
|
||||
"@strapi/plugin-users-permissions": "3.6.0",
|
||||
"@strapi/provider-email-mailgun": "3.6.0",
|
||||
"@strapi/provider-upload-aws-s3": "3.6.0",
|
||||
"@strapi/provider-upload-cloudinary": "3.6.0",
|
||||
"@strapi/utils": "3.6.0"
|
||||
"@strapi/strapi": "3.6.5",
|
||||
"@strapi/admin": "3.6.5",
|
||||
"@strapi/connector-bookshelf": "3.6.5",
|
||||
"@strapi/connector-mongoose": "3.6.5",
|
||||
"strapi-middleware-views": "3.6.5",
|
||||
"@strapi/plugin-content-manager": "3.6.5",
|
||||
"@strapi/plugin-content-type-builder": "3.6.5",
|
||||
"@strapi/plugin-documentation": "3.6.5",
|
||||
"@strapi/plugin-email": "3.6.5",
|
||||
"@strapi/plugin-graphql": "3.6.5",
|
||||
"@strapi/plugin-i18n": "3.6.5",
|
||||
"@strapi/plugin-upload": "3.6.5",
|
||||
"@strapi/plugin-users-permissions": "3.6.5",
|
||||
"@strapi/provider-email-mailgun": "3.6.5",
|
||||
"@strapi/provider-upload-aws-s3": "3.6.5",
|
||||
"@strapi/provider-upload-cloudinary": "3.6.5",
|
||||
"@strapi/utils": "3.6.5"
|
||||
},
|
||||
"strapi": {
|
||||
"uuid": "getstarted"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0 <=14.x.x",
|
||||
"node": ">=12.x.x <=16.x.x",
|
||||
"npm": ">=6.0.0"
|
||||
},
|
||||
"license": "SEE LICENSE IN LICENSE"
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { prefixPluginTranslations } from '@strapi/helper-plugin';
|
||||
import pluginPkg from '../../package.json';
|
||||
import pluginId from './pluginId';
|
||||
|
||||
@ -7,31 +8,47 @@ const name = pluginPkg.strapi.name;
|
||||
|
||||
export default {
|
||||
register(app) {
|
||||
app.addMenuLink({
|
||||
to: `/plugins/${pluginId}`,
|
||||
icon,
|
||||
intlLabel: {
|
||||
id: `${pluginId}.plugin.name`,
|
||||
defaultMessage: 'My plugin',
|
||||
},
|
||||
Component: () => 'My plugin',
|
||||
permissions: [],
|
||||
});
|
||||
app.registerPlugin({
|
||||
description: pluginDescription,
|
||||
icon,
|
||||
id: pluginId,
|
||||
isReady: true,
|
||||
isRequired: pluginPkg.strapi.required || false,
|
||||
mainComponent: () => 'My plugin',
|
||||
name,
|
||||
settings: null,
|
||||
trads: {},
|
||||
menu: {
|
||||
pluginsSectionLinks: [
|
||||
{
|
||||
destination: `/plugins/${pluginId}`,
|
||||
icon,
|
||||
label: {
|
||||
id: `${pluginId}.plugin.name`,
|
||||
defaultMessage: 'My plugin',
|
||||
},
|
||||
name,
|
||||
permissions: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
boot() {},
|
||||
async registerTrads({ locales }) {
|
||||
const importedTrads = await Promise.all(
|
||||
locales.map(locale => {
|
||||
return import(
|
||||
/* webpackChunkName: "[pluginId]-[request]" */ `./translations/${locale}.json`
|
||||
)
|
||||
.then(({ default: data }) => {
|
||||
return {
|
||||
data: prefixPluginTranslations(data, pluginId),
|
||||
locale,
|
||||
};
|
||||
})
|
||||
.catch(() => {
|
||||
return {
|
||||
data: {},
|
||||
locale,
|
||||
};
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
return Promise.resolve(importedTrads);
|
||||
},
|
||||
};
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import getTrad from '../../utils/getTrad';
|
||||
|
||||
const App = () => {
|
||||
return <FormattedMessage id={getTrad('plugin.name')} defaultMessage="My plugin" />;
|
||||
};
|
||||
|
||||
export default App;
|
||||
@ -0,0 +1,3 @@
|
||||
{
|
||||
"plugin.name": "My plugin"
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
import pluginId from '../pluginId';
|
||||
|
||||
const getTrad = id => `${pluginId}.${id}`;
|
||||
|
||||
export default getTrad;
|
||||
@ -54,7 +54,10 @@ module.exports = {
|
||||
],
|
||||
moduleNameMapper,
|
||||
rootDir: process.cwd(),
|
||||
setupFiles: ['<rootDir>/test/config/front/test-bundler.js'],
|
||||
setupFiles: [
|
||||
'<rootDir>/test/config/front/test-bundler.js',
|
||||
'<rootDir>/packages/admin-test-utils/lib/mocks/LocalStorageMock.js',
|
||||
],
|
||||
testPathIgnorePatterns: [
|
||||
'/node_modules/',
|
||||
'<rootDir>/examples/getstarted/',
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "3.6.0",
|
||||
"version": "3.6.5",
|
||||
"packages": [
|
||||
"packages/*",
|
||||
"examples/*"
|
||||
|
||||
@ -2,9 +2,10 @@
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@swc-node/jest": "^1.1.0",
|
||||
"@testing-library/jest-dom": "^5.11.10",
|
||||
"@testing-library/jest-dom": "^5.12.0",
|
||||
"@testing-library/react": "^11.2.6",
|
||||
"@testing-library/react-hooks": "^3.4.2",
|
||||
"@testing-library/user-event": "13.1.9",
|
||||
"axios-mock-adapter": "^1.19.0",
|
||||
"babel-eslint": "^10.0.0",
|
||||
"chalk": "4.1.1",
|
||||
@ -12,7 +13,7 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"enzyme": "^3.9.0",
|
||||
"enzyme-adapter-react-16": "^1.15.6",
|
||||
"eslint": "^7.24.0",
|
||||
"eslint": "^7.25.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-config-prettier": "^6.15.0",
|
||||
@ -43,7 +44,7 @@
|
||||
"request-promise-native": "^1.0.9",
|
||||
"rimraf": "3.0.2",
|
||||
"snyk": "^1.566.0",
|
||||
"stylelint": "13.12.0",
|
||||
"stylelint": "13.13.1",
|
||||
"stylelint-config-recommended": "3.0.0",
|
||||
"stylelint-config-styled-components": "0.1.1",
|
||||
"stylelint-processor-styled-components": "1.10.0",
|
||||
@ -98,7 +99,7 @@
|
||||
"url": "https://github.com/strapi/strapi/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0 <=14.x.x",
|
||||
"node": ">=12.x.x <=16.x.x",
|
||||
"npm": ">=6.0.0"
|
||||
},
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
|
||||
16
packages/admin-test-utils/lib/fixtures/index.js
Normal file
16
packages/admin-test-utils/lib/fixtures/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
const adminPermissions = require('./permissions/admin-permissions');
|
||||
const cmPermissions = require('./permissions/content-manager-permissions');
|
||||
const ctbPermissions = require('./permissions/content-type-builder-permissions');
|
||||
const store = require('./store');
|
||||
|
||||
const permissions = [...adminPermissions, ...cmPermissions, ...ctbPermissions];
|
||||
|
||||
module.exports = {
|
||||
adminPermissions,
|
||||
cmPermissions,
|
||||
ctbPermissions,
|
||||
permissions,
|
||||
store,
|
||||
};
|
||||
@ -0,0 +1,125 @@
|
||||
'use strict';
|
||||
|
||||
const adminPermissions = [
|
||||
{
|
||||
id: 169,
|
||||
action: 'admin::provider-login.read',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 170,
|
||||
action: 'admin::provider-login.update',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 171,
|
||||
action: 'admin::marketplace.read',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 172,
|
||||
action: 'admin::marketplace.plugins.install',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 173,
|
||||
action: 'admin::marketplace.plugins.uninstall',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 174,
|
||||
action: 'admin::webhooks.create',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 175,
|
||||
action: 'admin::webhooks.read',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 176,
|
||||
action: 'admin::webhooks.update',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 177,
|
||||
action: 'admin::webhooks.delete',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 178,
|
||||
action: 'admin::users.create',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 179,
|
||||
action: 'admin::users.read',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 180,
|
||||
action: 'admin::users.update',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 181,
|
||||
action: 'admin::users.delete',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 182,
|
||||
action: 'admin::roles.create',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 183,
|
||||
action: 'admin::roles.read',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 184,
|
||||
action: 'admin::roles.update',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 185,
|
||||
action: 'admin::roles.delete',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = adminPermissions;
|
||||
@ -0,0 +1,63 @@
|
||||
'use strict';
|
||||
|
||||
const cmPermissions = [
|
||||
{
|
||||
id: 2817,
|
||||
action: 'plugins::content-manager.single-types.configure-view',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 2818,
|
||||
action: 'plugins::content-manager.collection-types.configure-view',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 2819,
|
||||
action: 'plugins::content-manager.components.configure-layout',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
action: 'plugins::content-manager.explorer.create',
|
||||
subject: 'foo',
|
||||
properties: {
|
||||
fields: ['f1'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
action: 'plugins::content-manager.explorer.create',
|
||||
subject: 'foo',
|
||||
properties: {
|
||||
fields: ['f2'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
action: 'plugins::content-manager.explorer.read',
|
||||
subject: 'foo',
|
||||
properties: {
|
||||
fields: ['f1'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
action: 'plugins::content-manager.explorer.delete',
|
||||
subject: 'bar',
|
||||
},
|
||||
{
|
||||
action: 'plugins::content-manager.explorer.update',
|
||||
subject: 'bar',
|
||||
properties: {
|
||||
fields: ['f1'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = cmPermissions;
|
||||
@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
const ctbPermissions = [
|
||||
{
|
||||
id: 2820,
|
||||
action: 'plugins::content-type-builder.read',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{ id: 2821, action: 'plugins::upload.read', subject: null, properties: {}, conditions: [] },
|
||||
{
|
||||
id: 2822,
|
||||
action: 'plugins::upload.assets.create',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 2823,
|
||||
action: 'plugins::upload.assets.update',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 2824,
|
||||
action: 'plugins::upload.assets.download',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 2825,
|
||||
action: 'plugins::upload.assets.copy-link',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = ctbPermissions;
|
||||
15
packages/admin-test-utils/lib/fixtures/store/index.js
Normal file
15
packages/admin-test-utils/lib/fixtures/store/index.js
Normal file
@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line node/no-extraneous-require
|
||||
const { combineReducers, createStore } = require('redux');
|
||||
|
||||
const reducers = {
|
||||
rbacProvider: jest.fn(() => ({ allPermissions: null, collectionTypesRelatedPermissions: {} })),
|
||||
};
|
||||
|
||||
const store = createStore(combineReducers(reducers));
|
||||
|
||||
module.exports = {
|
||||
store,
|
||||
state: store.getState(),
|
||||
};
|
||||
7
packages/admin-test-utils/lib/index.js
Normal file
7
packages/admin-test-utils/lib/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const fixtures = require('./fixtures');
|
||||
|
||||
module.exports = {
|
||||
fixtures,
|
||||
};
|
||||
25
packages/admin-test-utils/lib/mocks/LocalStorageMock.js
Normal file
25
packages/admin-test-utils/lib/mocks/LocalStorageMock.js
Normal file
@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
class LocalStorageMock {
|
||||
constructor() {
|
||||
this.store = {};
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.store = {};
|
||||
}
|
||||
|
||||
getItem(key) {
|
||||
return this.store[key] || null;
|
||||
}
|
||||
|
||||
setItem(key, value) {
|
||||
this.store[key] = String(value);
|
||||
}
|
||||
|
||||
removeItem(key) {
|
||||
delete this.store[key];
|
||||
}
|
||||
}
|
||||
|
||||
global.localStorage = new LocalStorageMock();
|
||||
12
packages/admin-test-utils/package.json
Normal file
12
packages/admin-test-utils/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@strapi/admin-test-utils",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Test utilities for the Strapi administration panel",
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Strapi Team",
|
||||
"license": "MIT"
|
||||
}
|
||||
@ -2,15 +2,14 @@
|
||||
|
||||
const commander = require('commander');
|
||||
const generateNewApp = require('@strapi/generate-new');
|
||||
const promptUser = require('./utils/prompt-user');
|
||||
const packageJson = require('./package.json');
|
||||
|
||||
const program = new commander.Command(packageJson.name);
|
||||
|
||||
let projectName;
|
||||
|
||||
program
|
||||
.version(packageJson.version)
|
||||
.arguments('<directory>')
|
||||
.arguments('[directory]')
|
||||
.option('--no-run', 'Do not start the application after it is created')
|
||||
.option('--use-npm', 'Force usage of npm instead of yarn to create the project')
|
||||
.option('--debug', 'Display database connection error')
|
||||
@ -29,19 +28,41 @@ program
|
||||
.option('--template <templateurl>', 'Specify a Strapi template')
|
||||
.description('create a new application')
|
||||
.action(directory => {
|
||||
projectName = directory;
|
||||
initProject(directory, program);
|
||||
})
|
||||
.parse(process.argv);
|
||||
|
||||
if (projectName === undefined) {
|
||||
console.error('Please specify the <directory> of your project');
|
||||
function generateApp(projectName, options) {
|
||||
if (!projectName) {
|
||||
console.error('Please specify the <directory> of your project when using --quickstart');
|
||||
// eslint-disable-next-line no-process-exit
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-process-exit
|
||||
process.exit(1);
|
||||
return generateNewApp(projectName, options).then(() => {
|
||||
if (process.platform === 'win32') {
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
generateNewApp(projectName, program).then(() => {
|
||||
if (process.platform === 'win32') {
|
||||
process.exit(0);
|
||||
async function initProject(projectName, program) {
|
||||
if (program.quickstart) {
|
||||
return generateApp(projectName, program);
|
||||
}
|
||||
});
|
||||
|
||||
const prompt = await promptUser(projectName, program.template);
|
||||
|
||||
const directory = prompt.directory || projectName;
|
||||
const options = {
|
||||
template: prompt.template || program.template,
|
||||
quickstart: prompt.quick,
|
||||
};
|
||||
|
||||
const generateStrapiAppOptions = {
|
||||
...program,
|
||||
...options,
|
||||
};
|
||||
|
||||
return generateApp(directory, generateStrapiAppOptions);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-strapi-app",
|
||||
"version": "3.6.0",
|
||||
"version": "3.6.5",
|
||||
"description": "Generate a new Strapi application.",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"homepage": "https://strapi.io",
|
||||
@ -15,13 +15,13 @@
|
||||
"bin": {
|
||||
"create-strapi-app": "./index.js"
|
||||
},
|
||||
"files": [
|
||||
"create-strapi-app.js",
|
||||
"index.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"chalk": "4.1.1",
|
||||
"commander": "6.1.0",
|
||||
"@strapi/generate-new": "3.6.0"
|
||||
"inquirer": "8.1.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"@strapi/generate-new": "3.6.5"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"no tests yet\""
|
||||
@ -39,7 +39,7 @@
|
||||
"url": "https://github.com/strapi/strapi/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0 <=14.x.x",
|
||||
"node": ">=12.x.x <=16.x.x",
|
||||
"npm": ">=6.0.0"
|
||||
},
|
||||
"gitHead": "231263a3535658bab1e9492c6aaaed8692d62a53"
|
||||
|
||||
114
packages/cli/create-strapi-app/utils/prompt-user.js
Normal file
114
packages/cli/create-strapi-app/utils/prompt-user.js
Normal file
@ -0,0 +1,114 @@
|
||||
'use strict';
|
||||
|
||||
const inquirer = require('inquirer');
|
||||
const fetch = require('node-fetch');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
/**
|
||||
* @param {string|null} projectName - The name/path of project
|
||||
* @param {string|null} template - The Github repo of the template
|
||||
* @returns Object containting prompt answers
|
||||
*/
|
||||
module.exports = async function promptUser(projectName, template) {
|
||||
const questions = await getPromptQuestions(projectName, template);
|
||||
const [initialResponse, templateQuestion] = await Promise.all([
|
||||
inquirer.prompt(questions),
|
||||
getTemplateQuestion(),
|
||||
]);
|
||||
|
||||
if (initialResponse.useTemplate) {
|
||||
const updatedResponse = await inquirer.prompt(templateQuestion);
|
||||
return { ...initialResponse, ...updatedResponse };
|
||||
}
|
||||
|
||||
return initialResponse;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns Prompt question object
|
||||
*/
|
||||
async function getTemplateQuestion() {
|
||||
const content = await getTemplateData();
|
||||
// Fallback to manual input when fetch fails
|
||||
if (!content) {
|
||||
return {
|
||||
name: 'template',
|
||||
type: 'input',
|
||||
message: 'Please provide the GitHub URL for your template:',
|
||||
};
|
||||
}
|
||||
|
||||
const choices = content.map(option => {
|
||||
const name = option.title.replace('Template', '');
|
||||
return {
|
||||
name,
|
||||
value: `https://github.com/${option.repo}`,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
name: 'template',
|
||||
type: 'list',
|
||||
message: `Select a template`,
|
||||
pageSize: choices.length,
|
||||
choices,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns Array of prompt question objects
|
||||
*/
|
||||
async function getPromptQuestions(projectName, template) {
|
||||
return [
|
||||
{
|
||||
type: 'input',
|
||||
default: 'my-strapi-project',
|
||||
name: 'directory',
|
||||
message: 'What would you like to name your project?',
|
||||
when: !projectName,
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'quick',
|
||||
message: 'Choose your installation type',
|
||||
choices: [
|
||||
{
|
||||
name: 'Quickstart (recommended)',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: 'Custom (manual settings)',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'useTemplate',
|
||||
when: !template,
|
||||
message:
|
||||
'Would you like to use a template? (Templates are Strapi configurations designed for a specific use case)',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns JSON template data
|
||||
*/
|
||||
async function getTemplateData() {
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/strapi/community-content/contents/templates/templates.yml`
|
||||
);
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { content } = await response.json();
|
||||
const buff = Buffer.from(content, 'base64');
|
||||
const stringified = buff.toString('utf-8');
|
||||
|
||||
return yaml.load(stringified);
|
||||
}
|
||||
@ -4,12 +4,13 @@ const commander = require('commander');
|
||||
|
||||
const packageJson = require('./package.json');
|
||||
const buildStarter = require('./utils/build-starter');
|
||||
const promptUser = require('./utils/prompt-user');
|
||||
|
||||
const program = new commander.Command(packageJson.name);
|
||||
|
||||
program
|
||||
.version(packageJson.version)
|
||||
.arguments('<directory> <starterurl>')
|
||||
.arguments('[directory], [starterurl]')
|
||||
.option('--use-npm', 'Force usage of npm instead of yarn to create the project')
|
||||
.option('--debug', 'Display database connection error')
|
||||
.option('--quickstart', 'Quickstart app creation')
|
||||
@ -30,13 +31,41 @@ program
|
||||
.action((directory, starterUrl, programArgs) => {
|
||||
const projectArgs = { projectName: directory, starterUrl };
|
||||
|
||||
buildStarter(projectArgs, programArgs).catch(error => {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
initProject(projectArgs, programArgs);
|
||||
});
|
||||
|
||||
program.exitOverride();
|
||||
function generateApp(projectArgs, programArgs) {
|
||||
if (!projectArgs.projectName || !projectArgs.starterUrl) {
|
||||
console.error(
|
||||
'Please specify the <directory> and <starterurl> of your project when using --quickstart'
|
||||
);
|
||||
// eslint-disable-next-line no-process-exit
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return buildStarter(projectArgs, programArgs);
|
||||
}
|
||||
|
||||
async function initProject(projectArgs, program) {
|
||||
const { projectName, starterUrl } = projectArgs;
|
||||
if (program.quickstart) {
|
||||
return generateApp(projectArgs, program);
|
||||
}
|
||||
|
||||
const prompt = await promptUser(projectName, starterUrl);
|
||||
|
||||
const promptProjectArgs = {
|
||||
projectName: prompt.directory || projectName,
|
||||
starterUrl: prompt.starter || starterUrl,
|
||||
};
|
||||
|
||||
const programArgs = {
|
||||
...program,
|
||||
quickstart: prompt.quick,
|
||||
};
|
||||
|
||||
return generateApp(promptProjectArgs, programArgs);
|
||||
}
|
||||
|
||||
try {
|
||||
program.parse(process.argv);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-strapi-starter",
|
||||
"version": "3.6.0",
|
||||
"version": "3.6.5",
|
||||
"description": "Generate a new Strapi application.",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"homepage": "https://strapi.io",
|
||||
@ -22,9 +22,11 @@
|
||||
"execa": "5.0.0",
|
||||
"fs-extra": "9.1.0",
|
||||
"git-url-parse": "11.4.4",
|
||||
"node-fetch": "2.6.1",
|
||||
"ora": "5.3.0",
|
||||
"@strapi/generate-new": "3.6.0",
|
||||
"inquirer": "8.1.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"ora": "5.4.0",
|
||||
"@strapi/generate-new": "3.6.5",
|
||||
"tar": "6.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
@ -43,7 +45,7 @@
|
||||
"url": "https://github.com/strapi/strapi/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0 <=14.x.x",
|
||||
"node": ">=12.x.x <=16.x.x",
|
||||
"npm": ">=6.0.0"
|
||||
},
|
||||
"gitHead": "231263a3535658bab1e9492c6aaaed8692d62a53"
|
||||
|
||||
@ -12,12 +12,12 @@ const generateNewApp = require('@strapi/generate-new');
|
||||
|
||||
const hasYarn = require('./has-yarn');
|
||||
const { runInstall, runApp, initGit } = require('./child-process');
|
||||
const { getRepoInfo, downloadGithubRepo } = require('./fetch-github');
|
||||
const { getRepoInfo, downloadGitHubRepo } = require('./fetch-github');
|
||||
const logger = require('./logger');
|
||||
const stopProcess = require('./stop-process');
|
||||
|
||||
/**
|
||||
* @param {string} filePath Path to starter.json file
|
||||
* @param {string} - filePath Path to starter.json file
|
||||
*/
|
||||
function readStarterJson(filePath, starterUrl) {
|
||||
try {
|
||||
@ -29,11 +29,11 @@ function readStarterJson(filePath, starterUrl) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} rootPath Path to the project directory
|
||||
* @param {string} projectName Name of the project
|
||||
* @param {string} rootPath - Path to the project directory
|
||||
* @param {string} projectName - Name of the project
|
||||
*/
|
||||
async function initPackageJson(rootPath, projectName) {
|
||||
const packageManager = hasYarn ? 'yarn --cwd' : 'npm run --prefix';
|
||||
const packageManager = hasYarn() ? 'yarn --cwd' : 'npm run --prefix';
|
||||
|
||||
try {
|
||||
await fse.writeJson(
|
||||
@ -63,7 +63,7 @@ async function initPackageJson(rootPath, projectName) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path The directory path for install
|
||||
* @param {string} path - The directory path for install
|
||||
*/
|
||||
async function installWithLogs(path) {
|
||||
const installPrefix = chalk.yellow('Installing dependencies:');
|
||||
@ -86,20 +86,23 @@ async function installWithLogs(path) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} projectArgs projectName and starterUrl for the project
|
||||
* @param {object} program Commands for generating new application
|
||||
* @param {Object} projectArgs - The arguments for create a project
|
||||
* @param {string|null} projectArgs.projectName - The name/path of project
|
||||
* @param {string|null} projectArgs.starterUrl - The GitHub repo of the starter
|
||||
* @param {Object} program - Commands for generating new application
|
||||
*/
|
||||
module.exports = async function buildStarter(projectArgs, program) {
|
||||
const { projectName, starterUrl } = projectArgs;
|
||||
module.exports = async function buildStarter(programArgs, program) {
|
||||
let { projectName, starterUrl } = programArgs;
|
||||
|
||||
// Fetch repo info
|
||||
const repoInfo = await getRepoInfo(starterUrl);
|
||||
const { fullName } = repoInfo;
|
||||
|
||||
// Create temporary directory for starter
|
||||
const tmpDir = await fse.mkdtemp(join(os.tmpdir(), 'strapi-'));
|
||||
|
||||
// Fetch repo info
|
||||
const { full_name } = await getRepoInfo(starterUrl);
|
||||
|
||||
// Download repo inside tmp dir
|
||||
await downloadGithubRepo(starterUrl, tmpDir);
|
||||
// Download repo inside temporary directory
|
||||
await downloadGitHubRepo(repoInfo, tmpDir);
|
||||
|
||||
const starterJson = readStarterJson(join(tmpDir, 'starter.json'), starterUrl);
|
||||
|
||||
@ -130,14 +133,7 @@ module.exports = async function buildStarter(projectArgs, program) {
|
||||
// Delete temporary directory
|
||||
await fse.remove(tmpDir);
|
||||
|
||||
console.log(`Creating Strapi starter frontend at ${chalk.yellow(frontendPath)}.`);
|
||||
|
||||
// Install frontend dependencies
|
||||
console.log(`Installing ${chalk.yellow(full_name)} starter`);
|
||||
|
||||
await installWithLogs(frontendPath);
|
||||
|
||||
const fullUrl = `https://github.com/${full_name}`;
|
||||
const fullUrl = `https://github.com/${fullName}`;
|
||||
// Set command options for Strapi app
|
||||
const generateStrapiAppOptions = {
|
||||
...program,
|
||||
@ -149,6 +145,11 @@ module.exports = async function buildStarter(projectArgs, program) {
|
||||
// Create strapi app using the template
|
||||
await generateNewApp(join(rootPath, 'backend'), generateStrapiAppOptions);
|
||||
|
||||
// Install frontend dependencies
|
||||
console.log(`Creating Strapi starter frontend at ${chalk.green(frontendPath)}.`);
|
||||
console.log(`Installing ${chalk.yellow(fullName)} starter`);
|
||||
await installWithLogs(frontendPath);
|
||||
|
||||
// Setup monorepo
|
||||
initPackageJson(rootPath, projectBasename);
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ const logger = require('./logger');
|
||||
* @param {string} path Path to directory (frontend, backend)
|
||||
*/
|
||||
function runInstall(path) {
|
||||
if (hasYarn) {
|
||||
if (hasYarn()) {
|
||||
return execa('yarn', ['install'], {
|
||||
cwd: path,
|
||||
stdin: 'ignore',
|
||||
@ -20,7 +20,7 @@ function runInstall(path) {
|
||||
}
|
||||
|
||||
function runApp(rootPath) {
|
||||
if (hasYarn) {
|
||||
if (hasYarn()) {
|
||||
return execa('yarn', ['develop'], {
|
||||
stdio: 'inherit',
|
||||
cwd: rootPath,
|
||||
|
||||
@ -4,25 +4,29 @@ const tar = require('tar');
|
||||
const fetch = require('node-fetch');
|
||||
const parseGitUrl = require('git-url-parse');
|
||||
const chalk = require('chalk');
|
||||
|
||||
const stopProcess = require('./stop-process');
|
||||
|
||||
function getShortcut(starter) {
|
||||
let full_name;
|
||||
// Determine if it is another organization
|
||||
function parseShorthand(starter) {
|
||||
// Determine if it is comes from another owner
|
||||
if (starter.includes('/')) {
|
||||
const [org, project] = starter.split('/');
|
||||
full_name = `${org}/strapi-starter-${project}`;
|
||||
} else {
|
||||
full_name = `strapi/strapi-starter-${starter}`;
|
||||
const [owner, partialName] = starter.split('/');
|
||||
const name = `strapi-starter-${partialName}`;
|
||||
return {
|
||||
name,
|
||||
fullName: `${owner}/${name}`,
|
||||
};
|
||||
}
|
||||
|
||||
const name = `strapi-starter-${starter}`;
|
||||
return {
|
||||
full_name,
|
||||
usedShortcut: true,
|
||||
name,
|
||||
fullName: `strapi/${name}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} repo The path to repo
|
||||
* @param {string} repo The full name of the repository.
|
||||
*/
|
||||
async function getDefaultBranch(repo) {
|
||||
const response = await fetch(`https://api.github.com/repos/${repo}`);
|
||||
@ -36,49 +40,53 @@ async function getDefaultBranch(repo) {
|
||||
}
|
||||
|
||||
const { default_branch } = await response.json();
|
||||
|
||||
return default_branch;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} starterUrl Github url to starter project
|
||||
* @param {string} starter GitHub URL or shorthand to a starter project.
|
||||
*/
|
||||
async function getRepoInfo(starter) {
|
||||
const repoInfo = await parseGitUrl(starter);
|
||||
const { name, full_name, ref, protocols, source } = repoInfo;
|
||||
const { name, full_name: fullName, ref, filepath, protocols, source } = parseGitUrl(starter);
|
||||
|
||||
if (protocols.length === 0) {
|
||||
return getShortcut(starter);
|
||||
const repoInfo = parseShorthand(starter);
|
||||
return {
|
||||
...repoInfo,
|
||||
branch: await getDefaultBranch(repoInfo.fullName),
|
||||
usedShorthand: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (source !== 'github.com') {
|
||||
stopProcess(`Github URL not found for: ${chalk.yellow(starter)}`);
|
||||
stopProcess(`GitHub URL not found for: ${chalk.yellow(starter)}.`);
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
full_name,
|
||||
ref,
|
||||
};
|
||||
let branch;
|
||||
if (ref) {
|
||||
// Append the filepath to the parsed ref since a branch name could contain '/'
|
||||
// If so, the rest of the branch name will be considered 'filepath' by 'parseGitUrl'
|
||||
branch = filepath ? `${ref}/${filepath}` : ref;
|
||||
} else {
|
||||
branch = await getDefaultBranch(fullName);
|
||||
}
|
||||
|
||||
return { name, fullName, branch };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} starterUrl Github url for strapi starter
|
||||
* @param {string} tmpDir Path to temporary directory
|
||||
* @param {string} repoInfo GitHub repository information (full name, branch...).
|
||||
* @param {string} tmpDir Path to the destination temporary directory.
|
||||
*/
|
||||
async function downloadGithubRepo(starterUrl, tmpDir) {
|
||||
const { full_name, ref, usedShortcut } = await getRepoInfo(starterUrl);
|
||||
const default_branch = await getDefaultBranch(full_name);
|
||||
|
||||
const branch = ref ? ref : default_branch;
|
||||
|
||||
// Download from GitHub
|
||||
const codeload = `https://codeload.github.com/${full_name}/tar.gz/${branch}`;
|
||||
async function downloadGitHubRepo(repoInfo, tmpDir) {
|
||||
const { fullName, branch, usedShorthand } = repoInfo;
|
||||
|
||||
const codeload = `https://codeload.github.com/${fullName}/tar.gz/${branch}`;
|
||||
const response = await fetch(codeload);
|
||||
|
||||
if (!response.ok) {
|
||||
const message = usedShortcut ? `using the shortcut` : `using the url`;
|
||||
stopProcess(`Could not download the repository ${message}: ${chalk.yellow(`${starterUrl}`)}`);
|
||||
const message = usedShorthand ? `using the shorthand` : `using the url`;
|
||||
stopProcess(`Could not download the repository ${message}: ${chalk.yellow(fullName)}.`);
|
||||
}
|
||||
|
||||
await new Promise(resolve => {
|
||||
@ -86,4 +94,4 @@ async function downloadGithubRepo(starterUrl, tmpDir) {
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { getRepoInfo, downloadGithubRepo };
|
||||
module.exports = { getRepoInfo, downloadGitHubRepo };
|
||||
|
||||
@ -4,8 +4,9 @@ const execa = require('execa');
|
||||
|
||||
module.exports = function hasYarn() {
|
||||
try {
|
||||
const { code } = execa.shellSync('yarnpkg --version');
|
||||
if (code === 0) return true;
|
||||
const { exitCode } = execa.sync('yarn --version', { shell: true });
|
||||
|
||||
if (exitCode === 0) return true;
|
||||
return false;
|
||||
} catch (err) {
|
||||
return false;
|
||||
|
||||
103
packages/cli/create-strapi-starter/utils/prompt-user.js
Normal file
103
packages/cli/create-strapi-starter/utils/prompt-user.js
Normal file
@ -0,0 +1,103 @@
|
||||
'use strict';
|
||||
|
||||
const inquirer = require('inquirer');
|
||||
const fetch = require('node-fetch');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
/**
|
||||
* @param {string|null} projectName - The name/path of project
|
||||
* @param {string|null} starterUrl - The GitHub repo of the starter
|
||||
* @returns Object containting prompt answers
|
||||
*/
|
||||
module.exports = async function promptUser(projectName, starter) {
|
||||
const mainQuestions = [
|
||||
{
|
||||
type: 'input',
|
||||
default: 'my-strapi-project',
|
||||
name: 'directory',
|
||||
message: 'What would you like to name your project?',
|
||||
when: !projectName,
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'quick',
|
||||
message: 'Choose your installation type',
|
||||
choices: [
|
||||
{
|
||||
name: 'Quickstart (recommended)',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: 'Custom (manual settings)',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const [mainResponse, starterQuestion] = await Promise.all([
|
||||
inquirer.prompt(mainQuestions),
|
||||
getStarterQuestion(),
|
||||
]);
|
||||
|
||||
const starterResponse = await inquirer.prompt({
|
||||
name: 'starter',
|
||||
when: !starter,
|
||||
...starterQuestion,
|
||||
});
|
||||
|
||||
return { ...mainResponse, ...starterResponse };
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns Prompt question object
|
||||
*/
|
||||
async function getStarterQuestion() {
|
||||
const content = await getStarterData();
|
||||
|
||||
// Fallback to manual input when fetch fails
|
||||
if (!content) {
|
||||
return {
|
||||
type: 'input',
|
||||
message: 'Please provide the GitHub URL for the starter you would like to use:',
|
||||
};
|
||||
}
|
||||
|
||||
const choices = content.map(option => {
|
||||
const name = option.title.replace('Starter', '');
|
||||
|
||||
return {
|
||||
name,
|
||||
value: `https://github.com/${option.repo}`,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'list',
|
||||
message:
|
||||
'Which starter would you like to use? (Starters are fullstack Strapi applications designed for a specific use case)',
|
||||
pageSize: choices.length,
|
||||
choices,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns JSON starter data
|
||||
*/
|
||||
async function getStarterData() {
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/strapi/community-content/contents/starters/starters.yml`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { content } = await response.json();
|
||||
const buff = Buffer.from(content, 'base64');
|
||||
const stringified = buff.toString('utf-8');
|
||||
|
||||
return yaml.load(stringified);
|
||||
}
|
||||
@ -5,7 +5,7 @@ const { keys, each, prop, isEmpty } = require('lodash/fp');
|
||||
const { singular } = require('pluralize');
|
||||
const { toQueries, runPopulateQueries } = require('./utils/populate-queries');
|
||||
|
||||
const BOOLEAN_OPERATORS = ['or'];
|
||||
const BOOLEAN_OPERATORS = ['or', 'and'];
|
||||
|
||||
/**
|
||||
* Build filters on a bookshelf query
|
||||
@ -296,7 +296,7 @@ const buildJoinsAndFilter = (qb, model, filters) => {
|
||||
* @param {Object} options.value - Filter value
|
||||
*/
|
||||
const buildWhereClause = ({ qb, field, operator, value }) => {
|
||||
if (Array.isArray(value) && !['or', 'in', 'nin'].includes(operator)) {
|
||||
if (Array.isArray(value) && !['and', 'or', 'in', 'nin'].includes(operator)) {
|
||||
return qb.where(subQb => {
|
||||
for (let val of value) {
|
||||
subQb.orWhere(q => buildWhereClause({ qb: q, field, operator, value: val }));
|
||||
@ -305,6 +305,20 @@ const buildWhereClause = ({ qb, field, operator, value }) => {
|
||||
}
|
||||
|
||||
switch (operator) {
|
||||
case 'and':
|
||||
return qb.where(andQb => {
|
||||
value.forEach(andClause => {
|
||||
andQb.where(subQb => {
|
||||
if (Array.isArray(andClause)) {
|
||||
andClause.forEach(clause =>
|
||||
subQb.where(andQb => buildWhereClause({ qb: andQb, ...clause }))
|
||||
);
|
||||
} else {
|
||||
buildWhereClause({ qb: subQb, ...andClause });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
case 'or':
|
||||
return qb.where(orQb => {
|
||||
value.forEach(orClause => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strapi/connector-bookshelf",
|
||||
"version": "3.6.0",
|
||||
"version": "3.6.5",
|
||||
"description": "Bookshelf hook for the Strapi framework",
|
||||
"homepage": "https://strapi.io",
|
||||
"keywords": [
|
||||
@ -23,7 +23,7 @@
|
||||
"p-map": "4.0.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"rimraf": "3.0.2",
|
||||
"@strapi/utils": "3.6.0"
|
||||
"@strapi/utils": "3.6.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"knex": "0.21.18"
|
||||
@ -56,7 +56,7 @@
|
||||
"url": "https://github.com/strapi/strapi/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0 <=14.x.x",
|
||||
"node": ">=12.x.x <=16.x.x",
|
||||
"npm": ">=6.0.0"
|
||||
},
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@strapi/connector-mongoose",
|
||||
"version": "3.6.0",
|
||||
"version": "3.6.5",
|
||||
"description": "Mongoose hook for the Strapi framework",
|
||||
"homepage": "https://strapi.io",
|
||||
"keywords": [
|
||||
@ -22,7 +22,7 @@
|
||||
"p-map": "4.0.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"semver": "^7.3.5",
|
||||
"@strapi/utils": "3.6.0"
|
||||
"@strapi/utils": "3.6.5"
|
||||
},
|
||||
"author": {
|
||||
"email": "hi@strapi.io",
|
||||
@ -44,7 +44,7 @@
|
||||
"url": "https://github.com/strapi/strapi/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0 <=14.x.x",
|
||||
"node": ">=12.x.x <=16.x.x",
|
||||
"npm": ">=6.0.0"
|
||||
},
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
|
||||
@ -4,6 +4,9 @@ import { BrowserRouter } from 'react-router-dom';
|
||||
import { QueryClientProvider, QueryClient } from 'react-query';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { LibraryProvider, StrapiAppProvider } from '@strapi/helper-plugin';
|
||||
import pick from 'lodash/pick';
|
||||
import createHook from '@strapi/hooks';
|
||||
import invariant from 'invariant';
|
||||
import configureStore from './core/store/configureStore';
|
||||
import { Plugin } from './core/apis';
|
||||
import basename from './utils/basename';
|
||||
@ -12,13 +15,10 @@ import LanguageProvider from './components/LanguageProvider';
|
||||
import AutoReloadOverlayBlockerProvider from './components/AutoReloadOverlayBlockerProvider';
|
||||
import OverlayBlocker from './components/OverlayBlocker';
|
||||
import Fonts from './components/Fonts';
|
||||
|
||||
import GlobalStyle from './components/GlobalStyle';
|
||||
import Notifications from './components/Notifications';
|
||||
import themes from './themes';
|
||||
|
||||
// TODO
|
||||
import translations from './translations';
|
||||
import languageNativeNames from './translations/languageNativeNames';
|
||||
|
||||
window.strapi = {
|
||||
backendURL: process.env.STRAPI_ADMIN_BACKEND_URL,
|
||||
@ -32,16 +32,27 @@ const queryClient = new QueryClient({
|
||||
},
|
||||
});
|
||||
|
||||
const appLocales = Object.keys(translations);
|
||||
|
||||
class StrapiApp {
|
||||
constructor({ appPlugins, library, middlewares, reducers }) {
|
||||
constructor({ appPlugins, library, locales, middlewares, reducers }) {
|
||||
this.appLocales = ['en', ...locales.filter(loc => loc !== 'en')];
|
||||
this.appPlugins = appPlugins || {};
|
||||
this.library = library;
|
||||
this.middlewares = middlewares;
|
||||
this.plugins = {};
|
||||
this.reducers = reducers;
|
||||
this.translations = translations;
|
||||
this.translations = {};
|
||||
this.hooksDict = {};
|
||||
this.menu = [];
|
||||
this.settings = {
|
||||
global: {
|
||||
id: 'global',
|
||||
intlLabel: {
|
||||
id: 'Settings.global',
|
||||
defaultMessage: 'Global Settings',
|
||||
},
|
||||
links: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
addComponents = components => {
|
||||
@ -52,6 +63,28 @@ class StrapiApp {
|
||||
}
|
||||
};
|
||||
|
||||
addCorePluginMenuLink = link => {
|
||||
const stringifiedLink = JSON.stringify(link);
|
||||
|
||||
invariant(link.to, `link.to should be defined for ${stringifiedLink}`);
|
||||
invariant(
|
||||
typeof link.to === 'string',
|
||||
`Expected link.to to be a string instead received ${typeof link.to}`
|
||||
);
|
||||
invariant(
|
||||
['/plugins/content-manager', '/plugins/content-type-builder', '/plugins/upload'].includes(
|
||||
link.to
|
||||
),
|
||||
'This method is not available for your plugin'
|
||||
);
|
||||
invariant(
|
||||
link.intlLabel?.id && link.intlLabel?.defaultMessage,
|
||||
`link.intlLabel.id & link.intlLabel.defaultMessage for ${stringifiedLink}`
|
||||
);
|
||||
|
||||
this.menu.push(link);
|
||||
};
|
||||
|
||||
addFields = fields => {
|
||||
if (Array.isArray(fields)) {
|
||||
fields.map(field => this.library.fields.add(field));
|
||||
@ -60,6 +93,26 @@ class StrapiApp {
|
||||
}
|
||||
};
|
||||
|
||||
addMenuLink = link => {
|
||||
const stringifiedLink = JSON.stringify(link);
|
||||
|
||||
invariant(link.to, `link.to should be defined for ${stringifiedLink}`);
|
||||
invariant(
|
||||
typeof link.to === 'string',
|
||||
`Expected link.to to be a string instead received ${typeof link.to}`
|
||||
);
|
||||
invariant(
|
||||
link.intlLabel?.id && link.intlLabel?.defaultMessage,
|
||||
`link.intlLabel.id & link.intlLabel.defaultMessage for ${stringifiedLink}`
|
||||
);
|
||||
invariant(
|
||||
link.Component && typeof link.Component === 'function',
|
||||
`link.Component should be a valid React Component`
|
||||
);
|
||||
|
||||
this.menu.push(link);
|
||||
};
|
||||
|
||||
addMiddlewares = middlewares => {
|
||||
middlewares.forEach(middleware => {
|
||||
this.middlewares.add(middleware);
|
||||
@ -72,13 +125,44 @@ class StrapiApp {
|
||||
});
|
||||
};
|
||||
|
||||
addSettingsLink = (sectionId, link) => {
|
||||
invariant(this.settings[sectionId], 'The section does not exist');
|
||||
|
||||
const stringifiedLink = JSON.stringify(link);
|
||||
|
||||
invariant(link.id, `link.id should be defined for ${stringifiedLink}`);
|
||||
invariant(
|
||||
link.intlLabel?.id && link.intlLabel?.defaultMessage,
|
||||
`link.intlLabel.id & link.intlLabel.defaultMessage for ${stringifiedLink}`
|
||||
);
|
||||
invariant(link.to, `link.to should be defined for ${stringifiedLink}`);
|
||||
invariant(
|
||||
link.Component && typeof link.Component === 'function',
|
||||
`link.Component should be a valid React Component`
|
||||
);
|
||||
|
||||
this.settings[sectionId].links.push(link);
|
||||
};
|
||||
|
||||
addSettingsLinks = (sectionId, links) => {
|
||||
invariant(this.settings[sectionId], 'The section does not exist');
|
||||
invariant(Array.isArray(links), 'TypeError expected links to be an array');
|
||||
|
||||
links.forEach(link => {
|
||||
this.addSettingsLink(sectionId, link);
|
||||
});
|
||||
};
|
||||
|
||||
async initialize() {
|
||||
Object.keys(this.appPlugins).forEach(plugin => {
|
||||
this.appPlugins[plugin].register({
|
||||
addComponents: this.addComponents,
|
||||
addCorePluginMenuLink: this.addCorePluginMenuLink,
|
||||
addFields: this.addFields,
|
||||
addMenuLink: this.addMenuLink,
|
||||
addMiddlewares: this.addMiddlewares,
|
||||
addReducers: this.addReducers,
|
||||
createSettingSection: this.createSettingSection,
|
||||
registerPlugin: this.registerPlugin,
|
||||
});
|
||||
});
|
||||
@ -89,29 +173,87 @@ class StrapiApp {
|
||||
const boot = this.appPlugins[plugin].boot;
|
||||
|
||||
if (boot) {
|
||||
boot({ getPlugin: this.getPlugin });
|
||||
boot({
|
||||
addSettingsLink: this.addSettingsLink,
|
||||
addSettingsLinks: this.addSettingsLinks,
|
||||
getPlugin: this.getPlugin,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createSettingSection = (section, links) => {
|
||||
invariant(section.id, 'section.id should be defined');
|
||||
invariant(
|
||||
section.intlLabel?.id && section.intlLabel?.defaultMessage,
|
||||
'section.intlLabel should be defined'
|
||||
);
|
||||
|
||||
invariant(Array.isArray(links), 'TypeError expected links to be an array');
|
||||
invariant(this.settings[section.id] === undefined, 'A similar section already exists');
|
||||
|
||||
this.settings[section.id] = { ...section, links: [] };
|
||||
|
||||
links.forEach(link => {
|
||||
this.addSettingsLink(section.id, link);
|
||||
});
|
||||
};
|
||||
|
||||
createStore = () => {
|
||||
const store = configureStore(this.middlewares.middlewares, this.reducers.reducers);
|
||||
|
||||
return store;
|
||||
};
|
||||
|
||||
getPlugin = pluginId => {
|
||||
return this.plugins[pluginId];
|
||||
};
|
||||
|
||||
// FIXME
|
||||
registerPluginTranslations(pluginId, trads) {
|
||||
const pluginTranslations = appLocales.reduce((acc, currentLanguage) => {
|
||||
const currentLocale = trads[currentLanguage];
|
||||
async loadAdminTrads() {
|
||||
const arrayOfPromises = this.appLocales.map(locale => {
|
||||
return import(/* webpackChunkName: "[request]" */ `./translations/${locale}.json`)
|
||||
.then(({ default: data }) => {
|
||||
return { data, locale };
|
||||
})
|
||||
.catch(() => {
|
||||
return { data: {}, locale };
|
||||
});
|
||||
});
|
||||
const adminLocales = await Promise.all(arrayOfPromises);
|
||||
|
||||
if (currentLocale) {
|
||||
const localeprefixedWithPluginId = Object.keys(currentLocale).reduce((acc2, current) => {
|
||||
acc2[`${pluginId}.${current}`] = currentLocale[current];
|
||||
this.translations = adminLocales.reduce((acc, current) => {
|
||||
acc[current.locale] = current.data;
|
||||
|
||||
return acc2;
|
||||
}, {});
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
acc[currentLanguage] = localeprefixedWithPluginId;
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async loadTrads() {
|
||||
const arrayOfPromises = Object.keys(this.appPlugins)
|
||||
.map(plugin => {
|
||||
const registerTrads = this.appPlugins[plugin].registerTrads;
|
||||
|
||||
if (registerTrads) {
|
||||
return registerTrads({ locales: this.appLocales });
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter(a => a);
|
||||
|
||||
const pluginsTrads = await Promise.all(arrayOfPromises);
|
||||
const mergedTrads = pluginsTrads.reduce((acc, currentPluginTrads) => {
|
||||
const pluginTrads = currentPluginTrads.reduce((acc1, current) => {
|
||||
acc1[current.locale] = current.data;
|
||||
|
||||
return acc1;
|
||||
}, {});
|
||||
|
||||
Object.keys(pluginTrads).forEach(locale => {
|
||||
acc[locale] = { ...acc[locale], ...pluginTrads[locale] };
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
@ -119,26 +261,43 @@ class StrapiApp {
|
||||
this.translations = Object.keys(this.translations).reduce((acc, current) => {
|
||||
acc[current] = {
|
||||
...this.translations[current],
|
||||
...(pluginTranslations[current] || {}),
|
||||
...(mergedTrads[current] || {}),
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
registerPlugin = pluginConf => {
|
||||
// FIXME
|
||||
// Translations should be loaded differently
|
||||
// This is a temporary fix
|
||||
this.registerPluginTranslations(pluginConf.id, pluginConf.trads);
|
||||
|
||||
const plugin = Plugin(pluginConf);
|
||||
|
||||
this.plugins[plugin.pluginId] = plugin;
|
||||
};
|
||||
|
||||
createHook = name => {
|
||||
this.hooksDict[name] = createHook();
|
||||
};
|
||||
|
||||
registerHook = (name, fn) => {
|
||||
this.hooksDict[name].register(fn);
|
||||
};
|
||||
|
||||
runHookSeries = (name, asynchronous = false) =>
|
||||
asynchronous ? this.hooksDict[name].runSeriesAsync() : this.hooksDict[name].runSeries();
|
||||
|
||||
runHookWaterfall = (name, initialValue, asynchronous = false) =>
|
||||
asynchronous
|
||||
? this.hooksDict[name].runWaterfallAsync(initialValue)
|
||||
: this.hooksDict[name].runWaterfall(initialValue);
|
||||
|
||||
runHookParallel = name => this.hooksDict[name].runParallel();
|
||||
|
||||
render() {
|
||||
const store = configureStore(this.middlewares.middlewares, this.reducers.reducers);
|
||||
const store = this.createStore();
|
||||
const localeNames = pick(languageNativeNames, this.appLocales);
|
||||
|
||||
const {
|
||||
components: { components },
|
||||
fields: { fields },
|
||||
@ -150,9 +309,17 @@ class StrapiApp {
|
||||
<GlobalStyle />
|
||||
<Fonts />
|
||||
<Provider store={store}>
|
||||
<StrapiAppProvider getPlugin={this.getPlugin} plugins={this.plugins}>
|
||||
<StrapiAppProvider
|
||||
getPlugin={this.getPlugin}
|
||||
menu={this.menu}
|
||||
plugins={this.plugins}
|
||||
runHookParallel={this.runHookParallel}
|
||||
runHookWaterfall={this.runHookWaterfall}
|
||||
runHookSeries={this.runHookSeries}
|
||||
settings={this.settings}
|
||||
>
|
||||
<LibraryProvider components={components} fields={fields}>
|
||||
<LanguageProvider messages={this.translations}>
|
||||
<LanguageProvider messages={this.translations} localeNames={localeNames}>
|
||||
<AutoReloadOverlayBlockerProvider>
|
||||
<OverlayBlocker>
|
||||
<Notifications>
|
||||
@ -172,5 +339,5 @@ class StrapiApp {
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ appPlugins, library, middlewares, reducers }) =>
|
||||
new StrapiApp({ appPlugins, library, middlewares, reducers });
|
||||
export default ({ appPlugins, library, locales, middlewares, reducers }) =>
|
||||
new StrapiApp({ appPlugins, library, locales, middlewares, reducers });
|
||||
|
||||
7
packages/core/admin/admin/src/admin.config.js
Normal file
7
packages/core/admin/admin/src/admin.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
app: config => {
|
||||
config.locales = ['fr'];
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,65 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { LoadingIndicatorPage, AppInfosContext } from '@strapi/helper-plugin';
|
||||
import { useQueries } from 'react-query';
|
||||
import packageJSON from '../../../../package.json';
|
||||
import PluginsInitializer from '../PluginsInitializer';
|
||||
import RBACProvider from '../RBACProvider';
|
||||
import { fetchAppInfo, fetchCurrentUserPermissions, fetchStrapiLatestRelease } from './utils/api';
|
||||
import checkLatestStrapiVersion from './utils/checkLatestStrapiVersion';
|
||||
|
||||
const { STRAPI_ADMIN_UPDATE_NOTIFICATION } = process.env;
|
||||
const canFetchRelease = STRAPI_ADMIN_UPDATE_NOTIFICATION === 'true';
|
||||
const strapiVersion = packageJSON.version;
|
||||
|
||||
const AuthenticatedApp = () => {
|
||||
const [
|
||||
{ data: appInfos, status },
|
||||
{ data: tag_name, isLoading },
|
||||
{ data: permissions, status: fetchPermissionsStatus, refetch, isFetched, isFetching },
|
||||
] = useQueries([
|
||||
{ queryKey: 'app-infos', queryFn: fetchAppInfo },
|
||||
{
|
||||
queryKey: 'strapi-release',
|
||||
queryFn: fetchStrapiLatestRelease,
|
||||
enabled: canFetchRelease,
|
||||
initialData: strapiVersion,
|
||||
},
|
||||
{
|
||||
queryKey: 'admin-users-permission',
|
||||
queryFn: fetchCurrentUserPermissions,
|
||||
initialData: [],
|
||||
},
|
||||
]);
|
||||
|
||||
const shouldUpdateStrapi = useMemo(() => checkLatestStrapiVersion(strapiVersion, tag_name), [
|
||||
tag_name,
|
||||
]);
|
||||
|
||||
// We don't need to wait for the release query to be fetched before rendering the plugins
|
||||
// however, we need the appInfos and the permissions
|
||||
const shouldShowNotDependentQueriesLoader =
|
||||
(isFetching && isFetched) || status === 'loading' || fetchPermissionsStatus === 'loading';
|
||||
|
||||
const shouldShowLoader = isLoading || shouldShowNotDependentQueriesLoader;
|
||||
|
||||
if (shouldShowLoader) {
|
||||
return <LoadingIndicatorPage />;
|
||||
}
|
||||
|
||||
// TODO add error state
|
||||
if (status === 'error') {
|
||||
return <div>error...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppInfosContext.Provider
|
||||
value={{ ...appInfos, latestStrapiReleaseTag: tag_name, shouldUpdateStrapi }}
|
||||
>
|
||||
<RBACProvider permissions={permissions} refetchPermissions={refetch}>
|
||||
<PluginsInitializer />
|
||||
</RBACProvider>
|
||||
</AppInfosContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthenticatedApp;
|
||||
@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { QueryClientProvider, QueryClient } from 'react-query';
|
||||
import { fetchAppInfo, fetchCurrentUserPermissions, fetchStrapiLatestRelease } from '../utils/api';
|
||||
import packageJSON from '../../../../../package.json';
|
||||
import AuthenticatedApp from '..';
|
||||
|
||||
const strapiVersion = packageJSON.version;
|
||||
|
||||
jest.mock('../utils/api', () => ({
|
||||
fetchStrapiLatestRelease: jest.fn(),
|
||||
fetchAppInfo: jest.fn(),
|
||||
fetchCurrentUserPermissions: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../PluginsInitializer', () => () => <div>PluginsInitializer</div>);
|
||||
// eslint-disable-next-line react/prop-types
|
||||
jest.mock('../../RBACProvider', () => ({ children }) => <div>{children}</div>);
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const app = (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthenticatedApp />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
describe('Admin | components | AuthenticatedApp', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules(); // Most important - it clears the cache
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should not crash', () => {
|
||||
fetchStrapiLatestRelease.mockImplementation(() => Promise.resolve({ tag_name: strapiVersion }));
|
||||
fetchAppInfo.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
autoReload: true,
|
||||
communityEdition: false,
|
||||
currentEnvironment: 'development',
|
||||
nodeVersion: 'v14.13.1',
|
||||
strapiVersion: '3.6.0',
|
||||
})
|
||||
);
|
||||
fetchCurrentUserPermissions.mockImplementation(() => Promise.resolve([]));
|
||||
|
||||
const { container } = render(app);
|
||||
|
||||
expect(container.firstChild).toMatchInlineSnapshot(`
|
||||
.c0 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-pack: space-around;
|
||||
-webkit-justify-content: space-around;
|
||||
-ms-flex-pack: space-around;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.c0 > div {
|
||||
margin: auto;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 6px solid #f3f3f3;
|
||||
border-top: 6px solid #1c91e7;
|
||||
border-radius: 50%;
|
||||
-webkit-animation: fEWCgj 2s linear infinite;
|
||||
animation: fEWCgj 2s linear infinite;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not fetch the latest release', () => {
|
||||
render(app);
|
||||
|
||||
expect(fetchStrapiLatestRelease).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,48 @@
|
||||
import axios from 'axios';
|
||||
import axiosInstance from '../../../utils/axiosInstance';
|
||||
import packageJSON from '../../../../../package.json';
|
||||
|
||||
const strapiVersion = packageJSON.version;
|
||||
|
||||
const fetchStrapiLatestRelease = async () => {
|
||||
try {
|
||||
const {
|
||||
data: { tag_name },
|
||||
} = await axios.get('https://api.github.com/repos/strapi/strapi/releases/latest');
|
||||
|
||||
return tag_name;
|
||||
} catch (err) {
|
||||
// Don't throw an error
|
||||
return strapiVersion;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAppInfo = async () => {
|
||||
try {
|
||||
const { data, headers } = await axiosInstance.get('/admin/information');
|
||||
|
||||
if (!headers['content-type'].includes('application/json')) {
|
||||
throw new Error('Not found');
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCurrentUserPermissions = async () => {
|
||||
try {
|
||||
const { data, headers } = await axiosInstance.get('/admin/users/me/permissions');
|
||||
|
||||
if (!headers['content-type'].includes('application/json')) {
|
||||
throw new Error('Not found');
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
};
|
||||
|
||||
export { fetchAppInfo, fetchCurrentUserPermissions, fetchStrapiLatestRelease };
|
||||
@ -1,16 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* LanguageProvider actions
|
||||
*
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
import { CHANGE_LOCALE } from './constants';
|
||||
|
||||
export function changeLocale(languageLocale) {
|
||||
return {
|
||||
type: CHANGE_LOCALE,
|
||||
locale: languageLocale,
|
||||
};
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* LanguageProvider constants
|
||||
*
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE';
|
||||
@ -1,14 +0,0 @@
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { changeLocale } from '../actions';
|
||||
|
||||
const useChangeLanguage = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const changeLanguage = nextLocale => {
|
||||
dispatch(changeLocale(nextLocale));
|
||||
};
|
||||
|
||||
return changeLanguage;
|
||||
};
|
||||
|
||||
export default useChangeLanguage;
|
||||
@ -6,44 +6,45 @@
|
||||
* IntlProvider component and i18n messages (loaded from `app/translations`)
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useEffect, useReducer } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { defaultsDeep } from 'lodash';
|
||||
import { selectLocale } from './selectors';
|
||||
import defaultsDeep from 'lodash/defaultsDeep';
|
||||
import LocalesProvider from '../LocalesProvider';
|
||||
import localStorageKey from './utils/localStorageKey';
|
||||
import init from './init';
|
||||
import reducer, { initialState } from './reducer';
|
||||
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
export class LanguageProvider extends React.Component {
|
||||
render() {
|
||||
const messages = defaultsDeep(this.props.messages[this.props.locale], this.props.messages.en);
|
||||
const LanguageProvider = ({ children, localeNames, messages }) => {
|
||||
const [{ locale }, dispatch] = useReducer(reducer, initialState, () => init(localeNames));
|
||||
|
||||
return (
|
||||
<IntlProvider
|
||||
locale={this.props.locale}
|
||||
defaultLocale="en"
|
||||
messages={messages}
|
||||
textComponent="span"
|
||||
>
|
||||
{React.Children.only(this.props.children)}
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
// Set user language in local storage.
|
||||
window.localStorage.setItem(localStorageKey, locale);
|
||||
}, [locale]);
|
||||
|
||||
const changeLocale = locale => {
|
||||
dispatch({
|
||||
type: 'CHANGE_LOCALE',
|
||||
locale,
|
||||
});
|
||||
};
|
||||
|
||||
const appMessages = defaultsDeep(messages[locale], messages.en);
|
||||
|
||||
return (
|
||||
<IntlProvider locale={locale} defaultLocale="en" messages={appMessages} textComponent="span">
|
||||
<LocalesProvider changeLocale={changeLocale} localeNames={localeNames} messages={appMessages}>
|
||||
{children}
|
||||
</LocalesProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
};
|
||||
|
||||
LanguageProvider.propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
locale: PropTypes.string.isRequired,
|
||||
localeNames: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||
messages: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = createSelector(selectLocale(), locale => ({ locale }));
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
dispatch,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LanguageProvider);
|
||||
export default LanguageProvider;
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import localStorageKey from './utils/localStorageKey';
|
||||
|
||||
const init = localeNames => {
|
||||
const languageFromLocaleStorage = window.localStorage.getItem(localStorageKey);
|
||||
const appLanguage = localeNames[languageFromLocaleStorage] ? languageFromLocaleStorage : 'en';
|
||||
|
||||
return {
|
||||
locale: appLanguage,
|
||||
localeNames,
|
||||
};
|
||||
};
|
||||
|
||||
export default init;
|
||||
@ -4,47 +4,27 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import { get, includes, split } from 'lodash';
|
||||
|
||||
// Import supported languages from the translations folder
|
||||
import trads from '../../translations';
|
||||
import { CHANGE_LOCALE } from './constants';
|
||||
|
||||
const languages = Object.keys(trads);
|
||||
|
||||
// Define a key to store and get user preferences in local storage.
|
||||
const localStorageKey = 'strapi-admin-language';
|
||||
|
||||
// Detect user language.
|
||||
const userLanguage =
|
||||
window.localStorage.getItem(localStorageKey) ||
|
||||
window.navigator.language ||
|
||||
window.navigator.userLanguage;
|
||||
|
||||
let foundLanguage = includes(languages, userLanguage) && userLanguage;
|
||||
|
||||
if (!foundLanguage) {
|
||||
// Split user language in a correct format.
|
||||
const userLanguageShort = get(split(userLanguage, '-'), '0');
|
||||
|
||||
// Check that the language is included in the admin configuration.
|
||||
foundLanguage = includes(languages, userLanguageShort) && userLanguageShort;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
locale: foundLanguage || 'en',
|
||||
localeNames: { en: 'English' },
|
||||
locale: 'en',
|
||||
};
|
||||
|
||||
function languageProviderReducer(state = initialState, action) {
|
||||
const languageProviderReducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case CHANGE_LOCALE:
|
||||
// Set user language in local storage.
|
||||
window.localStorage.setItem(localStorageKey, action.locale);
|
||||
case 'CHANGE_LOCALE': {
|
||||
const { locale } = action;
|
||||
|
||||
return { ...state, locale: action.locale };
|
||||
default:
|
||||
if (!state.localeNames[locale]) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return { ...state, locale };
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default languageProviderReducer;
|
||||
export { initialState };
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
/**
|
||||
* Direct selector to the languageToggle state domain
|
||||
*/
|
||||
const selectLanguage = () => state => state.language;
|
||||
|
||||
/**
|
||||
* Select the language locale
|
||||
*/
|
||||
|
||||
const selectLocale = () => createSelector(selectLanguage(), languageState => languageState.locale);
|
||||
|
||||
const makeSelectLocale = () => createSelector(selectLocale(), locale => ({ locale }));
|
||||
|
||||
export default makeSelectLocale;
|
||||
export { selectLanguage, selectLocale };
|
||||
@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useLocalesProvider from '../../LocalesProvider/useLocalesProvider';
|
||||
import LanguageProvider from '../index';
|
||||
import en from '../../../translations/en.json';
|
||||
import fr from '../../../translations/fr.json';
|
||||
|
||||
const messages = { en, fr };
|
||||
const localeNames = { en: 'English', fr: 'Français' };
|
||||
|
||||
describe('LanguageProvider', () => {
|
||||
afterEach(() => {
|
||||
localStorage.removeItem('strapi-admin-language');
|
||||
});
|
||||
|
||||
it('should not crash', () => {
|
||||
const { container } = render(
|
||||
<LanguageProvider messages={messages} localeNames={localeNames}>
|
||||
<div>Test</div>
|
||||
</LanguageProvider>
|
||||
);
|
||||
|
||||
expect(container.firstChild).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
Test
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should change the locale and set the strapi-admin-language item in the localStorage', () => {
|
||||
const Test = () => {
|
||||
const { locale } = useIntl();
|
||||
const { changeLocale } = useLocalesProvider();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{localeNames[locale]}</h1>
|
||||
<button type="button" onClick={() => changeLocale('fr')}>
|
||||
CHANGE
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render(
|
||||
<LanguageProvider messages={messages} localeNames={localeNames}>
|
||||
<Test />
|
||||
</LanguageProvider>
|
||||
);
|
||||
|
||||
expect(localStorage.getItem('strapi-admin-language')).toEqual('en');
|
||||
|
||||
expect(screen.getByText('English')).toBeInTheDocument();
|
||||
|
||||
userEvent.click(screen.getByText('CHANGE'));
|
||||
|
||||
expect(screen.getByText('Français')).toBeInTheDocument();
|
||||
expect(localStorage.getItem('strapi-admin-language')).toEqual('fr');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,34 @@
|
||||
import init from '../init';
|
||||
|
||||
const localeNames = { en: 'English', fr: 'Français' };
|
||||
|
||||
describe('LanguageProvider | init', () => {
|
||||
afterEach(() => {
|
||||
localStorage.removeItem('strapi-admin-language');
|
||||
});
|
||||
|
||||
it('should return the language from the localStorage', () => {
|
||||
localStorage.setItem('strapi-admin-language', 'fr');
|
||||
|
||||
expect(init(localeNames)).toEqual({
|
||||
locale: 'fr',
|
||||
localeNames,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return "en" when the strapi-admin-language is not set in the locale storage', () => {
|
||||
expect(init(localeNames)).toEqual({
|
||||
locale: 'en',
|
||||
localeNames,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return "en" when the language from the local storage is not included in the localeNames', () => {
|
||||
localStorage.setItem('strapi-admin-language', 'foo');
|
||||
|
||||
expect(init(localeNames)).toEqual({
|
||||
locale: 'en',
|
||||
localeNames,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,41 @@
|
||||
import reducer, { initialState } from '../reducer';
|
||||
|
||||
describe('LanguageProvider | reducer', () => {
|
||||
let state;
|
||||
|
||||
beforeEach(() => {
|
||||
state = initialState;
|
||||
});
|
||||
|
||||
it('should return the initialState', () => {
|
||||
const action = { type: undefined };
|
||||
|
||||
expect(reducer(state, action)).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('should change the locale correctly when the locale is defined in the localeNames', () => {
|
||||
state = {
|
||||
localeNames: { en: 'English', fr: 'Français' },
|
||||
locale: 'en',
|
||||
};
|
||||
|
||||
const action = { type: 'CHANGE_LOCALE', locale: 'fr' };
|
||||
const expected = {
|
||||
localeNames: { en: 'English', fr: 'Français' },
|
||||
locale: 'fr',
|
||||
};
|
||||
|
||||
expect(reducer(state, action)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should not change the locale when the language is not defined in the localeNames', () => {
|
||||
state = {
|
||||
localeNames: { en: 'English', fr: 'Français' },
|
||||
locale: 'en',
|
||||
};
|
||||
|
||||
const action = { type: 'CHANGE_LOCALE', locale: 'foo' };
|
||||
|
||||
expect(reducer(state, action)).toEqual(state);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,3 @@
|
||||
const localStorageKey = 'strapi-admin-language';
|
||||
|
||||
export default localStorageKey;
|
||||
@ -1,24 +0,0 @@
|
||||
import {
|
||||
TOGGLE_IS_LOADING,
|
||||
SET_CT_OR_ST_LINKS,
|
||||
SET_SECTION_LINKS,
|
||||
UNSET_IS_LOADING,
|
||||
} from './constants';
|
||||
|
||||
export const setCtOrStLinks = (authorizedCtLinks, authorizedStLinks, contentTypeSchemas) => ({
|
||||
type: SET_CT_OR_ST_LINKS,
|
||||
data: { authorizedCtLinks, authorizedStLinks, contentTypeSchemas },
|
||||
});
|
||||
|
||||
export const setSectionLinks = (authorizedGeneralLinks, authorizedPluginLinks) => ({
|
||||
type: SET_SECTION_LINKS,
|
||||
data: { authorizedGeneralLinks, authorizedPluginLinks },
|
||||
});
|
||||
|
||||
export const toggleIsLoading = () => ({
|
||||
type: TOGGLE_IS_LOADING,
|
||||
});
|
||||
|
||||
export const unsetIsLoading = () => ({
|
||||
type: UNSET_IS_LOADING,
|
||||
});
|
||||
@ -5,11 +5,13 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { PropTypes } from 'prop-types';
|
||||
|
||||
import { useAppInfos } from '@strapi/helper-plugin';
|
||||
import Wrapper, { A } from './Wrapper';
|
||||
|
||||
function LeftMenuFooter({ version }) {
|
||||
function LeftMenuFooter() {
|
||||
const projectType = process.env.STRAPI_ADMIN_PROJECT_TYPE;
|
||||
const { strapiVersion } = useAppInfos();
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
@ -19,12 +21,12 @@ function LeftMenuFooter({ version }) {
|
||||
</A>
|
||||
|
||||
<A
|
||||
href={`https://github.com/strapi/strapi/releases/tag/v${version}`}
|
||||
href={`https://github.com/strapi/strapi/releases/tag/v${strapiVersion}`}
|
||||
key="github"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
v{version}
|
||||
v{strapiVersion}
|
||||
</A>
|
||||
|
||||
<A href="https://strapi.io" target="_blank" rel="noopener noreferrer">
|
||||
@ -35,8 +37,4 @@ function LeftMenuFooter({ version }) {
|
||||
);
|
||||
}
|
||||
|
||||
LeftMenuFooter.propTypes = {
|
||||
version: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default LeftMenuFooter;
|
||||
|
||||
@ -1,97 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* LeftMenuLink
|
||||
*
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { startsWith } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import styled from 'styled-components';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import en from '../../../../translations/en.json';
|
||||
import LeftMenuIcon from './LeftMenuIcon';
|
||||
import A from './A';
|
||||
import NotificationCount from './NotificationCount';
|
||||
|
||||
const LinkLabel = styled.span`
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding-right: 1rem;
|
||||
padding-left: 2.5rem;
|
||||
`;
|
||||
|
||||
// TODO: refacto this file
|
||||
const LeftMenuLinkContent = ({
|
||||
destination,
|
||||
iconName,
|
||||
label,
|
||||
location,
|
||||
notificationsCount,
|
||||
search,
|
||||
}) => {
|
||||
const isLinkActive = startsWith(
|
||||
location.pathname.replace('/admin', '').concat('/'),
|
||||
destination.concat('/')
|
||||
);
|
||||
|
||||
// Check if messageId exists in en locale to prevent warning messages
|
||||
const labelId = label.id || label;
|
||||
const content =
|
||||
en[labelId] || label.defaultMessage ? (
|
||||
<FormattedMessage
|
||||
id={labelId}
|
||||
defaultMessage={label.defaultMessage || '{label}'}
|
||||
values={{
|
||||
label: `${label.id || label}`,
|
||||
}}
|
||||
>
|
||||
{message => <LinkLabel>{message}</LinkLabel>}
|
||||
</FormattedMessage>
|
||||
) : (
|
||||
<LinkLabel>{labelId}</LinkLabel>
|
||||
);
|
||||
|
||||
// Create external or internal link.
|
||||
return destination.includes('http') ? (
|
||||
<A
|
||||
className={isLinkActive ? 'linkActive' : ''}
|
||||
href={destination}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<LeftMenuIcon icon={iconName} />
|
||||
{content}
|
||||
</A>
|
||||
) : (
|
||||
<A
|
||||
as={Link}
|
||||
className={isLinkActive ? 'linkActive' : ''}
|
||||
to={{
|
||||
pathname: destination,
|
||||
search,
|
||||
}}
|
||||
>
|
||||
<LeftMenuIcon icon={iconName} />
|
||||
{content}
|
||||
{notificationsCount > 0 && <NotificationCount count={notificationsCount} />}
|
||||
</A>
|
||||
);
|
||||
};
|
||||
|
||||
LeftMenuLinkContent.defaultProps = {
|
||||
search: null,
|
||||
};
|
||||
LeftMenuLinkContent.propTypes = {
|
||||
destination: PropTypes.string.isRequired,
|
||||
iconName: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired,
|
||||
location: PropTypes.shape({
|
||||
pathname: PropTypes.string,
|
||||
}).isRequired,
|
||||
notificationsCount: PropTypes.number.isRequired,
|
||||
search: PropTypes.string,
|
||||
};
|
||||
|
||||
export default withRouter(LeftMenuLinkContent);
|
||||
@ -1,6 +1,7 @@
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const A = styled.a`
|
||||
const Link = styled(NavLink)`
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding-top: 1rem;
|
||||
@ -30,10 +31,10 @@ const A = styled.a`
|
||||
color: ${props => props.theme.main.colors.leftMenu['link-color']};
|
||||
}
|
||||
|
||||
&.linkActive {
|
||||
&.active {
|
||||
color: white !important;
|
||||
border-left: 0.3rem solid ${props => props.theme.main.colors.strapi.blue};
|
||||
}
|
||||
`;
|
||||
|
||||
export default A;
|
||||
export default Link;
|
||||
@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const LinkLabel = styled.span`
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding-right: 1rem;
|
||||
padding-left: 2.5rem;
|
||||
`;
|
||||
|
||||
export default LinkLabel;
|
||||
@ -6,36 +6,35 @@
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import LinkLabel from './LinkLabel';
|
||||
import Link from './Link';
|
||||
import LeftMenuIcon from './LeftMenuIcon';
|
||||
import NotificationCount from './NotificationCount';
|
||||
|
||||
import LeftMenuLinkContent from './LeftMenuLinkContent';
|
||||
|
||||
const LeftMenuLink = ({ destination, iconName, label, location, notificationsCount, search }) => {
|
||||
const LeftMenuLink = ({ to, icon, intlLabel, notificationsCount }) => {
|
||||
return (
|
||||
<LeftMenuLinkContent
|
||||
destination={destination}
|
||||
iconName={iconName}
|
||||
label={label}
|
||||
location={location}
|
||||
notificationsCount={notificationsCount}
|
||||
search={search}
|
||||
/>
|
||||
<Link to={to}>
|
||||
<LeftMenuIcon icon={icon} />
|
||||
{/* TODO change with new DS */}
|
||||
<FormattedMessage {...intlLabel}>
|
||||
{message => <LinkLabel>{message}</LinkLabel>}
|
||||
</FormattedMessage>
|
||||
{notificationsCount > 0 && <NotificationCount count={notificationsCount} />}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
LeftMenuLink.propTypes = {
|
||||
destination: PropTypes.string.isRequired,
|
||||
iconName: PropTypes.string,
|
||||
label: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired,
|
||||
location: PropTypes.shape({
|
||||
pathname: PropTypes.string,
|
||||
}).isRequired,
|
||||
notificationsCount: PropTypes.number.isRequired,
|
||||
search: PropTypes.string,
|
||||
to: PropTypes.string.isRequired,
|
||||
icon: PropTypes.string,
|
||||
intlLabel: PropTypes.object.isRequired,
|
||||
notificationsCount: PropTypes.number,
|
||||
};
|
||||
|
||||
LeftMenuLink.defaultProps = {
|
||||
iconName: 'circle',
|
||||
search: null,
|
||||
icon: 'circle',
|
||||
notificationsCount: 0,
|
||||
};
|
||||
|
||||
export default LeftMenuLink;
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Search = styled.input`
|
||||
width: 100%;
|
||||
padding: 0 15px;
|
||||
outline: 0;
|
||||
font-size: 1.1rem;
|
||||
color: ${({ theme }) => theme.main.colors.white};
|
||||
`;
|
||||
|
||||
export default Search;
|
||||
@ -1,11 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const SearchButton = styled.button`
|
||||
padding: 0 10px;
|
||||
line-height: normal;
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default SearchButton;
|
||||
@ -1,11 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const SearchWrapper = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 19px;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid;
|
||||
`;
|
||||
|
||||
export default SearchWrapper;
|
||||
@ -1,81 +0,0 @@
|
||||
import React, { useState, createRef, useEffect } from 'react';
|
||||
import { camelCase } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
// TODO remove this
|
||||
import messages from './messages.json';
|
||||
import Search from './Search';
|
||||
import Title from './Title';
|
||||
import SearchButton from './SearchButton';
|
||||
import SearchWrapper from './SearchWrapper';
|
||||
|
||||
const LeftMenuLinkHeader = ({ section, searchable, setSearch, search }) => {
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
const ref = createRef();
|
||||
const { id, defaultMessage } = messages[camelCase(section)];
|
||||
|
||||
useEffect(() => {
|
||||
if (showSearch && ref.current) {
|
||||
ref.current.focus();
|
||||
}
|
||||
}, [ref, showSearch]);
|
||||
|
||||
const toggleSearch = () => {
|
||||
setShowSearch(prev => !prev);
|
||||
};
|
||||
|
||||
const handleChange = ({ target: { value } }) => {
|
||||
setSearch(value);
|
||||
};
|
||||
|
||||
const clearSearch = () => {
|
||||
setSearch('');
|
||||
setShowSearch(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Title>
|
||||
{!showSearch ? (
|
||||
<>
|
||||
<FormattedMessage id={id} defaultMessage={defaultMessage} />
|
||||
{searchable && (
|
||||
<SearchButton onClick={toggleSearch}>
|
||||
<FontAwesomeIcon icon="search" />
|
||||
</SearchButton>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<SearchWrapper>
|
||||
<div>
|
||||
<FontAwesomeIcon style={{ fontSize: 12 }} icon="search" />
|
||||
</div>
|
||||
<FormattedMessage id="components.Search.placeholder">
|
||||
{message => (
|
||||
<Search ref={ref} onChange={handleChange} value={search} placeholder={message} />
|
||||
)}
|
||||
</FormattedMessage>
|
||||
<SearchButton onClick={clearSearch}>
|
||||
<FontAwesomeIcon icon="times" />
|
||||
</SearchButton>
|
||||
</SearchWrapper>
|
||||
)}
|
||||
</Title>
|
||||
);
|
||||
};
|
||||
|
||||
LeftMenuLinkHeader.propTypes = {
|
||||
section: PropTypes.string.isRequired,
|
||||
searchable: PropTypes.bool,
|
||||
setSearch: PropTypes.func,
|
||||
search: PropTypes.string,
|
||||
};
|
||||
|
||||
LeftMenuLinkHeader.defaultProps = {
|
||||
search: null,
|
||||
searchable: false,
|
||||
setSearch: () => {},
|
||||
};
|
||||
|
||||
export default LeftMenuLinkHeader;
|
||||
@ -1,38 +0,0 @@
|
||||
{
|
||||
"collectionType": {
|
||||
"id": "app.components.LeftMenuLinkContainer.collectionTypes",
|
||||
"defaultMessage": "Collection Types"
|
||||
},
|
||||
"singleType": {
|
||||
"id": "app.components.LeftMenuLinkContainer.singleTypes",
|
||||
"defaultMessage": "Single Types"
|
||||
},
|
||||
"listPlugins": {
|
||||
"id": "app.components.LeftMenuLinkContainer.listPlugins",
|
||||
"defaultMessage": "Plugins"
|
||||
},
|
||||
"installNewPlugin": {
|
||||
"id": "app.components.LeftMenuLinkContainer.installNewPlugin",
|
||||
"defaultMessage": "Marketplace"
|
||||
},
|
||||
"configuration": {
|
||||
"id": "app.components.LeftMenuLinkContainer.configuration",
|
||||
"defaultMessage": "Configurations"
|
||||
},
|
||||
"plugins": {
|
||||
"id": "app.components.LeftMenuLinkContainer.plugins",
|
||||
"defaultMessage": "Plugins"
|
||||
},
|
||||
"general": {
|
||||
"id": "app.components.LeftMenuLinkContainer.general",
|
||||
"defaultMessage": "General"
|
||||
},
|
||||
"noPluginsInstalled": {
|
||||
"id": "app.components.LeftMenuLinkContainer.noPluginsInstalled",
|
||||
"defaultMessage": "No plugins installed yet"
|
||||
},
|
||||
"settings": {
|
||||
"id": "app.components.LeftMenuLinkContainer.settings",
|
||||
"defaultMessage": "Settings"
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const EmptyLinksList = styled.span`
|
||||
min-height: 3.6rem;
|
||||
padding: 0.6rem 1.6rem 2.6rem 0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
color: ${({ theme }) => theme.main.colors.leftMenu['link-color']};
|
||||
`;
|
||||
|
||||
EmptyLinksList.defaultProps = {
|
||||
theme: {
|
||||
main: {
|
||||
colors: {
|
||||
white: '#ffffff',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default EmptyLinksList;
|
||||
@ -1,9 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const EmptyLinksListWrapper = styled.div`
|
||||
margin-bottom: 0.1rem;
|
||||
padding: 0.8rem 0 0.7rem 2rem;
|
||||
line-height: 18px;
|
||||
`;
|
||||
|
||||
export default EmptyLinksListWrapper;
|
||||
@ -1,84 +1,22 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import matchSorter from 'match-sorter';
|
||||
import { sortBy } from 'lodash';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import LeftMenuLink from '../Link';
|
||||
import LeftMenuLinkHeader from '../LinkHeader';
|
||||
import LeftMenuListLink from './LeftMenuListLink';
|
||||
import EmptyLinksList from './EmptyLinksList';
|
||||
import EmptyLinksListWrapper from './EmptyLinksListWrapper';
|
||||
|
||||
const LeftMenuLinksSection = ({
|
||||
section,
|
||||
searchable,
|
||||
location,
|
||||
links,
|
||||
emptyLinksListMessage,
|
||||
shrink,
|
||||
}) => {
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const filteredList = sortBy(
|
||||
matchSorter(links, search, {
|
||||
keys: ['label'],
|
||||
}),
|
||||
'label'
|
||||
);
|
||||
|
||||
const LeftMenuLinksSection = ({ links }) => {
|
||||
return (
|
||||
<>
|
||||
<LeftMenuLinkHeader
|
||||
section={section}
|
||||
searchable={searchable}
|
||||
setSearch={setSearch}
|
||||
search={search}
|
||||
/>
|
||||
<LeftMenuListLink shrink={shrink}>
|
||||
{filteredList.length > 0 ? (
|
||||
filteredList.map((link, index) => (
|
||||
<LeftMenuLink
|
||||
location={location}
|
||||
// There is no id or unique value in the link object for the moment.
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={index}
|
||||
iconName={link.icon}
|
||||
label={link.label}
|
||||
destination={link.destination}
|
||||
notificationsCount={link.notificationsCount || 0}
|
||||
search={link.search}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<EmptyLinksListWrapper>
|
||||
<FormattedMessage id={emptyLinksListMessage} defaultMessage="No plugins installed yet">
|
||||
{msg => <EmptyLinksList>{msg}</EmptyLinksList>}
|
||||
</FormattedMessage>
|
||||
</EmptyLinksListWrapper>
|
||||
)}
|
||||
<LeftMenuListLink>
|
||||
{links.map(link => (
|
||||
<LeftMenuLink {...link} key={link.to} />
|
||||
))}
|
||||
</LeftMenuListLink>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
LeftMenuLinksSection.defaultProps = {
|
||||
shrink: false,
|
||||
};
|
||||
|
||||
LeftMenuLinksSection.propTypes = {
|
||||
section: PropTypes.string.isRequired,
|
||||
searchable: PropTypes.bool.isRequired,
|
||||
shrink: PropTypes.bool,
|
||||
location: PropTypes.shape({
|
||||
pathname: PropTypes.string,
|
||||
}).isRequired,
|
||||
links: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
emptyLinksListMessage: PropTypes.string,
|
||||
};
|
||||
|
||||
LeftMenuLinksSection.defaultProps = {
|
||||
emptyLinksListMessage: 'components.ListRow.empty',
|
||||
};
|
||||
|
||||
export default LeftMenuLinksSection;
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1140;
|
||||
background: ${({ theme }) => theme.main.colors.white};
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@ -1,34 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* This component is used to show a global loader while permissions are being checked
|
||||
* it prevents from lifting the state up in order to avoid setting more logic into the Admin container
|
||||
* this way we can show a global loader without modifying the Admin code
|
||||
*
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { LoadingIndicatorPage } from '@strapi/helper-plugin';
|
||||
import PropTypes from 'prop-types';
|
||||
import Wrapper from './Wrapper';
|
||||
|
||||
const MOUNT_NODE = document.getElementById('app') || document.createElement('div');
|
||||
|
||||
const Loader = ({ show }) => {
|
||||
if (show) {
|
||||
return createPortal(
|
||||
<Wrapper>
|
||||
<LoadingIndicatorPage />
|
||||
</Wrapper>,
|
||||
MOUNT_NODE
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
Loader.propTypes = {
|
||||
show: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default Loader;
|
||||
@ -1,6 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Title = styled.div`
|
||||
const SectionTitle = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-left: 2rem;
|
||||
@ -15,7 +15,7 @@ const Title = styled.div`
|
||||
max-height: 26px;
|
||||
`;
|
||||
|
||||
Title.defaultProps = {
|
||||
SectionTitle.defaultProps = {
|
||||
theme: {
|
||||
main: {
|
||||
colors: {
|
||||
@ -27,4 +27,4 @@ Title.defaultProps = {
|
||||
},
|
||||
};
|
||||
|
||||
export default Title;
|
||||
export default SectionTitle;
|
||||
@ -2,4 +2,4 @@ export { default as Footer } from './Footer';
|
||||
export { default as Header } from './Header';
|
||||
export { default as LinksContainer } from './Links';
|
||||
export { default as LinksSection } from './LinksSection';
|
||||
export { default as Loader } from './Loader';
|
||||
export { default as SectionTitle } from './SectionTitle';
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
export const SET_CT_OR_ST_LINKS = 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS';
|
||||
export const SET_SECTION_LINKS = 'StrapiAdmin/LeftMenu/SET_SECTION_LINKS';
|
||||
export const TOGGLE_IS_LOADING = 'StrapiAdmin/LeftMenu/TOGGLE_IS_LOADING';
|
||||
export const UNSET_IS_LOADING = 'StrapiAdmin/LeftMenu/UNSET_IS_LOADING';
|
||||
@ -1,96 +1,49 @@
|
||||
import React, { memo, useEffect } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
// import { LinksSection, LinksContainer } from '../LeftMenuCompos';
|
||||
import { Footer, Header, Loader, LinksContainer, LinksSection } from './compos';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Footer, Header, LinksContainer, LinksSection, SectionTitle } from './compos';
|
||||
import Wrapper from './Wrapper';
|
||||
import useMenuSections from './useMenuSections';
|
||||
|
||||
const LeftMenu = ({ shouldUpdateStrapi, version, plugins, setUpdateMenu }) => {
|
||||
const location = useLocation();
|
||||
|
||||
const {
|
||||
state: {
|
||||
isLoading,
|
||||
collectionTypesSectionLinks,
|
||||
singleTypesSectionLinks,
|
||||
generalSectionLinks,
|
||||
pluginsSectionLinks,
|
||||
},
|
||||
toggleLoading,
|
||||
generateMenu,
|
||||
} = useMenuSections(plugins, shouldUpdateStrapi);
|
||||
|
||||
const filteredCollectionTypeLinks = collectionTypesSectionLinks.filter(
|
||||
({ isDisplayed }) => isDisplayed
|
||||
);
|
||||
const filteredSingleTypeLinks = singleTypesSectionLinks.filter(({ isDisplayed }) => isDisplayed);
|
||||
|
||||
// This effect is really temporary until we create the menu api
|
||||
// We need this because we need to regenerate the links when the settings are being changed
|
||||
// in the content manager configurations list
|
||||
useEffect(() => {
|
||||
setUpdateMenu(() => {
|
||||
toggleLoading();
|
||||
generateMenu();
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Loader show={isLoading} />
|
||||
<Header />
|
||||
<LinksContainer>
|
||||
{filteredCollectionTypeLinks.length > 0 && (
|
||||
<LinksSection
|
||||
section="collectionType"
|
||||
name="collectionType"
|
||||
links={filteredCollectionTypeLinks}
|
||||
location={location}
|
||||
searchable
|
||||
/>
|
||||
)}
|
||||
{filteredSingleTypeLinks.length > 0 && (
|
||||
<LinksSection
|
||||
section="singleType"
|
||||
name="singleType"
|
||||
links={filteredSingleTypeLinks}
|
||||
location={location}
|
||||
searchable
|
||||
/>
|
||||
)}
|
||||
|
||||
{pluginsSectionLinks.length > 0 && (
|
||||
<LinksSection
|
||||
section="plugins"
|
||||
name="plugins"
|
||||
links={pluginsSectionLinks}
|
||||
location={location}
|
||||
searchable={false}
|
||||
emptyLinksListMessage="app.components.LeftMenuLinkContainer.noPluginsInstalled"
|
||||
/>
|
||||
<>
|
||||
<SectionTitle>
|
||||
<FormattedMessage
|
||||
id="app.components.LeftMenuLinkContainer.listPlugins"
|
||||
defaultMessage="Plugins"
|
||||
/>
|
||||
</SectionTitle>
|
||||
<LinksSection
|
||||
links={pluginsSectionLinks}
|
||||
searchable={false}
|
||||
emptyLinksListMessage="app.components.LeftMenuLinkContainer.noPluginsInstalled"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{generalSectionLinks.length > 0 && (
|
||||
<LinksSection
|
||||
section="general"
|
||||
name="general"
|
||||
links={generalSectionLinks}
|
||||
location={location}
|
||||
searchable={false}
|
||||
/>
|
||||
<>
|
||||
<SectionTitle>
|
||||
<FormattedMessage
|
||||
id="app.components.LeftMenuLinkContainer.general"
|
||||
defaultMessage="General"
|
||||
/>
|
||||
</SectionTitle>
|
||||
<LinksSection links={generalSectionLinks} searchable={false} />
|
||||
</>
|
||||
)}
|
||||
</LinksContainer>
|
||||
<Footer key="footer" version={version} />
|
||||
<Footer key="footer" />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
LeftMenu.propTypes = {
|
||||
shouldUpdateStrapi: PropTypes.bool.isRequired,
|
||||
version: PropTypes.string.isRequired,
|
||||
plugins: PropTypes.object.isRequired,
|
||||
setUpdateMenu: PropTypes.func.isRequired,
|
||||
generalSectionLinks: PropTypes.array.isRequired,
|
||||
pluginsSectionLinks: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default memo(LeftMenu);
|
||||
|
||||
@ -1,78 +0,0 @@
|
||||
/* eslint-disable consistent-return */
|
||||
import produce from 'immer';
|
||||
import adminPermissions from '../../permissions';
|
||||
import {
|
||||
SET_CT_OR_ST_LINKS,
|
||||
SET_SECTION_LINKS,
|
||||
TOGGLE_IS_LOADING,
|
||||
UNSET_IS_LOADING,
|
||||
} from './constants';
|
||||
|
||||
const initialState = {
|
||||
collectionTypesSectionLinks: [],
|
||||
generalSectionLinks: [
|
||||
{
|
||||
icon: 'list',
|
||||
label: 'app.components.LeftMenuLinkContainer.listPlugins',
|
||||
destination: '/list-plugins',
|
||||
isDisplayed: false,
|
||||
permissions: adminPermissions.marketplace.main,
|
||||
notificationsCount: 0,
|
||||
},
|
||||
{
|
||||
icon: 'shopping-basket',
|
||||
label: 'app.components.LeftMenuLinkContainer.installNewPlugin',
|
||||
destination: '/marketplace',
|
||||
isDisplayed: false,
|
||||
permissions: adminPermissions.marketplace.main,
|
||||
notificationsCount: 0,
|
||||
},
|
||||
{
|
||||
icon: 'cog',
|
||||
label: 'app.components.LeftMenuLinkContainer.settings',
|
||||
isDisplayed: false,
|
||||
destination: '/settings',
|
||||
// Permissions of this link are retrieved in the init phase
|
||||
// using the settings menu
|
||||
permissions: [],
|
||||
notificationsCount: 0,
|
||||
},
|
||||
],
|
||||
singleTypesSectionLinks: [],
|
||||
pluginsSectionLinks: [],
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
const reducer = (state = initialState, action) =>
|
||||
produce(state, draftState => {
|
||||
switch (action.type) {
|
||||
case SET_CT_OR_ST_LINKS: {
|
||||
const { authorizedCtLinks, authorizedStLinks } = action.data;
|
||||
draftState.collectionTypesSectionLinks = authorizedCtLinks;
|
||||
draftState.singleTypesSectionLinks = authorizedStLinks;
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_SECTION_LINKS: {
|
||||
const { authorizedGeneralLinks, authorizedPluginLinks } = action.data;
|
||||
draftState.generalSectionLinks = authorizedGeneralLinks;
|
||||
draftState.pluginsSectionLinks = authorizedPluginLinks;
|
||||
break;
|
||||
}
|
||||
|
||||
case TOGGLE_IS_LOADING: {
|
||||
draftState.isLoading = !state.isLoading;
|
||||
break;
|
||||
}
|
||||
case UNSET_IS_LOADING: {
|
||||
draftState.isLoading = false;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return draftState;
|
||||
}
|
||||
});
|
||||
|
||||
export default reducer;
|
||||
export { initialState };
|
||||
@ -1,3 +0,0 @@
|
||||
const selectMenuLinks = state => state.menu;
|
||||
|
||||
export default selectMenuLinks;
|
||||
@ -1,77 +0,0 @@
|
||||
import reducer, { initialState } from '../reducer';
|
||||
import { SET_CT_OR_ST_LINKS, SET_SECTION_LINKS, TOGGLE_IS_LOADING } from '../constants';
|
||||
|
||||
describe('ADMIN | LeftMenu | reducer', () => {
|
||||
describe('DEFAULT_ACTION', () => {
|
||||
it('should return the initialState', () => {
|
||||
const state = {
|
||||
test: true,
|
||||
};
|
||||
|
||||
expect(reducer(state, {})).toEqual(state);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TOGGLE_IS_LOADING', () => {
|
||||
it('should change the isLoading property correctly', () => {
|
||||
const state = {
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
const expected = {
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
const action = {
|
||||
type: TOGGLE_IS_LOADING,
|
||||
};
|
||||
|
||||
expect(reducer(state, action)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SET_SECTION_LINKS', () => {
|
||||
it('sets the generalSectionLinks and the pluginsSectionLinks with the action', () => {
|
||||
const state = { ...initialState };
|
||||
const action = {
|
||||
type: SET_SECTION_LINKS,
|
||||
data: {
|
||||
authorizedGeneralLinks: ['authorizd', 'links'],
|
||||
authorizedPluginLinks: ['authorizd', 'plugin-links'],
|
||||
},
|
||||
};
|
||||
|
||||
const expected = {
|
||||
...initialState,
|
||||
generalSectionLinks: ['authorizd', 'links'],
|
||||
pluginsSectionLinks: ['authorizd', 'plugin-links'],
|
||||
};
|
||||
const actual = reducer(state, action);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SET_CT_OR_ST_LINKS', () => {
|
||||
it('sets the generalSectionLinks and the pluginsSectionLinks with the action', () => {
|
||||
const state = { ...initialState };
|
||||
const action = {
|
||||
type: SET_CT_OR_ST_LINKS,
|
||||
data: {
|
||||
authorizedCtLinks: ['authorizd', 'ct-links'],
|
||||
authorizedStLinks: ['authorizd', 'st-links'],
|
||||
},
|
||||
};
|
||||
|
||||
const expected = {
|
||||
...initialState,
|
||||
collectionTypesSectionLinks: ['authorizd', 'ct-links'],
|
||||
singleTypesSectionLinks: ['authorizd', 'st-links'],
|
||||
};
|
||||
|
||||
const actual = reducer(state, action);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,65 +0,0 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useUser, useNotification } from '@strapi/helper-plugin';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import getCtOrStLinks from './utils/getCtOrStLinks';
|
||||
import getPluginSectionLinks from './utils/getPluginSectionLinks';
|
||||
import getGeneralLinks from './utils/getGeneralLinks';
|
||||
import { setCtOrStLinks, setSectionLinks, toggleIsLoading, unsetIsLoading } from './actions';
|
||||
import useSettingsMenu from '../../hooks/useSettingsMenu';
|
||||
import toPluginLinks from './utils/toPluginLinks';
|
||||
import selectMenuLinks from './selectors';
|
||||
|
||||
const useMenuSections = (plugins, shouldUpdateStrapi) => {
|
||||
const toggleNotification = useNotification();
|
||||
const state = useSelector(selectMenuLinks);
|
||||
const dispatch = useDispatch();
|
||||
const { userPermissions } = useUser();
|
||||
const { menu: settingsMenu } = useSettingsMenu(true);
|
||||
// We are using a ref because we don't want our effect to have this in its dependencies array
|
||||
const generalSectionLinksRef = useRef(state.generalSectionLinks);
|
||||
const shouldUpdateStrapiRef = useRef(shouldUpdateStrapi);
|
||||
// Since the settingsMenu is not managing any state because of the true argument we can use a ref here
|
||||
// so we don't need to add it to the effect dependencies array
|
||||
const settingsMenuRef = useRef(settingsMenu);
|
||||
// Once in the app lifecycle the plugins should not be added into any dependencies array, in order to prevent
|
||||
// the effect to be run when another plugin is using one plugins internal api for instance
|
||||
// so it's definitely ok to use a ref here
|
||||
const pluginsRef = useRef(plugins);
|
||||
|
||||
const toggleLoading = () => dispatch(toggleIsLoading());
|
||||
|
||||
const resolvePermissions = async (permissions = userPermissions) => {
|
||||
const pluginsSectionLinks = toPluginLinks(pluginsRef.current);
|
||||
const { authorizedCtLinks, authorizedStLinks, contentTypes } = await getCtOrStLinks(
|
||||
permissions,
|
||||
toggleNotification
|
||||
);
|
||||
|
||||
const authorizedPluginSectionLinks = await getPluginSectionLinks(
|
||||
permissions,
|
||||
pluginsSectionLinks
|
||||
);
|
||||
|
||||
const authorizedGeneralSectionLinks = await getGeneralLinks(
|
||||
permissions,
|
||||
generalSectionLinksRef.current,
|
||||
settingsMenuRef.current,
|
||||
shouldUpdateStrapiRef.current
|
||||
);
|
||||
|
||||
dispatch(setCtOrStLinks(authorizedCtLinks, authorizedStLinks, contentTypes));
|
||||
dispatch(setSectionLinks(authorizedGeneralSectionLinks, authorizedPluginSectionLinks));
|
||||
dispatch(unsetIsLoading());
|
||||
};
|
||||
|
||||
const resolvePermissionsRef = useRef(resolvePermissions);
|
||||
|
||||
useEffect(() => {
|
||||
resolvePermissionsRef.current(userPermissions);
|
||||
}, [userPermissions, dispatch]);
|
||||
|
||||
return { state, generateMenu: resolvePermissionsRef.current, toggleLoading };
|
||||
};
|
||||
|
||||
export default useMenuSections;
|
||||
@ -1,20 +0,0 @@
|
||||
import { get, isEmpty } from 'lodash';
|
||||
|
||||
const getSettingsMenuLinksPermissions = menu =>
|
||||
menu.reduce((acc, current) => {
|
||||
const links = get(current, 'links', []);
|
||||
|
||||
const permissions = links.reduce((acc, current) => {
|
||||
let currentPermissions = get(current, 'permissions', null);
|
||||
|
||||
if (isEmpty(currentPermissions)) {
|
||||
return [...acc, null];
|
||||
}
|
||||
|
||||
return [...acc, ...currentPermissions];
|
||||
}, []);
|
||||
|
||||
return [...acc, ...permissions];
|
||||
}, []);
|
||||
|
||||
export default getSettingsMenuLinksPermissions;
|
||||
@ -1,2 +0,0 @@
|
||||
export { default as generateModelsLinks } from './generateModelsLinks';
|
||||
export { default as getSettingsMenuLinksPermissions } from './getSettingsMenuLinksPermissions';
|
||||
@ -1,549 +0,0 @@
|
||||
import { hasPermissions } from '@strapi/helper-plugin';
|
||||
import getGeneralLinks from '../getGeneralLinks';
|
||||
|
||||
jest.mock('@strapi/helper-plugin');
|
||||
|
||||
describe('getGeneralLinks', () => {
|
||||
it('resolves valid general links from real data', async () => {
|
||||
hasPermissions.mockImplementation(() => Promise.resolve(true));
|
||||
|
||||
const permissions = [
|
||||
{
|
||||
id: 458,
|
||||
action: 'plugins::i18n.locale.read',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 459,
|
||||
action: 'plugins::content-manager.explorer.create',
|
||||
subject: 'application::article.article',
|
||||
properties: {
|
||||
fields: ['Name'],
|
||||
locales: ['en'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 460,
|
||||
action: 'plugins::content-manager.explorer.read',
|
||||
subject: 'application::article.article',
|
||||
properties: {
|
||||
fields: ['Name'],
|
||||
locales: ['en'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 461,
|
||||
action: 'plugins::content-manager.explorer.read',
|
||||
subject: 'application::article.article',
|
||||
properties: {
|
||||
fields: ['Name'],
|
||||
locales: ['fr-FR'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 462,
|
||||
action: 'plugins::content-manager.explorer.update',
|
||||
subject: 'application::article.article',
|
||||
properties: {
|
||||
fields: ['Name'],
|
||||
locales: ['fr-FR'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
];
|
||||
const generalSectionRawLinks = [
|
||||
{
|
||||
icon: 'list',
|
||||
label: 'app.components.LeftMenuLinkContainer.listPlugins',
|
||||
destination: '/list-plugins',
|
||||
isDisplayed: false,
|
||||
permissions: [
|
||||
{
|
||||
action: 'admin::marketplace.read',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::marketplace.plugins.install',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::marketplace.plugins.uninstall',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
notificationsCount: 0,
|
||||
},
|
||||
{
|
||||
icon: 'shopping-basket',
|
||||
label: 'app.components.LeftMenuLinkContainer.installNewPlugin',
|
||||
destination: '/marketplace',
|
||||
isDisplayed: false,
|
||||
permissions: [
|
||||
{
|
||||
action: 'admin::marketplace.read',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::marketplace.plugins.install',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::marketplace.plugins.uninstall',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
notificationsCount: 0,
|
||||
},
|
||||
{
|
||||
icon: 'cog',
|
||||
label: 'app.components.LeftMenuLinkContainer.settings',
|
||||
isDisplayed: false,
|
||||
destination: '/settings',
|
||||
permissions: [],
|
||||
notificationsCount: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const settingsMenu = [
|
||||
{
|
||||
id: 'global',
|
||||
title: {
|
||||
id: 'Settings.global',
|
||||
},
|
||||
links: [
|
||||
{
|
||||
title: {
|
||||
id: 'i18n.plugin.name',
|
||||
defaultMessage: 'Internationalization',
|
||||
},
|
||||
name: 'internationalization',
|
||||
to: '/settings/internationalization',
|
||||
permissions: [
|
||||
{
|
||||
action: 'plugins::i18n.locale.read',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'plugins::i18n.locale.create',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
isDisplayed: false,
|
||||
},
|
||||
{
|
||||
title: {
|
||||
id: 'upload.plugin.name',
|
||||
defaultMessage: 'Media Library',
|
||||
},
|
||||
name: 'media-library',
|
||||
to: '/settings/media-library',
|
||||
permissions: [
|
||||
{
|
||||
action: 'plugins::upload.settings.read',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
isDisplayed: false,
|
||||
},
|
||||
{
|
||||
title: {
|
||||
id: 'Settings.sso.title',
|
||||
},
|
||||
to: '/settings/single-sign-on',
|
||||
name: 'sso',
|
||||
isDisplayed: false,
|
||||
permissions: [
|
||||
{
|
||||
action: 'admin::provider-login.read',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: {
|
||||
id: 'Settings.webhooks.title',
|
||||
},
|
||||
to: '/settings/webhooks',
|
||||
name: 'webhooks',
|
||||
isDisplayed: false,
|
||||
permissions: [
|
||||
{
|
||||
action: 'admin::webhooks.create',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::webhooks.read',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::webhooks.update',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::webhooks.delete',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'permissions',
|
||||
title: 'Settings.permissions',
|
||||
links: [
|
||||
{
|
||||
title: {
|
||||
id: 'Settings.permissions.menu.link.roles.label',
|
||||
},
|
||||
to: '/settings/roles',
|
||||
name: 'roles',
|
||||
isDisplayed: false,
|
||||
permissions: [
|
||||
{
|
||||
action: 'admin::roles.create',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::roles.update',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::roles.read',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::roles.delete',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: {
|
||||
id: 'Settings.permissions.menu.link.users.label',
|
||||
},
|
||||
to: '/settings/users?pageSize=10&page=1&_sort=firstname%3AASC',
|
||||
name: 'users',
|
||||
isDisplayed: false,
|
||||
permissions: [
|
||||
{
|
||||
action: 'admin::users.create',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::users.read',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::users.update',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::users.delete',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{
|
||||
icon: 'list',
|
||||
label: 'app.components.LeftMenuLinkContainer.listPlugins',
|
||||
destination: '/list-plugins',
|
||||
isDisplayed: false,
|
||||
permissions: [
|
||||
{
|
||||
action: 'admin::marketplace.read',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::marketplace.plugins.install',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::marketplace.plugins.uninstall',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
notificationsCount: 0,
|
||||
},
|
||||
{
|
||||
icon: 'shopping-basket',
|
||||
label: 'app.components.LeftMenuLinkContainer.installNewPlugin',
|
||||
destination: '/marketplace',
|
||||
isDisplayed: false,
|
||||
permissions: [
|
||||
{
|
||||
action: 'admin::marketplace.read',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::marketplace.plugins.install',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::marketplace.plugins.uninstall',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
notificationsCount: 0,
|
||||
},
|
||||
{
|
||||
icon: 'cog',
|
||||
label: 'app.components.LeftMenuLinkContainer.settings',
|
||||
isDisplayed: false,
|
||||
destination: '/settings',
|
||||
permissions: [
|
||||
{
|
||||
action: 'plugins::i18n.locale.read',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'plugins::i18n.locale.create',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'plugins::upload.settings.read',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::provider-login.read',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::webhooks.create',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::webhooks.read',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::webhooks.update',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::webhooks.delete',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::roles.create',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::roles.update',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::roles.read',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::roles.delete',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::users.create',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::users.read',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::users.update',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'admin::users.delete',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
notificationsCount: 0,
|
||||
notificationCount: 0,
|
||||
},
|
||||
];
|
||||
const actual = await getGeneralLinks(permissions, generalSectionRawLinks, settingsMenu, false);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('resolves an empty array when the destination (/settings) is not in the authorized links', async () => {
|
||||
hasPermissions.mockImplementation(() => Promise.resolve(false));
|
||||
|
||||
const permissions = [
|
||||
{
|
||||
id: 458,
|
||||
action: 'plugins::i18n.locale.read',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 459,
|
||||
action: 'plugins::content-manager.explorer.create',
|
||||
subject: 'application::article.article',
|
||||
properties: {
|
||||
fields: ['Name'],
|
||||
locales: ['en'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 460,
|
||||
action: 'plugins::content-manager.explorer.read',
|
||||
subject: 'application::article.article',
|
||||
properties: {
|
||||
fields: ['Name'],
|
||||
locales: ['en'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 461,
|
||||
action: 'plugins::content-manager.explorer.read',
|
||||
subject: 'application::article.article',
|
||||
properties: {
|
||||
fields: ['Name'],
|
||||
locales: ['fr-FR'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 462,
|
||||
action: 'plugins::content-manager.explorer.update',
|
||||
subject: 'application::article.article',
|
||||
properties: {
|
||||
fields: ['Name'],
|
||||
locales: ['fr-FR'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
];
|
||||
const generalSectionRawLinks = [
|
||||
{
|
||||
icon: 'list',
|
||||
label: 'app.components.LeftMenuLinkContainer.listPlugins',
|
||||
destination: '/list-plugins',
|
||||
isDisplayed: false,
|
||||
permissions: [],
|
||||
notificationsCount: 0,
|
||||
},
|
||||
{
|
||||
icon: 'shopping-basket',
|
||||
label: 'app.components.LeftMenuLinkContainer.installNewPlugin',
|
||||
destination: '/marketplace',
|
||||
isDisplayed: false,
|
||||
permissions: [],
|
||||
notificationsCount: 0,
|
||||
},
|
||||
{
|
||||
icon: 'cog',
|
||||
label: 'app.components.LeftMenuLinkContainer.settings',
|
||||
isDisplayed: false,
|
||||
destination: '/settings',
|
||||
permissions: [],
|
||||
notificationsCount: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const settingsMenu = [
|
||||
{
|
||||
id: 'global',
|
||||
title: {
|
||||
id: 'Settings.global',
|
||||
},
|
||||
links: [
|
||||
{
|
||||
title: {
|
||||
id: 'i18n.plugin.name',
|
||||
defaultMessage: 'Internationalization',
|
||||
},
|
||||
name: 'internationalization',
|
||||
to: '/settings/internationalization',
|
||||
permissions: [
|
||||
{
|
||||
action: 'plugins::i18n.locale.read',
|
||||
subject: null,
|
||||
},
|
||||
{
|
||||
action: 'plugins::i18n.locale.create',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
isDisplayed: false,
|
||||
},
|
||||
{
|
||||
title: {
|
||||
id: 'upload.plugin.name',
|
||||
defaultMessage: 'Media Library',
|
||||
},
|
||||
name: 'media-library',
|
||||
to: '/settings/media-library',
|
||||
permissions: [
|
||||
{
|
||||
action: 'plugins::upload.settings.read',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
isDisplayed: false,
|
||||
},
|
||||
{
|
||||
title: {
|
||||
id: 'Settings.sso.title',
|
||||
},
|
||||
to: '/settings/single-sign-on',
|
||||
name: 'sso',
|
||||
isDisplayed: false,
|
||||
permissions: [],
|
||||
},
|
||||
{
|
||||
title: {
|
||||
id: 'Settings.webhooks.title',
|
||||
},
|
||||
to: '/settings/webhooks',
|
||||
name: 'webhooks',
|
||||
isDisplayed: false,
|
||||
permissions: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'permissions',
|
||||
title: 'Settings.permissions',
|
||||
links: [
|
||||
{
|
||||
title: {
|
||||
id: 'Settings.permissions.menu.link.roles.label',
|
||||
},
|
||||
to: '/settings/roles',
|
||||
name: 'roles',
|
||||
isDisplayed: false,
|
||||
permissions: [],
|
||||
},
|
||||
{
|
||||
title: {
|
||||
id: 'Settings.permissions.menu.link.users.label',
|
||||
},
|
||||
to: '/settings/users?pageSize=10&page=1&_sort=firstname%3AASC',
|
||||
name: 'users',
|
||||
isDisplayed: false,
|
||||
permissions: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [];
|
||||
const actual = await getGeneralLinks(permissions, generalSectionRawLinks, settingsMenu, false);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@ -1,86 +0,0 @@
|
||||
import getSettingsMenuLinksPermissions from '../getSettingsMenuLinksPermissions';
|
||||
|
||||
describe('ADMIN | LeftMenu | utils | getSettingsMenuLinksPermissions', () => {
|
||||
it('should return an array containing all the permissions of each link', () => {
|
||||
const data = [
|
||||
{
|
||||
id: 'global',
|
||||
title: { id: 'Settings.global' },
|
||||
links: [
|
||||
{
|
||||
title: 'Settings.webhooks.title',
|
||||
to: '/settings/webhooks',
|
||||
name: 'webhooks',
|
||||
permissions: [
|
||||
{ action: 'admin::webhook.create', subject: null },
|
||||
{ action: 'admin::webhook.read', subject: null },
|
||||
{ action: 'admin::webhook.update', subject: null },
|
||||
{ action: 'admin::webhook.delete', subject: null },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'media-library',
|
||||
permissions: [
|
||||
{
|
||||
action: 'plugins::upload.settings.read',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
title: { id: 'upload.plugin.name', defaultMessage: 'Media Library' },
|
||||
to: '/settings/media-library',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'permissions',
|
||||
title: 'Settings.permissions',
|
||||
links: [
|
||||
{
|
||||
title: 'roles',
|
||||
to: '/settings/roles',
|
||||
name: 'roles',
|
||||
permissions: [
|
||||
{ action: 'admin::roles.create', subject: null },
|
||||
{ action: 'admin::roles.update', subject: null },
|
||||
{ action: 'admin::roles.read', subject: null },
|
||||
{ action: 'admin::roles.delete', subject: null },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'users',
|
||||
|
||||
to: '/settings/users?pageSize=10&page=1&_sort=firstname%3AASC',
|
||||
name: 'users',
|
||||
permissions: [
|
||||
{ action: 'admin::users.create', subject: null },
|
||||
{ action: 'admin::users.read', subject: null },
|
||||
{ action: 'admin::users.update', subject: null },
|
||||
{ action: 'admin::users.delete', subject: null },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{ action: 'admin::webhook.create', subject: null },
|
||||
{ action: 'admin::webhook.read', subject: null },
|
||||
{ action: 'admin::webhook.update', subject: null },
|
||||
{ action: 'admin::webhook.delete', subject: null },
|
||||
{
|
||||
action: 'plugins::upload.settings.read',
|
||||
subject: null,
|
||||
},
|
||||
{ action: 'admin::roles.create', subject: null },
|
||||
{ action: 'admin::roles.update', subject: null },
|
||||
{ action: 'admin::roles.read', subject: null },
|
||||
{ action: 'admin::roles.delete', subject: null },
|
||||
{ action: 'admin::users.create', subject: null },
|
||||
{ action: 'admin::users.read', subject: null },
|
||||
{ action: 'admin::users.update', subject: null },
|
||||
{ action: 'admin::users.delete', subject: null },
|
||||
];
|
||||
|
||||
expect(getSettingsMenuLinksPermissions(data)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@ -1,57 +0,0 @@
|
||||
import toPluginLinks from '../toPluginLinks';
|
||||
|
||||
describe('toPluginLinks', () => {
|
||||
it('transforms a plugin object into an array of plugin page links', async () => {
|
||||
const plugins = [
|
||||
{
|
||||
id: 'content-type-builder',
|
||||
description: 'content-type-builder.plugin.description',
|
||||
name: 'Content Type Builder',
|
||||
menu: {
|
||||
pluginsSectionLinks: [
|
||||
{
|
||||
destination: '/plugins/content-type-builder',
|
||||
icon: 'paint-brush',
|
||||
label: {
|
||||
id: 'content-type-builder.plugin.name',
|
||||
defaultMessage: 'Content-Types Builder',
|
||||
},
|
||||
name: 'Content Type Builder',
|
||||
permissions: [
|
||||
{
|
||||
action: 'plugins::content-type-builder.read',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'content-manager',
|
||||
description: 'content-manager.plugin.description',
|
||||
name: 'Content Manager',
|
||||
},
|
||||
];
|
||||
|
||||
const expected = [
|
||||
{
|
||||
destination: '/plugins/content-type-builder',
|
||||
icon: 'paint-brush',
|
||||
label: {
|
||||
id: 'content-type-builder.plugin.name',
|
||||
defaultMessage: 'Content-Types Builder',
|
||||
},
|
||||
permissions: [
|
||||
{
|
||||
action: 'plugins::content-type-builder.read',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const actual = toPluginLinks(plugins);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@ -1,19 +0,0 @@
|
||||
import get from 'lodash/get';
|
||||
import omit from 'lodash/omit';
|
||||
import sortLinks from '../../../utils/sortLinks';
|
||||
|
||||
const toPluginLinks = plugins => {
|
||||
const pluginsLinks = Object.values(plugins).reduce((acc, current) => {
|
||||
const pluginsSectionLinks = get(current, 'menu.pluginsSectionLinks', []);
|
||||
|
||||
return [...acc, ...pluginsSectionLinks];
|
||||
}, []);
|
||||
|
||||
const sortedLinks = sortLinks(pluginsLinks).map(link => {
|
||||
return { ...omit(link, 'name') };
|
||||
});
|
||||
|
||||
return sortedLinks;
|
||||
};
|
||||
|
||||
export default toPluginLinks;
|
||||
@ -4,76 +4,42 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import { bindActionCreators, compose } from 'redux';
|
||||
import cn from 'classnames';
|
||||
import React, { useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { ButtonDropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
|
||||
import translationMessages, { languageNativeNames } from '../../translations';
|
||||
import makeSelectLocale from '../LanguageProvider/selectors';
|
||||
import { changeLocale } from '../LanguageProvider/actions';
|
||||
import useLocalesProvider from '../LocalesProvider/useLocalesProvider';
|
||||
import Wrapper from './Wrapper';
|
||||
|
||||
// TODO
|
||||
const languages = Object.keys(translationMessages);
|
||||
export class LocaleToggle extends React.Component {
|
||||
// eslint-disable-line
|
||||
state = { isOpen: false };
|
||||
const LocaleToggle = () => {
|
||||
const { changeLocale, localeNames } = useLocalesProvider();
|
||||
|
||||
toggle = () => this.setState(prevState => ({ isOpen: !prevState.isOpen }));
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const toggle = () => setIsOpen(prev => !prev);
|
||||
const { locale } = useIntl();
|
||||
|
||||
render() {
|
||||
const {
|
||||
currentLocale: { locale },
|
||||
} = this.props;
|
||||
return (
|
||||
<Wrapper>
|
||||
<ButtonDropdown isOpen={isOpen} toggle={toggle}>
|
||||
<DropdownToggle className="localeDropdownContent">
|
||||
<span>{localeNames[locale]}</span>
|
||||
</DropdownToggle>
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<ButtonDropdown isOpen={this.state.isOpen} toggle={this.toggle}>
|
||||
<DropdownToggle className="localeDropdownContent">
|
||||
<span>{languageNativeNames[locale]}</span>
|
||||
</DropdownToggle>
|
||||
|
||||
<DropdownMenu className="localeDropdownMenu">
|
||||
{languages.map(language => (
|
||||
<DropdownMenu className="localeDropdownMenu">
|
||||
{Object.keys(localeNames).map(lang => {
|
||||
return (
|
||||
<DropdownItem
|
||||
key={language}
|
||||
onClick={() => this.props.changeLocale(language)}
|
||||
className={cn(
|
||||
'localeToggleItem',
|
||||
locale === language ? 'localeToggleItemActive' : ''
|
||||
)}
|
||||
key={lang}
|
||||
onClick={() => changeLocale(lang)}
|
||||
className={`localeToggleItem ${locale === lang ? 'localeToggleItemActive' : ''}`}
|
||||
>
|
||||
{languageNativeNames[language]}
|
||||
{localeNames[lang]}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</ButtonDropdown>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LocaleToggle.propTypes = {
|
||||
changeLocale: PropTypes.func.isRequired,
|
||||
currentLocale: PropTypes.object.isRequired,
|
||||
);
|
||||
})}
|
||||
</DropdownMenu>
|
||||
</ButtonDropdown>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentLocale: makeSelectLocale(),
|
||||
});
|
||||
|
||||
export function mapDispatchToProps(dispatch) {
|
||||
return bindActionCreators(
|
||||
{
|
||||
changeLocale,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
}
|
||||
|
||||
const withConnect = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export default compose(withConnect)(LocaleToggle);
|
||||
export default LocaleToggle;
|
||||
|
||||
@ -1,61 +1,217 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { DropdownItem } from 'reactstrap';
|
||||
import { changeLocale } from '../../LanguageProvider/actions';
|
||||
import { LocaleToggle, mapDispatchToProps } from '../index';
|
||||
import { render } from '@testing-library/react';
|
||||
import LanguageProvider from '../../LanguageProvider';
|
||||
import en from '../../../translations/en.json';
|
||||
import LocaleToggle from '../index';
|
||||
|
||||
const messages = { en };
|
||||
const localeNames = { en: 'English' };
|
||||
|
||||
describe('<LocaleToggle />', () => {
|
||||
let props;
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
changeLocale: jest.fn(),
|
||||
currentLocale: {
|
||||
locale: 'en',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('should not crash', () => {
|
||||
shallow(<LocaleToggle {...props} />);
|
||||
});
|
||||
const App = (
|
||||
<LanguageProvider messages={messages} localeNames={localeNames}>
|
||||
<LocaleToggle />
|
||||
</LanguageProvider>
|
||||
);
|
||||
|
||||
describe('<LocaleToggle />, toggle instance', () => {
|
||||
it('should update the state when called', () => {
|
||||
const renderedComponent = shallow(<LocaleToggle {...props} />);
|
||||
const { toggle } = renderedComponent.instance();
|
||||
const { container } = render(App);
|
||||
expect(container.firstChild).toMatchInlineSnapshot(`
|
||||
.c0 {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
toggle();
|
||||
.c0 > div {
|
||||
height: 6rem;
|
||||
line-height: 5.8rem;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
expect(renderedComponent.state('isOpen')).toBe(true);
|
||||
});
|
||||
.c0 > div > button {
|
||||
width: 100%;
|
||||
padding: 0 30px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
color: #333740;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
-webkit-transition: background 0.2s ease-out;
|
||||
transition: background 0.2s ease-out;
|
||||
}
|
||||
|
||||
it('call the toggle handle on click', () => {
|
||||
const renderedComponent = shallow(<LocaleToggle {...props} />);
|
||||
renderedComponent.setState({ isOpen: true });
|
||||
const dropDown = renderedComponent.find(DropdownItem).at(0);
|
||||
dropDown.simulate('click');
|
||||
.c0 > div > button:hover,
|
||||
.c0 > div > button:focus,
|
||||
.c0 > div > button:active {
|
||||
color: #333740;
|
||||
background-color: #fafafb !important;
|
||||
}
|
||||
|
||||
expect(props.changeLocale).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
.c0 > div > button > i,
|
||||
.c0 > div > button > svg {
|
||||
margin-left: 10px;
|
||||
-webkit-transition: -webkit-transform 0.3s ease-out;
|
||||
-webkit-transition: transform 0.3s ease-out;
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
|
||||
describe('<LocaleToggle />, mapDispatchToProps', () => {
|
||||
describe('changeLocale', () => {
|
||||
it('should be injected', () => {
|
||||
const dispatch = jest.fn();
|
||||
const result = mapDispatchToProps(dispatch);
|
||||
.c0 > div > button > i[alt='true'],
|
||||
.c0 > div > button > svg[alt='true'] {
|
||||
-webkit-transform: rotateX(180deg);
|
||||
-ms-transform: rotateX(180deg);
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
|
||||
expect(result.changeLocale).toBeDefined();
|
||||
});
|
||||
.c0 .localeDropdownContent {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
it('should dispatch the changeLocale action when called', () => {
|
||||
const dispatch = jest.fn();
|
||||
const result = mapDispatchToProps(dispatch);
|
||||
result.changeLocale();
|
||||
.c0 .localeDropdownContent span {
|
||||
color: #333740;
|
||||
font-size: 13px;
|
||||
font-family: Lato;
|
||||
font-weight: 500;
|
||||
-webkit-letter-spacing: 0.5;
|
||||
-moz-letter-spacing: 0.5;
|
||||
-ms-letter-spacing: 0.5;
|
||||
letter-spacing: 0.5;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith(changeLocale());
|
||||
});
|
||||
});
|
||||
.c0 .localeDropdownMenu {
|
||||
min-width: 90px !important;
|
||||
max-height: 162px !important;
|
||||
overflow: auto !important;
|
||||
margin: 0 !important;
|
||||
padding: 0;
|
||||
line-height: 1.8rem;
|
||||
border: none !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
border-top-right-radius: 0 !important;
|
||||
box-shadow: 0 1px 4px 0px rgba(40,42,49,0.05);
|
||||
}
|
||||
|
||||
.c0 .localeDropdownMenu:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
left: -1px;
|
||||
width: calc(100% + 1px);
|
||||
height: 3px;
|
||||
box-shadow: 0 1px 2px 0 rgba(40,42,49,0.16);
|
||||
}
|
||||
|
||||
.c0 .localeDropdownMenu > button {
|
||||
height: 40px;
|
||||
padding: 0px 15px;
|
||||
line-height: 40px;
|
||||
color: #f75b1d;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
-webkit-letter-spacing: 0.5;
|
||||
-moz-letter-spacing: 0.5;
|
||||
-ms-letter-spacing: 0.5;
|
||||
letter-spacing: 0.5;
|
||||
}
|
||||
|
||||
.c0 .localeDropdownMenu > button:hover,
|
||||
.c0 .localeDropdownMenu > button:focus,
|
||||
.c0 .localeDropdownMenu > button:active {
|
||||
background-color: #fafafb !important;
|
||||
border-radius: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.c0 .localeDropdownMenu > button:first-child {
|
||||
line-height: 50px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.c0 .localeDropdownMenu > button:first-child:hover,
|
||||
.c0 .localeDropdownMenu > button:first-child:active {
|
||||
color: #333740;
|
||||
}
|
||||
|
||||
.c0 .localeDropdownMenu > button:not(:first-child) {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.c0 .localeDropdownMenu > button:not(:first-child) > i,
|
||||
.c0 .localeDropdownMenu > button:not(:first-child) > svg {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.c0 .localeDropdownMenuNotLogged {
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
border: 1px solid #e3e9f3 !important;
|
||||
border-top: 0px !important;
|
||||
}
|
||||
|
||||
.c0 .localeDropdownMenuNotLogged button {
|
||||
padding-left: 17px;
|
||||
}
|
||||
|
||||
.c0 .localeDropdownMenuNotLogged button:hover {
|
||||
background-color: #f7f8f8 !important;
|
||||
}
|
||||
|
||||
.c0 .localeDropdownMenuNotLogged:before {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.c0 .localeToggleItem img {
|
||||
max-height: 13.37px;
|
||||
margin-left: 9px;
|
||||
}
|
||||
|
||||
.c0 .localeToggleItem:active {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.c0 .localeToggleItem:hover {
|
||||
background-color: #fafafb !important;
|
||||
}
|
||||
|
||||
.c0 .localeToggleItemActive {
|
||||
color: #333740 !important;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
<div
|
||||
class="btn-group"
|
||||
>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
class="localeDropdownContent btn btn-secondary"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
English
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="localeDropdownMenu dropdown-menu"
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
>
|
||||
<button
|
||||
class="localeToggleItem localeToggleItemActive dropdown-item"
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
English
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
const LocalesProviderContext = createContext();
|
||||
|
||||
export default LocalesProviderContext;
|
||||
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import LocalesProviderContext from './context';
|
||||
|
||||
const LocalesProvider = ({ changeLocale, children, localeNames, messages }) => {
|
||||
return (
|
||||
<LocalesProviderContext.Provider value={{ changeLocale, localeNames, messages }}>
|
||||
{children}
|
||||
</LocalesProviderContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
LocalesProvider.propTypes = {
|
||||
changeLocale: PropTypes.func.isRequired,
|
||||
children: PropTypes.element.isRequired,
|
||||
localeNames: PropTypes.object.isRequired,
|
||||
messages: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default LocalesProvider;
|
||||
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import LocalesProvider from '../index';
|
||||
|
||||
describe('LocalesProvider', () => {
|
||||
it('should not crash', () => {
|
||||
const { container } = render(
|
||||
<LocalesProvider
|
||||
changeLocale={jest.fn()}
|
||||
localeNames={{ en: 'English' }}
|
||||
messages={{ en: {} }}
|
||||
>
|
||||
<div>Test</div>
|
||||
</LocalesProvider>
|
||||
);
|
||||
|
||||
expect(container.firstChild).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
Test
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { useContext } from 'react';
|
||||
import LocalesProviderContext from './context';
|
||||
|
||||
const useLocalesProvider = () => {
|
||||
const { changeLocale, localeNames, messages } = useContext(LocalesProviderContext);
|
||||
|
||||
return { changeLocale, localeNames, messages };
|
||||
};
|
||||
|
||||
export default useLocalesProvider;
|
||||
@ -1,18 +1,26 @@
|
||||
import React, { useEffect, useReducer, memo } from 'react';
|
||||
import React, { useEffect, useReducer } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import axios from 'axios';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faQuestion, faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
import cn from 'classnames';
|
||||
import { useGlobalContext } from '@strapi/helper-plugin';
|
||||
import { useTracking } from '@strapi/helper-plugin';
|
||||
import formatVideoArray from './utils/formatAndStoreVideoArray';
|
||||
import StaticLinks from './StaticLinks';
|
||||
import Video from './Video';
|
||||
import Wrapper from './Wrapper';
|
||||
import reducer, { initialState } from './reducer';
|
||||
|
||||
const Onboarding = () => {
|
||||
if (process.env.STRAPI_ADMIN_SHOW_TUTORIALS !== 'true') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <OnboardingVideos />;
|
||||
};
|
||||
|
||||
const OnboardingVideos = () => {
|
||||
const { emitEvent } = useGlobalContext();
|
||||
const { trackUsage } = useTracking();
|
||||
const [{ isLoading, isOpen, videos }, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
useEffect(() => {
|
||||
@ -50,7 +58,7 @@ const OnboardingVideos = () => {
|
||||
: 'didCloseGetStartedVideoContainer';
|
||||
|
||||
dispatch({ type: 'SET_IS_OPEN' });
|
||||
emitEvent(eventName);
|
||||
trackUsage(eventName);
|
||||
};
|
||||
const handleClickOpenVideo = videoIndexToOpen => {
|
||||
dispatch({
|
||||
@ -104,12 +112,12 @@ const OnboardingVideos = () => {
|
||||
didPlayVideo={(_, elapsedTime) => {
|
||||
const eventName = `didPlay${index}GetStartedVideo`;
|
||||
|
||||
emitEvent(eventName, { timestamp: elapsedTime });
|
||||
trackUsage(eventName, { timestamp: elapsedTime });
|
||||
}}
|
||||
didStopVideo={(_, elapsedTime) => {
|
||||
const eventName = `didStop${index}Video`;
|
||||
|
||||
emitEvent(eventName, { timestamp: elapsedTime });
|
||||
trackUsage(eventName, { timestamp: elapsedTime });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@ -127,4 +135,4 @@ const OnboardingVideos = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(OnboardingVideos);
|
||||
export default Onboarding;
|
||||
|
||||
@ -4,12 +4,12 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import favicon from '../../favicon.png';
|
||||
|
||||
const PageTitle = ({ title }) => (
|
||||
<Helmet title={title} link={[{ rel: 'icon', type: 'image/png', href: favicon }]} />
|
||||
);
|
||||
const PageTitle = ({ title }) => {
|
||||
return <Helmet title={title} link={[{ rel: 'icon', type: 'image/png', href: favicon }]} />;
|
||||
};
|
||||
|
||||
PageTitle.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]).isRequired,
|
||||
};
|
||||
|
||||
export default memo(PageTitle);
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
import {
|
||||
GET_USER_PERMISSIONS,
|
||||
GET_USER_PERMISSIONS_SUCCEEDED,
|
||||
GET_USER_PERMISSIONS_ERROR,
|
||||
} from './constants';
|
||||
|
||||
export function getUserPermissions() {
|
||||
return {
|
||||
type: GET_USER_PERMISSIONS,
|
||||
};
|
||||
}
|
||||
|
||||
export function getUserPermissionsError(error) {
|
||||
return {
|
||||
type: GET_USER_PERMISSIONS_ERROR,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function getUserPermissionsSucceeded(data) {
|
||||
return {
|
||||
type: GET_USER_PERMISSIONS_SUCCEEDED,
|
||||
data,
|
||||
};
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
export const GET_USER_PERMISSIONS = 'StrapiAdmin/PermissionsManager/GET_USER_PERMISSIONS';
|
||||
export const GET_USER_PERMISSIONS_ERROR =
|
||||
'StrapiAdmin/PermissionsManager/GET_USER_PERMISSIONS_ERROR';
|
||||
export const GET_USER_PERMISSIONS_SUCCEEDED =
|
||||
'StrapiAdmin/PermissionsManager/GET_USER_PERMISSIONS_SUCCEEDED';
|
||||
@ -1,49 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { LoadingIndicatorPage, UserProvider, request } from '@strapi/helper-plugin';
|
||||
import {
|
||||
getUserPermissions,
|
||||
getUserPermissionsError,
|
||||
getUserPermissionsSucceeded,
|
||||
} from './actions';
|
||||
|
||||
const PermissionsManager = ({ children }) => {
|
||||
const { isLoading, userPermissions } = useSelector(state => state.permissionsManager);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const fetchUserPermissions = async (resetState = false) => {
|
||||
if (resetState) {
|
||||
// Show a loader
|
||||
dispatch(getUserPermissions());
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await request('/admin/users/me/permissions', { method: 'GET' });
|
||||
|
||||
dispatch(getUserPermissionsSucceeded(data));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
dispatch(getUserPermissionsError(err));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserPermissions(true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingIndicatorPage />;
|
||||
}
|
||||
|
||||
return <UserProvider value={{ userPermissions, fetchUserPermissions }}>{children}</UserProvider>;
|
||||
};
|
||||
|
||||
PermissionsManager.defaultProps = {};
|
||||
|
||||
PermissionsManager.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default PermissionsManager;
|
||||
@ -1,103 +0,0 @@
|
||||
import produce from 'immer';
|
||||
import {
|
||||
getUserPermissions,
|
||||
getUserPermissionsError,
|
||||
getUserPermissionsSucceeded,
|
||||
} from '../actions';
|
||||
import permissionsManagerReducer from '../reducer';
|
||||
|
||||
describe('permissionsManagerReducer', () => {
|
||||
let state;
|
||||
|
||||
beforeEach(() => {
|
||||
state = {
|
||||
isLoading: true,
|
||||
userPermissions: [],
|
||||
collectionTypesRelatedPermissions: {},
|
||||
};
|
||||
});
|
||||
|
||||
it('returns the initial state', () => {
|
||||
const expected = state;
|
||||
|
||||
expect(permissionsManagerReducer(undefined, {})).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle the getUserPermissions action correctly', () => {
|
||||
state.userPermissions = ['test'];
|
||||
state.collectionTypesRelatedPermissions = null;
|
||||
|
||||
const expected = produce(state, draft => {
|
||||
draft.isLoading = true;
|
||||
draft.userPermissions = [];
|
||||
draft.collectionTypesRelatedPermissions = {};
|
||||
});
|
||||
|
||||
expect(permissionsManagerReducer(state, getUserPermissions())).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle the getUserPermissionsError action correctly', () => {
|
||||
const error = 'Error';
|
||||
const expected = produce(state, draft => {
|
||||
draft.isLoading = false;
|
||||
draft.error = error;
|
||||
});
|
||||
|
||||
expect(permissionsManagerReducer(state, getUserPermissionsError(error))).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle the getUserPermissionsSucceeded action correctly', () => {
|
||||
const data = [
|
||||
{
|
||||
action: 'create',
|
||||
subject: 'address',
|
||||
properties: {
|
||||
fields: ['f1'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
action: 'create',
|
||||
subject: 'address',
|
||||
properties: {
|
||||
fields: ['f2'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
action: 'tes',
|
||||
subject: null,
|
||||
properties: {},
|
||||
conditions: [],
|
||||
},
|
||||
];
|
||||
const expected = produce(state, draft => {
|
||||
draft.isLoading = false;
|
||||
draft.userPermissions = data;
|
||||
draft.collectionTypesRelatedPermissions = {
|
||||
address: {
|
||||
create: [
|
||||
{
|
||||
action: 'create',
|
||||
subject: 'address',
|
||||
properties: {
|
||||
fields: ['f1'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
action: 'create',
|
||||
subject: 'address',
|
||||
properties: {
|
||||
fields: ['f2'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
expect(permissionsManagerReducer(state, getUserPermissionsSucceeded(data))).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@ -1,12 +1,12 @@
|
||||
import React, { useReducer, useRef } from 'react';
|
||||
import { LoadingIndicatorPage, useStrapiApp } from '@strapi/helper-plugin';
|
||||
import Admin from '../../pages/Admin';
|
||||
|
||||
import init from './init';
|
||||
import reducer, { initialState } from './reducer';
|
||||
|
||||
const PluginsInitializer = () => {
|
||||
const { plugins: appPlugins } = useStrapiApp();
|
||||
|
||||
const [{ plugins }, dispatch] = useReducer(reducer, initialState, () => init(appPlugins));
|
||||
const setPlugin = useRef(pluginId => {
|
||||
dispatch({ type: 'SET_PLUGIN_READY', pluginId });
|
||||
@ -35,7 +35,7 @@ const PluginsInitializer = () => {
|
||||
);
|
||||
}
|
||||
|
||||
return <Admin plugins={plugins} />;
|
||||
return <Admin />;
|
||||
};
|
||||
|
||||
export default PluginsInitializer;
|
||||
|
||||
@ -32,7 +32,7 @@ const PrivateRoute = ({ component: Component, path, ...rest }) => (
|
||||
);
|
||||
|
||||
PrivateRoute.propTypes = {
|
||||
component: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
|
||||
component: PropTypes.any.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user