Merge branch 'v4/backend' into v4/database
@ -36,8 +36,6 @@ module.exports = {
|
||||
BACKEND_URL: true,
|
||||
PUBLIC_PATH: true,
|
||||
NODE_ENV: true,
|
||||
STRAPI_ADMIN_SHOW_TUTORIALS: true,
|
||||
STRAPI_ADMIN_UPDATE_NOTIFICATION: true,
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
const frontPaths = [
|
||||
'packages/**/admin/src/**/*.js',
|
||||
'packages/generators/app/lib/resources/files/admin/app.js',
|
||||
'packages/**/ee/admin/**/*.js',
|
||||
'packages/core/helper-plugin/**/*.js',
|
||||
'packages/**/tests/front/**/*.js',
|
||||
|
||||
18
.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
|
||||
@ -165,7 +165,7 @@ jobs:
|
||||
STRAPI_LICENSE: ${{ secrets.strapiLicense }}
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14]
|
||||
node: [12, 14, 16]
|
||||
max-parallel: 2
|
||||
services:
|
||||
postgres:
|
||||
@ -205,7 +205,7 @@ jobs:
|
||||
STRAPI_LICENSE: ${{ secrets.strapiLicense }}
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14]
|
||||
node: [12, 14, 16]
|
||||
max-parallel: 2
|
||||
services:
|
||||
mysql:
|
||||
@ -244,7 +244,7 @@ jobs:
|
||||
STRAPI_LICENSE: ${{ secrets.strapiLicense }}
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14]
|
||||
node: [12, 14, 16]
|
||||
max-parallel: 2
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
@ -97,7 +97,7 @@ Complete installation requirements can be found in the documentation under <a hr
|
||||
|
||||
**Node:**
|
||||
|
||||
- NodeJS >= 10.16 <=14
|
||||
- NodeJS >= 12 <= 16
|
||||
- NPM >= 6.x
|
||||
|
||||
**Database:**
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
'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;
|
||||
},
|
||||
};
|
||||
33
examples/getstarted/admin/app.js
Normal file
@ -0,0 +1,33 @@
|
||||
// import MyCompo from './extensions/MyCompo';
|
||||
|
||||
export default {
|
||||
config: {
|
||||
// Leaving this commented on purpose
|
||||
// auth: {
|
||||
// logo:
|
||||
// 'https://images.unsplash.com/photo-1593642634367-d91a135587b5?ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=750&q=80',
|
||||
// },
|
||||
// head: {
|
||||
// favicon:
|
||||
// 'https://images.unsplash.com/photo-1593642634367-d91a135587b5?ixid=MnwxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=750&q=80',
|
||||
// title: 'Strapi',
|
||||
// },
|
||||
// locales: ['fr', 'toto'],
|
||||
// menu: {
|
||||
// logo: null,
|
||||
// },
|
||||
// theme: {
|
||||
// main: {
|
||||
// colors: { ok: 't' },
|
||||
// },
|
||||
// },
|
||||
// translations: {
|
||||
// fr: {
|
||||
// 'Auth.form.email.label': 'test',
|
||||
// },
|
||||
// },
|
||||
// tutorials: false,
|
||||
// notifications: { release: false },
|
||||
},
|
||||
bootstrap() {},
|
||||
};
|
||||
9
examples/getstarted/admin/webpack.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
module.exports = (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;
|
||||
};
|
||||
@ -99,7 +99,8 @@
|
||||
}
|
||||
},
|
||||
"slug": {
|
||||
"type": "uid"
|
||||
"type": "uid",
|
||||
"targetField": "city"
|
||||
},
|
||||
"notrepeat_req": {
|
||||
"type": "component",
|
||||
|
||||
@ -2,12 +2,14 @@
|
||||
"collectionName": "components_blog_test_comos",
|
||||
"info": {
|
||||
"name": "test comp",
|
||||
"icon": "ad"
|
||||
"icon": "ad",
|
||||
"description": ""
|
||||
},
|
||||
"options": {},
|
||||
"attributes": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"default": "toto"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
"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"
|
||||
|
||||
@ -2,6 +2,8 @@ import { prefixPluginTranslations } from '@strapi/helper-plugin';
|
||||
import pluginPkg from '../../package.json';
|
||||
import pluginId from './pluginId';
|
||||
|
||||
import App from './pages/App';
|
||||
|
||||
const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
|
||||
const icon = pluginPkg.strapi.icon;
|
||||
const name = pluginPkg.strapi.name;
|
||||
@ -15,7 +17,7 @@ export default {
|
||||
id: `${pluginId}.plugin.name`,
|
||||
defaultMessage: 'My plugin',
|
||||
},
|
||||
Component: () => 'My plugin',
|
||||
Component: App,
|
||||
permissions: [],
|
||||
});
|
||||
app.registerPlugin({
|
||||
@ -27,7 +29,7 @@ export default {
|
||||
name,
|
||||
});
|
||||
},
|
||||
boot() {},
|
||||
bootstrap() {},
|
||||
async registerTrads({ locales }) {
|
||||
const importedTrads = await Promise.all(
|
||||
locales.map(locale => {
|
||||
|
||||
@ -38,14 +38,15 @@ module.exports = {
|
||||
__webpack_public_path__: 'http://localhost:4000',
|
||||
strapi: {
|
||||
backendURL: 'http://localhost:1337',
|
||||
isEE: false,
|
||||
features: [],
|
||||
projectType: 'Community',
|
||||
},
|
||||
BACKEND_URL: 'http://localhost:1337',
|
||||
ADMIN_PATH: '/admin',
|
||||
NODE_ENV: 'test',
|
||||
'process.env.STRAPI_ADMIN_ENABLED_EE_FEATURES': [],
|
||||
STRAPI_ADMIN_ENABLED_EE_FEATURES: [],
|
||||
'process.env.STRAPI_ADMIN_SHOW_TUTORIALS': 'false',
|
||||
'process.env.STRAPI_ADMIN_UPDATE_NOTIFICATION': 'false',
|
||||
|
||||
// FIXME create a clean config file
|
||||
},
|
||||
moduleDirectories: [
|
||||
'node_modules',
|
||||
|
||||
@ -38,6 +38,7 @@
|
||||
"lint-staged": "^10.5.4",
|
||||
"lodash": "4.17.21",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"plop": "2.7.4",
|
||||
"prettier": "^1.18.2",
|
||||
"qs": "6.10.1",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
@ -57,6 +58,7 @@
|
||||
"setup": "yarn && yarn build",
|
||||
"watch": "lerna run --stream watch --no-private",
|
||||
"build": "lerna run --stream build --no-private",
|
||||
"generate": "plop --plopfile ./packages/generators/admin/plopfile.js",
|
||||
"lint": "npm-run-all -p lint:code lint:css",
|
||||
"lint:code": "eslint .",
|
||||
"lint:css": "stylelint packages/**/admin/src/**/*.js",
|
||||
@ -100,7 +102,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",
|
||||
|
||||
@ -0,0 +1,483 @@
|
||||
'use strict';
|
||||
|
||||
const addressCT = {
|
||||
uid: 'application::address.address',
|
||||
settings: {
|
||||
bulkable: true,
|
||||
filterable: true,
|
||||
searchable: true,
|
||||
pageSize: 10,
|
||||
mainField: 'postal_coder',
|
||||
defaultSortBy: 'postal_coder',
|
||||
defaultSortOrder: 'ASC',
|
||||
},
|
||||
metadatas: {
|
||||
id: { edit: {}, list: { label: 'Id', searchable: true, sortable: true } },
|
||||
postal_coder: {
|
||||
edit: {
|
||||
label: 'Postal_coder',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
},
|
||||
list: { label: 'Postal_coder', searchable: true, sortable: true },
|
||||
},
|
||||
categories: {
|
||||
list: {
|
||||
label: 'Categories',
|
||||
searchable: false,
|
||||
sortable: false,
|
||||
mainField: { name: 'name', schema: { type: 'string' } },
|
||||
},
|
||||
edit: {
|
||||
label: 'Categories',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
mainField: { name: 'name', schema: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
cover: {
|
||||
edit: { label: 'Cover', description: '', placeholder: '', visible: true, editable: true },
|
||||
list: { label: 'Cover', searchable: false, sortable: false },
|
||||
},
|
||||
images: {
|
||||
edit: { label: 'Images', description: '', placeholder: '', visible: true, editable: true },
|
||||
list: { label: 'Images', searchable: false, sortable: false },
|
||||
},
|
||||
city: {
|
||||
edit: { label: 'City', description: '', placeholder: '', visible: true, editable: true },
|
||||
list: { label: 'City', searchable: true, sortable: true },
|
||||
},
|
||||
likes: {
|
||||
list: {
|
||||
label: 'Likes',
|
||||
searchable: false,
|
||||
sortable: false,
|
||||
mainField: { name: 'id', schema: { type: 'integer' } },
|
||||
},
|
||||
edit: {
|
||||
label: 'Likes',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
mainField: { name: 'id', schema: { type: 'integer' } },
|
||||
},
|
||||
},
|
||||
json: {
|
||||
edit: { label: 'Json', description: '', placeholder: '', visible: true, editable: true },
|
||||
list: { label: 'Json', searchable: false, sortable: false },
|
||||
},
|
||||
slug: {
|
||||
edit: { label: 'Slug', description: '', placeholder: '', visible: true, editable: true },
|
||||
list: { label: 'Slug', searchable: true, sortable: true },
|
||||
},
|
||||
notrepeat_req: {
|
||||
edit: {
|
||||
label: 'Notrepeat_req',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
},
|
||||
list: { label: 'Notrepeat_req', searchable: false, sortable: false },
|
||||
},
|
||||
repeat_req: {
|
||||
edit: {
|
||||
label: 'Repeat_req',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
},
|
||||
list: { label: 'Repeat_req', searchable: false, sortable: false },
|
||||
},
|
||||
repeat_req_min: {
|
||||
edit: {
|
||||
label: 'Repeat_req_min',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
},
|
||||
list: { label: 'Repeat_req_min', searchable: false, sortable: false },
|
||||
},
|
||||
created_at: {
|
||||
edit: {
|
||||
label: 'Created_at',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: false,
|
||||
editable: true,
|
||||
},
|
||||
list: { label: 'Created_at', searchable: true, sortable: true },
|
||||
},
|
||||
updated_at: {
|
||||
edit: {
|
||||
label: 'Updated_at',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: false,
|
||||
editable: true,
|
||||
},
|
||||
list: { label: 'Updated_at', searchable: true, sortable: true },
|
||||
},
|
||||
},
|
||||
layouts: {
|
||||
list: [
|
||||
{
|
||||
key: '__id_key__',
|
||||
name: 'id',
|
||||
fieldSchema: { type: 'integer' },
|
||||
metadatas: { label: 'Id', searchable: true, sortable: true },
|
||||
},
|
||||
{
|
||||
key: '__postal_coder_key__',
|
||||
name: 'postal_coder',
|
||||
fieldSchema: { type: 'string', pluginOptions: { i18n: { localized: true } } },
|
||||
metadatas: { label: 'Postal_coder', searchable: true, sortable: true },
|
||||
},
|
||||
{
|
||||
key: '__categories_key__',
|
||||
name: 'categories',
|
||||
fieldSchema: {
|
||||
collection: 'category',
|
||||
via: 'addresses',
|
||||
dominant: true,
|
||||
attribute: 'category',
|
||||
column: 'id',
|
||||
isVirtual: true,
|
||||
type: 'relation',
|
||||
targetModel: 'application::category.category',
|
||||
relationType: 'manyToMany',
|
||||
},
|
||||
metadatas: {
|
||||
label: 'Categories',
|
||||
searchable: false,
|
||||
sortable: false,
|
||||
mainField: { name: 'name', schema: { type: 'string' } },
|
||||
},
|
||||
queryInfos: {
|
||||
endPoint: 'collection-types/application::address.address',
|
||||
defaultParams: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: '__cover_key__',
|
||||
name: 'cover',
|
||||
fieldSchema: {
|
||||
type: 'media',
|
||||
multiple: false,
|
||||
required: false,
|
||||
allowedTypes: ['files', 'images', 'videos'],
|
||||
pluginOptions: { i18n: { localized: true } },
|
||||
},
|
||||
metadatas: { label: 'Cover', searchable: false, sortable: false },
|
||||
},
|
||||
],
|
||||
edit: [
|
||||
[
|
||||
{
|
||||
name: 'postal_coder',
|
||||
size: 6,
|
||||
fieldSchema: { type: 'string', pluginOptions: { i18n: { localized: true } } },
|
||||
metadatas: {
|
||||
label: 'Postal_coder',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'cover',
|
||||
size: 6,
|
||||
fieldSchema: {
|
||||
type: 'media',
|
||||
multiple: false,
|
||||
required: false,
|
||||
allowedTypes: ['files', 'images', 'videos'],
|
||||
pluginOptions: { i18n: { localized: true } },
|
||||
},
|
||||
metadatas: {
|
||||
label: 'Cover',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'images',
|
||||
size: 6,
|
||||
fieldSchema: {
|
||||
type: 'media',
|
||||
multiple: true,
|
||||
required: false,
|
||||
allowedTypes: ['images'],
|
||||
pluginOptions: { i18n: { localized: true } },
|
||||
},
|
||||
metadatas: {
|
||||
label: 'Images',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'city',
|
||||
size: 6,
|
||||
fieldSchema: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
maxLength: 200,
|
||||
pluginOptions: { i18n: { localized: true } },
|
||||
},
|
||||
metadatas: {
|
||||
label: 'City',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'json',
|
||||
size: 12,
|
||||
fieldSchema: { type: 'json', pluginOptions: { i18n: { localized: true } } },
|
||||
metadatas: {
|
||||
label: 'Json',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'slug',
|
||||
size: 6,
|
||||
fieldSchema: { type: 'uid', targetField: 'city' },
|
||||
metadatas: {
|
||||
label: 'Slug',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'notrepeat_req',
|
||||
size: 12,
|
||||
fieldSchema: {
|
||||
type: 'component',
|
||||
repeatable: false,
|
||||
pluginOptions: { i18n: { localized: false } },
|
||||
component: 'blog.test-como',
|
||||
required: true,
|
||||
},
|
||||
metadatas: {
|
||||
label: 'Notrepeat_req',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'repeat_req',
|
||||
size: 12,
|
||||
fieldSchema: {
|
||||
type: 'component',
|
||||
repeatable: true,
|
||||
pluginOptions: { i18n: { localized: true } },
|
||||
component: 'blog.test-como',
|
||||
required: true,
|
||||
},
|
||||
metadatas: {
|
||||
label: 'Repeat_req',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'repeat_req_min',
|
||||
size: 12,
|
||||
fieldSchema: {
|
||||
type: 'component',
|
||||
repeatable: true,
|
||||
pluginOptions: { i18n: { localized: true } },
|
||||
component: 'blog.test-como',
|
||||
required: false,
|
||||
min: 2,
|
||||
},
|
||||
metadatas: {
|
||||
label: 'Repeat_req_min',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
editRelations: [
|
||||
{
|
||||
name: 'categories',
|
||||
size: 6,
|
||||
fieldSchema: {
|
||||
collection: 'category',
|
||||
via: 'addresses',
|
||||
dominant: true,
|
||||
attribute: 'category',
|
||||
column: 'id',
|
||||
isVirtual: true,
|
||||
type: 'relation',
|
||||
targetModel: 'application::category.category',
|
||||
relationType: 'manyToMany',
|
||||
},
|
||||
metadatas: {
|
||||
label: 'Categories',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
mainField: { name: 'name', schema: { type: 'string' } },
|
||||
},
|
||||
queryInfos: {
|
||||
endPoint: '/content-manager/relations/application::address.address/categories',
|
||||
containsKey: 'name_contains',
|
||||
defaultParams: {},
|
||||
shouldDisplayRelationLink: true,
|
||||
},
|
||||
targetModelPluginOptions: {},
|
||||
},
|
||||
{
|
||||
name: 'likes',
|
||||
size: 6,
|
||||
fieldSchema: {
|
||||
collection: 'like',
|
||||
via: 'address',
|
||||
isVirtual: true,
|
||||
type: 'relation',
|
||||
targetModel: 'application::like.like',
|
||||
relationType: 'oneToMany',
|
||||
},
|
||||
metadatas: {
|
||||
label: 'Likes',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
mainField: { name: 'id', schema: { type: 'integer' } },
|
||||
},
|
||||
queryInfos: {
|
||||
endPoint: '/content-manager/relations/application::address.address/likes',
|
||||
containsKey: 'id_contains',
|
||||
defaultParams: {},
|
||||
shouldDisplayRelationLink: true,
|
||||
},
|
||||
targetModelPluginOptions: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
isDisplayed: true,
|
||||
apiID: 'address',
|
||||
kind: 'collectionType',
|
||||
info: { name: 'address', description: '', label: 'Addresses' },
|
||||
options: {
|
||||
draftAndPublish: true,
|
||||
increments: true,
|
||||
timestamps: ['created_at', 'updated_at'],
|
||||
comment: '',
|
||||
},
|
||||
pluginOptions: { i18n: { localized: true } },
|
||||
attributes: {
|
||||
id: { type: 'integer' },
|
||||
postal_coder: { type: 'string', pluginOptions: { i18n: { localized: true } } },
|
||||
categories: {
|
||||
collection: 'category',
|
||||
via: 'addresses',
|
||||
dominant: true,
|
||||
attribute: 'category',
|
||||
column: 'id',
|
||||
isVirtual: true,
|
||||
type: 'relation',
|
||||
targetModel: 'application::category.category',
|
||||
relationType: 'manyToMany',
|
||||
},
|
||||
cover: {
|
||||
type: 'media',
|
||||
multiple: false,
|
||||
required: false,
|
||||
allowedTypes: ['files', 'images', 'videos'],
|
||||
pluginOptions: { i18n: { localized: true } },
|
||||
},
|
||||
images: {
|
||||
type: 'media',
|
||||
multiple: true,
|
||||
required: false,
|
||||
allowedTypes: ['images'],
|
||||
pluginOptions: { i18n: { localized: true } },
|
||||
},
|
||||
city: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
maxLength: 200,
|
||||
pluginOptions: { i18n: { localized: true } },
|
||||
},
|
||||
likes: {
|
||||
collection: 'like',
|
||||
via: 'address',
|
||||
isVirtual: true,
|
||||
type: 'relation',
|
||||
targetModel: 'application::like.like',
|
||||
relationType: 'oneToMany',
|
||||
},
|
||||
json: { type: 'json', pluginOptions: { i18n: { localized: true } } },
|
||||
slug: { type: 'uid', targetField: 'city' },
|
||||
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,
|
||||
},
|
||||
created_at: { type: 'timestamp' },
|
||||
updated_at: { type: 'timestamp' },
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = addressCT;
|
||||
101
packages/admin-test-utils/lib/fixtures/metaData/address.js
Normal file
@ -0,0 +1,101 @@
|
||||
'use strict';
|
||||
|
||||
const addressMetaData = {
|
||||
id: { edit: {}, list: { label: 'Id', searchable: true, sortable: true } },
|
||||
postal_coder: {
|
||||
edit: {
|
||||
label: 'Postal_coder',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
},
|
||||
list: { label: 'Postal_coder', searchable: true, sortable: true },
|
||||
},
|
||||
categories: {
|
||||
list: {
|
||||
label: 'Categories',
|
||||
searchable: false,
|
||||
sortable: false,
|
||||
mainField: { name: 'name', schema: { type: 'string' } },
|
||||
},
|
||||
edit: {
|
||||
label: 'Categories',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
mainField: { name: 'name', schema: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
cover: {
|
||||
edit: { label: 'Cover', description: '', placeholder: '', visible: true, editable: true },
|
||||
list: { label: 'Cover', searchable: false, sortable: false },
|
||||
},
|
||||
images: {
|
||||
edit: { label: 'Images', description: '', placeholder: '', visible: true, editable: true },
|
||||
list: { label: 'Images', searchable: false, sortable: false },
|
||||
},
|
||||
city: {
|
||||
edit: { label: 'City', description: '', placeholder: '', visible: true, editable: true },
|
||||
list: { label: 'City', searchable: true, sortable: true },
|
||||
},
|
||||
likes: {
|
||||
list: {
|
||||
label: 'Likes',
|
||||
searchable: false,
|
||||
sortable: false,
|
||||
mainField: { name: 'id', schema: { type: 'integer' } },
|
||||
},
|
||||
edit: {
|
||||
label: 'Likes',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
mainField: { name: 'id', schema: { type: 'integer' } },
|
||||
},
|
||||
},
|
||||
json: {
|
||||
edit: { label: 'Json', description: '', placeholder: '', visible: true, editable: true },
|
||||
list: { label: 'Json', searchable: false, sortable: false },
|
||||
},
|
||||
slug: {
|
||||
edit: { label: 'Slug', description: '', placeholder: '', visible: true, editable: true },
|
||||
list: { label: 'Slug', searchable: true, sortable: true },
|
||||
},
|
||||
notrepeat_req: {
|
||||
edit: {
|
||||
label: 'Notrepeat_req',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
},
|
||||
list: { label: 'Notrepeat_req', searchable: false, sortable: false },
|
||||
},
|
||||
repeat_req: {
|
||||
edit: { label: 'Repeat_req', description: '', placeholder: '', visible: true, editable: true },
|
||||
list: { label: 'Repeat_req', searchable: false, sortable: false },
|
||||
},
|
||||
repeat_req_min: {
|
||||
edit: {
|
||||
label: 'Repeat_req_min',
|
||||
description: '',
|
||||
placeholder: '',
|
||||
visible: true,
|
||||
editable: true,
|
||||
},
|
||||
list: { label: 'Repeat_req_min', searchable: false, sortable: false },
|
||||
},
|
||||
created_at: {
|
||||
edit: { label: 'Created_at', description: '', placeholder: '', visible: false, editable: true },
|
||||
list: { label: 'Created_at', searchable: true, sortable: true },
|
||||
},
|
||||
updated_at: {
|
||||
edit: { label: 'Updated_at', description: '', placeholder: '', visible: false, editable: true },
|
||||
list: { label: 'Updated_at', searchable: true, sortable: true },
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = addressMetaData;
|
||||
@ -4,6 +4,37 @@
|
||||
const { combineReducers, createStore } = require('redux');
|
||||
|
||||
const reducers = {
|
||||
'content-manager_app': jest.fn(() => ({
|
||||
components: [],
|
||||
status: 'loading',
|
||||
models: [],
|
||||
collectionTypeLinks: [],
|
||||
singleTypeLinks: [],
|
||||
})),
|
||||
'content-manager_listView': jest.fn(() => ({
|
||||
data: [],
|
||||
didDeleteData: false,
|
||||
entriesToDelete: [],
|
||||
isLoading: true,
|
||||
showModalConfirmButtonLoading: false,
|
||||
showWarningDelete: false,
|
||||
showWarningDeleteAll: false,
|
||||
contentType: {},
|
||||
initialDisplayedHeaders: [],
|
||||
displayedHeaders: [],
|
||||
pagination: {
|
||||
total: 0,
|
||||
},
|
||||
})),
|
||||
'content-manager_rbacManager': jest.fn(() => ({ permissions: null })),
|
||||
'content-manager_editViewLayoutManager': jest.fn(() => ({ currentLayout: null })),
|
||||
'content-manager_editViewCrudReducer': jest.fn(() => ({
|
||||
componentsDataStructure: {},
|
||||
contentTypeDataStructure: {},
|
||||
isLoading: true,
|
||||
data: {},
|
||||
status: 'resolved',
|
||||
})),
|
||||
rbacProvider: jest.fn(() => ({ allPermissions: null, collectionTypesRelatedPermissions: {} })),
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
// FIXME
|
||||
/* eslint-disable import/extensions */
|
||||
const commander = require('commander');
|
||||
const generateNewApp = require('@strapi/generate-new');
|
||||
const promptUser = require('./utils/prompt-user');
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
// FIXME
|
||||
/* eslint-disable import/extensions */
|
||||
const commander = require('commander');
|
||||
|
||||
const packageJson = require('./package.json');
|
||||
|
||||
@ -45,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"
|
||||
|
||||
@ -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",
|
||||
|
||||
0
packages/core/admin/.env
Normal file
2
packages/core/admin/.gitignore
vendored
@ -8,4 +8,4 @@ yarn-error.log
|
||||
.DS_Store
|
||||
npm-debug.log
|
||||
.idea
|
||||
.env
|
||||
.env
|
||||
|
||||
@ -1,40 +1,43 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
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 merge from 'lodash/merge';
|
||||
import pick from 'lodash/pick';
|
||||
import createHook from '@strapi/hooks';
|
||||
import isFunction from 'lodash/isFunction';
|
||||
import invariant from 'invariant';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { basename, createHook } from './core/utils';
|
||||
import configureStore from './core/store/configureStore';
|
||||
import { Plugin } from './core/apis';
|
||||
import basename from './utils/basename';
|
||||
import App from './pages/App';
|
||||
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';
|
||||
import AuthLogo from './assets/images/logo_strapi_auth.png';
|
||||
import MenuLogo from './assets/images/logo_strapi_menu.png';
|
||||
import Providers from './components/Providers';
|
||||
import Theme from './components/Theme';
|
||||
import languageNativeNames from './translations/languageNativeNames';
|
||||
|
||||
window.strapi = {
|
||||
backendURL: process.env.STRAPI_ADMIN_BACKEND_URL,
|
||||
};
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
import {
|
||||
INJECT_COLUMN_IN_TABLE,
|
||||
MUTATE_COLLECTION_TYPES_LINKS,
|
||||
MUTATE_EDIT_VIEW_LAYOUT,
|
||||
MUTATE_SINGLE_TYPES_LINKS,
|
||||
} from './exposedHooks';
|
||||
import injectionZones from './injectionZones';
|
||||
import favicon from './favicon.ico';
|
||||
import themes from './themes';
|
||||
|
||||
class StrapiApp {
|
||||
constructor({ appPlugins, library, locales, middlewares, reducers }) {
|
||||
this.appLocales = ['en', ...locales.filter(loc => loc !== 'en')];
|
||||
constructor({ adminConfig, appPlugins, library, middlewares, reducers }) {
|
||||
this.customConfigurations = adminConfig.config;
|
||||
this.customBootstrapConfiguration = adminConfig.bootstrap;
|
||||
this.configurations = {
|
||||
authLogo: AuthLogo,
|
||||
head: { favicon },
|
||||
locales: ['en'],
|
||||
menuLogo: MenuLogo,
|
||||
notifications: { releases: true },
|
||||
theme: themes,
|
||||
translations: {},
|
||||
tutorials: true,
|
||||
};
|
||||
this.appPlugins = appPlugins || {};
|
||||
this.library = library;
|
||||
this.middlewares = middlewares;
|
||||
@ -42,6 +45,10 @@ class StrapiApp {
|
||||
this.reducers = reducers;
|
||||
this.translations = {};
|
||||
this.hooksDict = {};
|
||||
this.admin = {
|
||||
injectionZones,
|
||||
};
|
||||
|
||||
this.menu = [];
|
||||
this.settings = {
|
||||
global: {
|
||||
@ -72,9 +79,7 @@ class StrapiApp {
|
||||
`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
|
||||
),
|
||||
['/plugins/content-type-builder', '/plugins/upload'].includes(link.to),
|
||||
'This method is not available for your plugin'
|
||||
);
|
||||
invariant(
|
||||
@ -153,35 +158,86 @@ class StrapiApp {
|
||||
});
|
||||
};
|
||||
|
||||
async initialize() {
|
||||
async bootstrap() {
|
||||
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,
|
||||
});
|
||||
});
|
||||
}
|
||||
const bootstrap = this.appPlugins[plugin].bootstrap;
|
||||
|
||||
async boot() {
|
||||
Object.keys(this.appPlugins).forEach(plugin => {
|
||||
const boot = this.appPlugins[plugin].boot;
|
||||
|
||||
if (boot) {
|
||||
boot({
|
||||
if (bootstrap) {
|
||||
bootstrap({
|
||||
addSettingsLink: this.addSettingsLink,
|
||||
addSettingsLinks: this.addSettingsLinks,
|
||||
getPlugin: this.getPlugin,
|
||||
injectContentManagerComponent: this.injectContentManagerComponent,
|
||||
injectAdminComponent: this.injectAdminComponent,
|
||||
registerHook: this.registerHook,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (isFunction(this.customBootstrapConfiguration)) {
|
||||
this.customBootstrapConfiguration({
|
||||
addComponents: this.addComponents,
|
||||
addFields: this.addFields,
|
||||
addMenuLink: this.addMenuLink,
|
||||
addReducers: this.addReducers,
|
||||
addSettingsLink: this.addSettingsLink,
|
||||
addSettingsLinks: this.addSettingsLinks,
|
||||
getPlugin: this.getPlugin,
|
||||
injectContentManagerComponent: this.injectContentManagerComponent,
|
||||
injectAdminComponent: this.injectAdminComponent,
|
||||
registerHook: this.registerHook,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bootstrapAdmin = async () => {
|
||||
await this.createCustomConfigurations();
|
||||
|
||||
this.createHook(INJECT_COLUMN_IN_TABLE);
|
||||
this.createHook(MUTATE_COLLECTION_TYPES_LINKS);
|
||||
this.createHook(MUTATE_SINGLE_TYPES_LINKS);
|
||||
this.createHook(MUTATE_EDIT_VIEW_LAYOUT);
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
createCustomConfigurations = async () => {
|
||||
if (this.customConfigurations?.locales) {
|
||||
this.configurations.locales = [
|
||||
'en',
|
||||
...this.customConfigurations.locales?.filter(loc => loc !== 'en'),
|
||||
];
|
||||
}
|
||||
|
||||
if (this.customConfigurations?.auth?.logo) {
|
||||
this.configurations.authLogo = this.customConfigurations.auth.logo;
|
||||
}
|
||||
|
||||
if (this.customConfigurations?.menu?.logo) {
|
||||
this.configurations.menuLogo = this.customConfigurations.menu.logo;
|
||||
}
|
||||
|
||||
if (this.customConfigurations?.head?.favicon) {
|
||||
this.configurations.head.favicon = this.customConfigurations.head.favicon;
|
||||
}
|
||||
|
||||
if (this.customConfigurations?.theme) {
|
||||
this.configurations.theme = merge(this.configurations.theme, this.customConfigurations.theme);
|
||||
}
|
||||
|
||||
if (this.customConfigurations?.notifications?.releases !== undefined) {
|
||||
this.configurations.notifications.releases = this.customConfigurations.notifications.releases;
|
||||
}
|
||||
|
||||
if (this.customConfigurations?.tutorials !== undefined) {
|
||||
this.configurations.tutorials = this.customConfigurations.tutorials;
|
||||
}
|
||||
};
|
||||
|
||||
createHook = name => {
|
||||
this.hooksDict[name] = createHook();
|
||||
};
|
||||
|
||||
createSettingSection = (section, links) => {
|
||||
invariant(section.id, 'section.id should be defined');
|
||||
invariant(
|
||||
@ -205,38 +261,97 @@ class StrapiApp {
|
||||
return store;
|
||||
};
|
||||
|
||||
getAdminInjectedComponents = (moduleName, containerName, blockName) => {
|
||||
try {
|
||||
return this.admin.injectionZones[moduleName][containerName][blockName] || [];
|
||||
} catch (err) {
|
||||
console.error('Cannot get injected component', err);
|
||||
|
||||
return err;
|
||||
}
|
||||
};
|
||||
|
||||
getPlugin = pluginId => {
|
||||
return this.plugins[pluginId];
|
||||
};
|
||||
|
||||
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,
|
||||
createHook: this.createHook,
|
||||
createSettingSection: this.createSettingSection,
|
||||
registerPlugin: this.registerPlugin,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
injectContentManagerComponent = (containerName, blockName, component) => {
|
||||
invariant(
|
||||
this.admin.injectionZones.contentManager[containerName]?.[blockName],
|
||||
`The ${containerName} ${blockName} zone is not defined in the content manager`
|
||||
);
|
||||
invariant(component, 'A Component must be provided');
|
||||
|
||||
this.admin.injectionZones.contentManager[containerName][blockName].push(component);
|
||||
};
|
||||
|
||||
injectAdminComponent = (containerName, blockName, component) => {
|
||||
invariant(
|
||||
this.admin.injectionZones.admin[containerName]?.[blockName],
|
||||
`The ${containerName} ${blockName} zone is not defined in the admin`
|
||||
);
|
||||
invariant(component, 'A Component must be provided');
|
||||
|
||||
this.admin.injectionZones.admin[containerName][blockName].push(component);
|
||||
};
|
||||
|
||||
/**
|
||||
* Load the admin translations
|
||||
* @returns {Object} The imported admin translations
|
||||
*/
|
||||
async loadAdminTrads() {
|
||||
const arrayOfPromises = this.appLocales.map(locale => {
|
||||
const arrayOfPromises = this.configurations.locales.map(locale => {
|
||||
return import(/* webpackChunkName: "[request]" */ `./translations/${locale}.json`)
|
||||
.then(({ default: data }) => {
|
||||
return { data, locale };
|
||||
})
|
||||
.catch(() => {
|
||||
return { data: {}, locale };
|
||||
return { data: null, locale };
|
||||
});
|
||||
});
|
||||
const adminLocales = await Promise.all(arrayOfPromises);
|
||||
|
||||
this.translations = adminLocales.reduce((acc, current) => {
|
||||
acc[current.locale] = current.data;
|
||||
const translations = adminLocales.reduce((acc, current) => {
|
||||
if (current.data) {
|
||||
acc[current.locale] = current.data;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return Promise.resolve();
|
||||
return translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the application's translations and merged the custom translations
|
||||
* with the default ones.
|
||||
*
|
||||
*/
|
||||
async loadTrads() {
|
||||
const adminTranslations = await this.loadAdminTrads();
|
||||
|
||||
const arrayOfPromises = Object.keys(this.appPlugins)
|
||||
.map(plugin => {
|
||||
const registerTrads = this.appPlugins[plugin].registerTrads;
|
||||
|
||||
if (registerTrads) {
|
||||
return registerTrads({ locales: this.appLocales });
|
||||
return registerTrads({ locales: this.configurations.locales });
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -258,45 +373,49 @@ class StrapiApp {
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
this.translations = Object.keys(this.translations).reduce((acc, current) => {
|
||||
const translations = this.configurations.locales.reduce((acc, current) => {
|
||||
acc[current] = {
|
||||
...this.translations[current],
|
||||
...adminTranslations[current],
|
||||
...(mergedTrads[current] || {}),
|
||||
...this.customConfigurations?.translations?.[current],
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
this.configurations.translations = translations;
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
registerHook = (name, fn) => {
|
||||
invariant(
|
||||
this.hooksDict[name],
|
||||
`The hook ${name} is not defined. You are trying to register a hook that does not exist in the application.`
|
||||
);
|
||||
this.hooksDict[name].register(fn);
|
||||
};
|
||||
|
||||
registerPlugin = pluginConf => {
|
||||
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);
|
||||
runHookWaterfall = (name, initialValue, asynchronous = false, store) => {
|
||||
return asynchronous
|
||||
? this.hooksDict[name].runWaterfallAsync(initialValue, store)
|
||||
: this.hooksDict[name].runWaterfall(initialValue, store);
|
||||
};
|
||||
|
||||
runHookParallel = name => this.hooksDict[name].runParallel();
|
||||
|
||||
render() {
|
||||
const store = this.createStore();
|
||||
const localeNames = pick(languageNativeNames, this.appLocales);
|
||||
const localeNames = pick(languageNativeNames, this.configurations.locales || []);
|
||||
|
||||
const {
|
||||
components: { components },
|
||||
@ -304,40 +423,47 @@ class StrapiApp {
|
||||
} = this.library;
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider theme={themes}>
|
||||
<GlobalStyle />
|
||||
<Fonts />
|
||||
<Provider store={store}>
|
||||
<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} localeNames={localeNames}>
|
||||
<AutoReloadOverlayBlockerProvider>
|
||||
<OverlayBlocker>
|
||||
<Notifications>
|
||||
<BrowserRouter basename={basename}>
|
||||
<App store={store} />
|
||||
</BrowserRouter>
|
||||
</Notifications>
|
||||
</OverlayBlocker>
|
||||
</AutoReloadOverlayBlockerProvider>
|
||||
</LanguageProvider>
|
||||
</LibraryProvider>
|
||||
</StrapiAppProvider>
|
||||
</Provider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
<Theme theme={this.configurations.theme}>
|
||||
<Providers
|
||||
authLogo={this.configurations.authLogo}
|
||||
components={components}
|
||||
fields={fields}
|
||||
localeNames={localeNames}
|
||||
getAdminInjectedComponents={this.getAdminInjectedComponents}
|
||||
getPlugin={this.getPlugin}
|
||||
messages={this.configurations.translations}
|
||||
menu={this.menu}
|
||||
menuLogo={this.configurations.menuLogo}
|
||||
plugins={this.plugins}
|
||||
runHookParallel={this.runHookParallel}
|
||||
runHookWaterfall={(name, initialValue, async = false) => {
|
||||
return this.runHookWaterfall(name, initialValue, async, store);
|
||||
}}
|
||||
runHookSeries={this.runHookSeries}
|
||||
settings={this.settings}
|
||||
showTutorials={this.configurations.tutorials}
|
||||
showReleaseNotification={this.configurations.notifications.releases}
|
||||
store={store}
|
||||
>
|
||||
<>
|
||||
<Helmet
|
||||
link={[
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/png',
|
||||
href: this.configurations.head.favicon,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<BrowserRouter basename={basename}>
|
||||
<App store={store} />
|
||||
</BrowserRouter>
|
||||
</>
|
||||
</Providers>
|
||||
</Theme>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ({ appPlugins, library, locales, middlewares, reducers }) =>
|
||||
new StrapiApp({ appPlugins, library, locales, middlewares, reducers });
|
||||
export default ({ adminConfig = {}, appPlugins, library, middlewares, reducers }) =>
|
||||
new StrapiApp({ adminConfig, appPlugins, library, middlewares, reducers });
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
app: config => {
|
||||
config.locales = ['fr'];
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
9
packages/core/admin/admin/src/app.js
Normal file
@ -0,0 +1,9 @@
|
||||
export default {
|
||||
config: {},
|
||||
bootstrap(app) {
|
||||
app.injectContentManagerComponent('editView', 'informations', {
|
||||
name: 'i18n-locale-filter-edit-view',
|
||||
Component: () => 'test',
|
||||
});
|
||||
},
|
||||
};
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
@ -2,16 +2,16 @@ import React, { useMemo } from 'react';
|
||||
import { LoadingIndicatorPage, AppInfosContext } from '@strapi/helper-plugin';
|
||||
import { useQueries } from 'react-query';
|
||||
import packageJSON from '../../../../package.json';
|
||||
import { useConfigurations } from '../../hooks';
|
||||
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 { showReleaseNotification } = useConfigurations();
|
||||
const [
|
||||
{ data: appInfos, status },
|
||||
{ data: tag_name, isLoading },
|
||||
@ -21,7 +21,7 @@ const AuthenticatedApp = () => {
|
||||
{
|
||||
queryKey: 'strapi-release',
|
||||
queryFn: fetchStrapiLatestRelease,
|
||||
enabled: canFetchRelease,
|
||||
enabled: showReleaseNotification,
|
||||
initialData: strapiVersion,
|
||||
},
|
||||
{
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { QueryClientProvider, QueryClient } from 'react-query';
|
||||
import { ConfigurationsContext } from '../../../contexts';
|
||||
import { fetchAppInfo, fetchCurrentUserPermissions, fetchStrapiLatestRelease } from '../utils/api';
|
||||
import packageJSON from '../../../../../package.json';
|
||||
import AuthenticatedApp from '..';
|
||||
@ -27,7 +28,9 @@ const queryClient = new QueryClient({
|
||||
|
||||
const app = (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthenticatedApp />
|
||||
<ConfigurationsContext.Provider value={{ showReleaseNotification: false }}>
|
||||
<AuthenticatedApp />
|
||||
</ConfigurationsContext.Provider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import axiosInstance from '../../../utils/axiosInstance';
|
||||
import { axiosInstance } from '../../../core/utils';
|
||||
import packageJSON from '../../../../../package.json';
|
||||
|
||||
const strapiVersion = packageJSON.version;
|
||||
|
||||
@ -10,7 +10,7 @@ import { useAppInfos } from '@strapi/helper-plugin';
|
||||
import Wrapper, { A } from './Wrapper';
|
||||
|
||||
function LeftMenuFooter() {
|
||||
const projectType = process.env.STRAPI_ADMIN_PROJECT_TYPE;
|
||||
const projectType = strapi.projectType;
|
||||
const { strapiVersion } = useAppInfos();
|
||||
|
||||
return (
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Logo from '../../../../assets/images/logo-strapi.png';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
background-color: #007eff;
|
||||
padding-left: 2rem;
|
||||
@ -22,7 +20,7 @@ const Wrapper = styled.div`
|
||||
letter-spacing: 0.2rem;
|
||||
color: $white;
|
||||
|
||||
background-image: url(${Logo});
|
||||
background-image: url(${props => props.logo});
|
||||
background-repeat: no-repeat;
|
||||
background-position: left center;
|
||||
background-size: auto 2.5rem;
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useConfigurations } from '../../../../hooks';
|
||||
import Wrapper from './Wrapper';
|
||||
|
||||
const LeftMenuHeader = () => (
|
||||
<Wrapper>
|
||||
<Link to="/" className="leftMenuHeaderLink">
|
||||
<span className="projectName" />
|
||||
</Link>
|
||||
</Wrapper>
|
||||
);
|
||||
const LeftMenuHeader = () => {
|
||||
const { menuLogo } = useConfigurations();
|
||||
|
||||
return (
|
||||
<Wrapper logo={menuLogo}>
|
||||
<Link to="/" className="leftMenuHeaderLink">
|
||||
<span className="projectName" />
|
||||
</Link>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeftMenuHeader;
|
||||
|
||||
@ -1,14 +1,29 @@
|
||||
import React, { memo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { BaselineAlignment } from '@strapi/helper-plugin';
|
||||
import { Footer, Header, LinksContainer, LinksSection, SectionTitle } from './compos';
|
||||
import LeftMenuLink from './compos/Link';
|
||||
|
||||
import Wrapper from './Wrapper';
|
||||
|
||||
const LeftMenu = ({ generalSectionLinks, pluginsSectionLinks }) => {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Header />
|
||||
|
||||
<LinksContainer>
|
||||
<BaselineAlignment top size="16px" />
|
||||
<LeftMenuLink
|
||||
to="/content-manager"
|
||||
icon="book-open"
|
||||
intlLabel={{
|
||||
id: `content-manager.plugin.name`,
|
||||
defaultMessage: 'Content manager',
|
||||
}}
|
||||
/>
|
||||
<BaselineAlignment bottom size="2px" />
|
||||
|
||||
{pluginsSectionLinks.length > 0 && (
|
||||
<>
|
||||
<SectionTitle>
|
||||
|
||||
@ -1,150 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Li = styled.li`
|
||||
display: block;
|
||||
padding: 8px 20px;
|
||||
cursor: pointer;
|
||||
margin-top: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: #f7f8f8;
|
||||
.title {
|
||||
color: #0e7de7;
|
||||
}
|
||||
}
|
||||
.txtWrapper,
|
||||
.thumbWrapper {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.thumbWrapper {
|
||||
position: relative;
|
||||
width: 55px;
|
||||
height: 38px;
|
||||
background-color: #d8d8d8;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(#0e7de7, 0.8);
|
||||
}
|
||||
img {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.play {
|
||||
position: absolute;
|
||||
top: calc(50% - 10px);
|
||||
left: calc(50% - 10px);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: #0e7de7;
|
||||
border: 1px solid white;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
border-radius: 50%;
|
||||
z-index: 2;
|
||||
&::before {
|
||||
content: '\f04b';
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
height: 100%;
|
||||
font-family: 'FontAwesome';
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
margin-left: 3px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.finished {
|
||||
.title {
|
||||
color: #919bae;
|
||||
}
|
||||
.thumbWrapper {
|
||||
.overlay {
|
||||
background-color: transparent;
|
||||
}
|
||||
img {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.play {
|
||||
background-color: #5a9e06;
|
||||
border-color: #5a9e06;
|
||||
&::before {
|
||||
content: '\f00c';
|
||||
margin-left: 0;
|
||||
font-size: 11px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.txtWrapper {
|
||||
padding: 0 15px;
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 26px;
|
||||
font-family: Lato;
|
||||
font-weight: 600;
|
||||
}
|
||||
.time {
|
||||
color: #919bae;
|
||||
font-family: Lato;
|
||||
font-weight: bold;
|
||||
font-size: 11px;
|
||||
line-height: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.hiddenPlayerWrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.videoModal {
|
||||
margin-right: auto !important;
|
||||
margin-left: auto !important;
|
||||
.videoModalHeader {
|
||||
padding-bottom: 0;
|
||||
border-bottom: 0;
|
||||
> h5 {
|
||||
font-family: Lato;
|
||||
font-weight: bold !important;
|
||||
font-size: 1.8rem !important;
|
||||
line-height: 3.1rem;
|
||||
color: #333740;
|
||||
}
|
||||
> button {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
margin-top: 0;
|
||||
margin-right: 0;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
span {
|
||||
line-height: 0.6em;
|
||||
}
|
||||
}
|
||||
}
|
||||
.videoPlayer {
|
||||
> button {
|
||||
top: 50%;
|
||||
margin-top: -0.75em;
|
||||
left: 50%;
|
||||
margin-left: -1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Li;
|
||||
@ -7,6 +7,7 @@
|
||||
import React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { InjectionZone } from '../../../shared/components';
|
||||
import StyledLink from './StyledLink';
|
||||
|
||||
function StaticLinks() {
|
||||
@ -38,6 +39,7 @@ function StaticLinks() {
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
<InjectionZone area="admin.tutorials.links" />
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,180 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* OnboardingList
|
||||
*
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import { isNaN } from 'lodash';
|
||||
|
||||
import { Modal, ModalHeader, ModalBody } from 'reactstrap';
|
||||
import { Player } from 'video-react';
|
||||
import 'video-react/dist/video-react.css';
|
||||
import Li from './Li';
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
class OnboardingVideo extends React.Component {
|
||||
componentDidMount() {
|
||||
if (this.hiddenPlayer.current) {
|
||||
this.hiddenPlayer.current.subscribeToStateChange(this.handleChangeState);
|
||||
}
|
||||
}
|
||||
|
||||
hiddenPlayer = React.createRef();
|
||||
|
||||
player = React.createRef();
|
||||
|
||||
handleChangeState = (state, prevState) => {
|
||||
const { duration } = state;
|
||||
const { id } = this.props;
|
||||
|
||||
if (duration !== prevState.duration && !isNaN(duration)) {
|
||||
this.props.setVideoDuration(id, duration);
|
||||
}
|
||||
};
|
||||
|
||||
handleChangeIsPlayingState = (state, prevState) => {
|
||||
const { isActive } = state;
|
||||
const { id } = this.props;
|
||||
|
||||
if (isActive !== prevState.isActive && isActive) {
|
||||
this.props.didPlayVideo(id, this.props.video.startTime);
|
||||
}
|
||||
};
|
||||
|
||||
handleCurrentTimeChange = curr => {
|
||||
this.props.getVideoCurrentTime(this.props.id, curr, this.props.video.duration);
|
||||
};
|
||||
|
||||
handleModalOpen = () => {
|
||||
this.player.current.subscribeToStateChange(this.handleChangeIsPlayingState);
|
||||
|
||||
this.player.current.play();
|
||||
|
||||
if (this.props.video.startTime === 0) {
|
||||
const { player } = this.player.current.getState();
|
||||
player.isActive = true;
|
||||
|
||||
this.props.didPlayVideo(this.props.id, this.props.video.startTime);
|
||||
} else {
|
||||
this.player.current.pause();
|
||||
}
|
||||
};
|
||||
|
||||
handleVideoPause = () => {
|
||||
const { player } = this.player.current.getState();
|
||||
const currTime = player.currentTime;
|
||||
|
||||
this.handleCurrentTimeChange(currTime);
|
||||
this.props.didStopVideo(this.props.id, currTime);
|
||||
};
|
||||
|
||||
handleModalClose = () => {
|
||||
const { player } = this.player.current.getState();
|
||||
const paused = player.paused;
|
||||
|
||||
if (!paused) {
|
||||
this.handleVideoPause();
|
||||
}
|
||||
};
|
||||
|
||||
getVideoTime = (duration, sign) => {
|
||||
const operator = Math.floor(eval(duration + sign + 60));
|
||||
|
||||
if (operator < 10) {
|
||||
return `0${operator}`;
|
||||
}
|
||||
return operator;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { video } = this.props;
|
||||
const time = isNaN(video.duration)
|
||||
? '\xA0'
|
||||
: `${Math.floor(video.duration / 60)}:${this.getVideoTime(video.duration, '%')}`;
|
||||
|
||||
return (
|
||||
<Li
|
||||
key={this.props.id}
|
||||
onClick={this.props.onClick}
|
||||
id={this.props.id}
|
||||
className={cn(video.end && 'finished')}
|
||||
>
|
||||
<div className="thumbWrapper">
|
||||
<img src={video.preview} alt="preview" />
|
||||
<div className="overlay" />
|
||||
<div className="play" />
|
||||
</div>
|
||||
<div className="txtWrapper">
|
||||
<p className="title">{video.title}</p>
|
||||
<p className="time">{time}</p>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
isOpen={video.isOpen}
|
||||
toggle={this.props.onClick} // eslint-disable-line react/jsx-handler-names
|
||||
className="videoModal"
|
||||
onOpened={this.handleModalOpen}
|
||||
onClosed={this.handleModalClose}
|
||||
>
|
||||
<ModalHeader
|
||||
toggle={this.props.onClick} // eslint-disable-line react/jsx-handler-names
|
||||
className="videoModalHeader"
|
||||
>
|
||||
{video.title}
|
||||
</ModalHeader>
|
||||
<ModalBody className="modalBodyHelper">
|
||||
<div>
|
||||
<Player
|
||||
ref={this.player}
|
||||
className="videoPlayer"
|
||||
src={video.video}
|
||||
startTime={video.startTime}
|
||||
preload="auto"
|
||||
onPause={this.handleVideoPause}
|
||||
onplay={this.videoStart}
|
||||
subscribeToStateChange={this.subscribeToStateChange}
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
|
||||
{!this.props.video.duration && (
|
||||
<div className="hiddenPlayerWrapper">
|
||||
<Player
|
||||
ref={this.hiddenPlayer}
|
||||
src={video.video}
|
||||
preload="auto"
|
||||
subscribeToStateChange={this.subscribeToStateChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OnboardingVideo.defaultProps = {
|
||||
didPlayVideo: () => {},
|
||||
didStopVideo: () => {},
|
||||
getVideoCurrentTime: () => {},
|
||||
id: 0,
|
||||
onClick: () => {},
|
||||
setVideoDuration: () => {},
|
||||
video: {},
|
||||
};
|
||||
|
||||
OnboardingVideo.propTypes = {
|
||||
didPlayVideo: PropTypes.func,
|
||||
didStopVideo: PropTypes.func,
|
||||
getVideoCurrentTime: PropTypes.func,
|
||||
id: PropTypes.number,
|
||||
onClick: PropTypes.func,
|
||||
setVideoDuration: PropTypes.func,
|
||||
video: PropTypes.object,
|
||||
};
|
||||
|
||||
export default OnboardingVideo;
|
||||
@ -1,18 +1,15 @@
|
||||
import React, { useEffect, useReducer } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import axios from 'axios';
|
||||
import React, { useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faQuestion, faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||
import cn from 'classnames';
|
||||
import { useTracking } from '@strapi/helper-plugin';
|
||||
import formatVideoArray from './utils/formatAndStoreVideoArray';
|
||||
import { useConfigurations } from '../../hooks';
|
||||
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') {
|
||||
const { showTutorials } = useConfigurations();
|
||||
|
||||
if (!showTutorials) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -20,108 +17,15 @@ const Onboarding = () => {
|
||||
};
|
||||
|
||||
const OnboardingVideos = () => {
|
||||
const { trackUsage } = useTracking();
|
||||
const [{ isLoading, isOpen, videos }, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
useEffect(() => {
|
||||
const getData = async () => {
|
||||
try {
|
||||
const { data } = await axios.get('https://strapi.io/videos', {
|
||||
timeout: 1000,
|
||||
});
|
||||
const { didWatchVideos, videos } = formatVideoArray(data);
|
||||
|
||||
dispatch({
|
||||
type: 'GET_DATA_SUCCEEDED',
|
||||
didWatchVideos,
|
||||
videos,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
dispatch({
|
||||
type: 'HIDE_VIDEO_ONBOARDING',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
getData();
|
||||
}, []);
|
||||
|
||||
// Hide the player in case of request error
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
const eventName = isOpen
|
||||
? 'didOpenGetStartedVideoContainer'
|
||||
: 'didCloseGetStartedVideoContainer';
|
||||
|
||||
dispatch({ type: 'SET_IS_OPEN' });
|
||||
trackUsage(eventName);
|
||||
setIsOpen(prev => !prev);
|
||||
};
|
||||
const handleClickOpenVideo = videoIndexToOpen => {
|
||||
dispatch({
|
||||
type: 'TOGGLE_VIDEO_MODAL',
|
||||
videoIndexToOpen,
|
||||
});
|
||||
};
|
||||
const handleUpdateVideoStartTime = (videoIndex, elapsedTime) => {
|
||||
dispatch({
|
||||
type: 'UPDATE_VIDEO_STARTED_TIME_AND_PLAYED_INFOS',
|
||||
videoIndex,
|
||||
elapsedTime,
|
||||
});
|
||||
};
|
||||
const setVideoDuration = (videoIndex, duration) => {
|
||||
dispatch({
|
||||
type: 'SET_VIDEO_DURATION',
|
||||
duration,
|
||||
videoIndex,
|
||||
});
|
||||
};
|
||||
|
||||
const hasVideos = videos.length > 0;
|
||||
const className = hasVideos ? 'visible' : 'hidden';
|
||||
|
||||
return (
|
||||
<Wrapper className={className} isOpen={isOpen}>
|
||||
<Wrapper className="visible" isOpen={isOpen}>
|
||||
<div className={cn('videosContent', isOpen ? 'shown' : 'hide')}>
|
||||
<div className="videosHeader">
|
||||
<p>
|
||||
<FormattedMessage id="app.components.Onboarding.title" />
|
||||
</p>
|
||||
<p>
|
||||
{Math.floor((videos.filter(v => v.end).length * 100) / videos.length)}
|
||||
<FormattedMessage id="app.components.Onboarding.label.completed" />
|
||||
</p>
|
||||
</div>
|
||||
<ul className="onboardingList">
|
||||
{videos.map((video, index) => (
|
||||
<Video
|
||||
key={video.id || index}
|
||||
id={index}
|
||||
video={video}
|
||||
onClick={() => handleClickOpenVideo(index)}
|
||||
setVideoDuration={(_, duration) => {
|
||||
setVideoDuration(index, duration);
|
||||
}}
|
||||
getVideoCurrentTime={(_, elapsedTime) => {
|
||||
handleUpdateVideoStartTime(index, elapsedTime);
|
||||
}}
|
||||
didPlayVideo={(_, elapsedTime) => {
|
||||
const eventName = `didPlay${index}GetStartedVideo`;
|
||||
|
||||
trackUsage(eventName, { timestamp: elapsedTime });
|
||||
}}
|
||||
didStopVideo={(_, elapsedTime) => {
|
||||
const eventName = `didStop${index}Video`;
|
||||
|
||||
trackUsage(eventName, { timestamp: elapsedTime });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<StaticLinks />
|
||||
</div>
|
||||
<div className="openBtn">
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
import produce from 'immer';
|
||||
import set from 'lodash/set';
|
||||
|
||||
const initialState = {
|
||||
isLoading: true,
|
||||
isOpen: false,
|
||||
videos: [],
|
||||
};
|
||||
|
||||
const reducer = (state, action) =>
|
||||
// eslint-disable-next-line consistent-return
|
||||
produce(state, draftState => {
|
||||
switch (action.type) {
|
||||
case 'GET_DATA_SUCCEEDED': {
|
||||
draftState.isOpen = !action.didWatchVideos;
|
||||
draftState.isLoading = false;
|
||||
draftState.videos = action.videos;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'SET_IS_OPEN': {
|
||||
draftState.isOpen = !state.isOpen;
|
||||
break;
|
||||
}
|
||||
case 'SET_VIDEO_DURATION': {
|
||||
set(draftState, ['videos', action.videoIndex, 'duration'], parseFloat(action.duration, 10));
|
||||
break;
|
||||
}
|
||||
case 'TOGGLE_VIDEO_MODAL': {
|
||||
const nextVideos = state.videos.map((video, index) => {
|
||||
if (index === action.videoIndexToOpen) {
|
||||
return { ...video, isOpen: !video.isOpen };
|
||||
}
|
||||
|
||||
return { ...video, isOpen: false };
|
||||
});
|
||||
draftState.videos = nextVideos;
|
||||
break;
|
||||
}
|
||||
case 'UPDATE_VIDEO_STARTED_TIME_AND_PLAYED_INFOS': {
|
||||
const nextVideos = state.videos.map((video, index) => {
|
||||
if (index !== action.videoIndex) {
|
||||
return video;
|
||||
}
|
||||
const elapsedTime = parseFloat(action.elapsedTime, 10);
|
||||
const videoDuration = parseFloat(video.duration, 10);
|
||||
const percentElapsedTime = (elapsedTime * 100) / videoDuration;
|
||||
const end = video.end === true ? video.end : percentElapsedTime > 80;
|
||||
|
||||
return { ...video, startTime: elapsedTime, end };
|
||||
});
|
||||
|
||||
// Update the local storage and make sure that the modal video does not automatically open
|
||||
localStorage.setItem(
|
||||
'videos',
|
||||
JSON.stringify(nextVideos.map(v => ({ ...v, isOpen: false })))
|
||||
);
|
||||
// Update the state
|
||||
draftState.videos = nextVideos;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return draftState;
|
||||
}
|
||||
});
|
||||
|
||||
export default reducer;
|
||||
export { initialState };
|
||||
@ -1,27 +0,0 @@
|
||||
const formatVideosArray = array => {
|
||||
const alreadyFetchedVideos = JSON.parse(localStorage.getItem('videos')) || [];
|
||||
const didWatchVideos = alreadyFetchedVideos.length === array.length;
|
||||
let videos;
|
||||
|
||||
if (!didWatchVideos) {
|
||||
videos = array.map(video => {
|
||||
return {
|
||||
...video,
|
||||
duration: null,
|
||||
end: false,
|
||||
isOpen: false,
|
||||
key: video.order,
|
||||
startTime: 0,
|
||||
};
|
||||
});
|
||||
|
||||
// Store the videos in the localStorage
|
||||
localStorage.setItem('videos', JSON.stringify(videos));
|
||||
} else {
|
||||
videos = alreadyFetchedVideos;
|
||||
}
|
||||
|
||||
return { didWatchVideos, videos };
|
||||
};
|
||||
|
||||
export default formatVideosArray;
|
||||
@ -2,10 +2,8 @@ import React, { memo } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import favicon from '../../favicon.png';
|
||||
|
||||
const PageTitle = ({ title }) => {
|
||||
return <Helmet title={title} link={[{ rel: 'icon', type: 'image/png', href: favicon }]} />;
|
||||
return <Helmet title={title} />;
|
||||
};
|
||||
|
||||
PageTitle.propTypes = {
|
||||
|
||||
106
packages/core/admin/admin/src/components/Providers/index.js
Normal file
@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { QueryClientProvider, QueryClient } from 'react-query';
|
||||
import { LibraryProvider, StrapiAppProvider } from '@strapi/helper-plugin';
|
||||
import { Provider } from 'react-redux';
|
||||
import { AdminContext, ConfigurationsContext } from '../../contexts';
|
||||
import LanguageProvider from '../LanguageProvider';
|
||||
import AutoReloadOverlayBlockerProvider from '../AutoReloadOverlayBlockerProvider';
|
||||
import Notifications from '../Notifications';
|
||||
import OverlayBlocker from '../OverlayBlocker';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const Providers = ({
|
||||
authLogo,
|
||||
children,
|
||||
components,
|
||||
fields,
|
||||
getAdminInjectedComponents,
|
||||
getPlugin,
|
||||
localeNames,
|
||||
menu,
|
||||
menuLogo,
|
||||
messages,
|
||||
plugins,
|
||||
runHookParallel,
|
||||
runHookSeries,
|
||||
runHookWaterfall,
|
||||
settings,
|
||||
showReleaseNotification,
|
||||
showTutorials,
|
||||
|
||||
store,
|
||||
}) => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<AdminContext.Provider value={{ getAdminInjectedComponents }}>
|
||||
<ConfigurationsContext.Provider
|
||||
value={{ authLogo, menuLogo, showReleaseNotification, showTutorials }}
|
||||
>
|
||||
<StrapiAppProvider
|
||||
getPlugin={getPlugin}
|
||||
menu={menu}
|
||||
plugins={plugins}
|
||||
runHookParallel={runHookParallel}
|
||||
runHookWaterfall={runHookWaterfall}
|
||||
runHookSeries={runHookSeries}
|
||||
settings={settings}
|
||||
>
|
||||
<LibraryProvider components={components} fields={fields}>
|
||||
<LanguageProvider messages={messages} localeNames={localeNames}>
|
||||
<AutoReloadOverlayBlockerProvider>
|
||||
<OverlayBlocker>
|
||||
<Notifications>{children}</Notifications>
|
||||
</OverlayBlocker>
|
||||
</AutoReloadOverlayBlockerProvider>
|
||||
</LanguageProvider>
|
||||
</LibraryProvider>
|
||||
</StrapiAppProvider>
|
||||
</ConfigurationsContext.Provider>
|
||||
</AdminContext.Provider>
|
||||
</Provider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
Providers.propTypes = {
|
||||
authLogo: PropTypes.oneOfType([PropTypes.string, PropTypes.any]).isRequired,
|
||||
children: PropTypes.element.isRequired,
|
||||
components: PropTypes.object.isRequired,
|
||||
fields: PropTypes.object.isRequired,
|
||||
getAdminInjectedComponents: PropTypes.func.isRequired,
|
||||
getPlugin: PropTypes.func.isRequired,
|
||||
localeNames: PropTypes.objectOf(PropTypes.string).isRequired,
|
||||
menu: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
to: PropTypes.string.isRequired,
|
||||
icon: PropTypes.string,
|
||||
intlLabel: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
permissions: PropTypes.array,
|
||||
Component: PropTypes.func,
|
||||
})
|
||||
).isRequired,
|
||||
menuLogo: PropTypes.oneOfType([PropTypes.string, PropTypes.any]).isRequired,
|
||||
messages: PropTypes.object.isRequired,
|
||||
plugins: PropTypes.object.isRequired,
|
||||
runHookParallel: PropTypes.func.isRequired,
|
||||
runHookWaterfall: PropTypes.func.isRequired,
|
||||
runHookSeries: PropTypes.func.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
showReleaseNotification: PropTypes.bool.isRequired,
|
||||
showTutorials: PropTypes.bool.isRequired,
|
||||
store: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Providers;
|
||||
20
packages/core/admin/admin/src/components/Theme/index.js
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
import GlobalStyle from '../GlobalStyle';
|
||||
import Fonts from '../Fonts';
|
||||
|
||||
const Theme = ({ children, theme }) => (
|
||||
<ThemeProvider theme={theme}>
|
||||
<GlobalStyle />
|
||||
<Fonts />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
Theme.propTypes = {
|
||||
children: PropTypes.element.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Theme;
|
||||
@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from 'react-intl';
|
||||
import LinkNotification from '../LinkNotification';
|
||||
import basename from '../../../utils/basename';
|
||||
import basename from '../../../core/utils/basename';
|
||||
|
||||
const MagicLink = ({ registrationToken }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
|
Before Width: | Height: | Size: 648 B After Width: | Height: | Size: 648 B |
|
Before Width: | Height: | Size: 364 B After Width: | Height: | Size: 364 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 244 B After Width: | Height: | Size: 244 B |
@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import { DateTime } from '@buffetjs/custom';
|
||||
import { DatePicker, InputText, InputNumber, Select, TimePicker } from '@buffetjs/core';
|
||||
import { DateWrapper } from './components';
|
||||
|
||||
function GenericInput({ type, onChange, value, ...rest }) {
|
||||
switch (type) {
|
||||
case 'boolean':
|
||||
return <Select onChange={e => onChange(e.target.value)} value={value} {...rest} />;
|
||||
|
||||
case 'date':
|
||||
case 'timestamp':
|
||||
case 'timestampUpdate': {
|
||||
const momentValue = moment(value);
|
||||
|
||||
return (
|
||||
<DateWrapper type={type}>
|
||||
<DatePicker
|
||||
onChange={e => {
|
||||
if (e.target.value) {
|
||||
onChange(e.target.value.format('YYYY-MM-DD'));
|
||||
}
|
||||
}}
|
||||
value={momentValue}
|
||||
{...rest}
|
||||
/>
|
||||
</DateWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
case 'datetime': {
|
||||
const momentValue = moment(value);
|
||||
|
||||
return (
|
||||
<DateWrapper type={type}>
|
||||
<DateTime
|
||||
onChange={e => {
|
||||
if (e.target.value) {
|
||||
onChange(e.target.value.toISOString());
|
||||
}
|
||||
}}
|
||||
value={momentValue}
|
||||
{...rest}
|
||||
/>
|
||||
</DateWrapper>
|
||||
);
|
||||
}
|
||||
case 'enumeration':
|
||||
return <Select onChange={e => onChange(e.target.value)} value={value} {...rest} />;
|
||||
|
||||
case 'integer':
|
||||
case 'decimal':
|
||||
case 'float':
|
||||
return <InputNumber onChange={e => onChange(e.target.value)} value={value} {...rest} />;
|
||||
|
||||
case 'time':
|
||||
return <TimePicker onChange={e => onChange(e.target.value)} value={value} {...rest} />;
|
||||
|
||||
/**
|
||||
* "biginteger" type falls into this section
|
||||
*/
|
||||
default:
|
||||
return <InputText onChange={e => onChange(e.target.value)} value={value} {...rest} />;
|
||||
}
|
||||
}
|
||||
|
||||
GenericInput.defaultProps = {
|
||||
value: undefined,
|
||||
};
|
||||
|
||||
GenericInput.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
value: PropTypes.any,
|
||||
};
|
||||
|
||||
export default GenericInput;
|
||||
@ -0,0 +1,24 @@
|
||||
import { Button } from '@buffetjs/core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const StyledButton = styled(Button)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const FormWrapper = styled.form`
|
||||
min-width: 330px;
|
||||
max-width: 400px;
|
||||
padding: 13px 15px;
|
||||
|
||||
& > * + * {
|
||||
margin-top: 11px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DateWrapper = styled.div`
|
||||
display: ${({ type }) => (type === 'datetime' ? 'flex' : 'block')};
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,47 @@
|
||||
import get from 'lodash/get';
|
||||
import { useRBACProvider, findMatchingPermissions } from '@strapi/helper-plugin';
|
||||
|
||||
const NOT_ALLOWED_FILTERS = ['json', 'component', 'media', 'richtext', 'dynamiczone'];
|
||||
|
||||
const useAllowedAttributes = (contentType, slug) => {
|
||||
const { allPermissions } = useRBACProvider();
|
||||
|
||||
let timestamps = get(contentType, ['options', 'timestamps']);
|
||||
|
||||
if (!Array.isArray(timestamps)) {
|
||||
timestamps = [];
|
||||
}
|
||||
|
||||
const readPermissionsForSlug = findMatchingPermissions(allPermissions, [
|
||||
{
|
||||
action: 'plugins::content-manager.explorer.read',
|
||||
subject: slug,
|
||||
},
|
||||
]);
|
||||
|
||||
const readPermissionForAttr = get(readPermissionsForSlug, ['0', 'properties', 'fields'], []);
|
||||
const attributesArray = Object.keys(get(contentType, ['attributes']), {});
|
||||
const allowedAttributes = attributesArray
|
||||
.filter(attr => {
|
||||
const current = get(contentType, ['attributes', attr], {});
|
||||
|
||||
if (!current.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (NOT_ALLOWED_FILTERS.includes(current.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!readPermissionForAttr.includes(attr) && attr !== 'id' && !timestamps.includes(attr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.sort();
|
||||
|
||||
return allowedAttributes;
|
||||
};
|
||||
|
||||
export default useAllowedAttributes;
|
||||
@ -0,0 +1,115 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Picker, Select } from '@buffetjs/core';
|
||||
import {
|
||||
FilterIcon,
|
||||
getFilterType as comparatorsForType,
|
||||
useTracking,
|
||||
useQueryParams,
|
||||
} from '@strapi/helper-plugin';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import useAllowedAttributes from './hooks/useAllowedAttributes';
|
||||
import getTrad from '../../utils/getTrad';
|
||||
import formatAttribute from './utils/formatAttribute';
|
||||
import getAttributeType from './utils/getAttributeType';
|
||||
import GenericInput from './GenericInput';
|
||||
import { StyledButton, FormWrapper } from './components';
|
||||
|
||||
const AttributeFilter = ({ contentType, slug, metaData }) => {
|
||||
const { trackUsage } = useTracking();
|
||||
const [{ query }, setQuery] = useQueryParams();
|
||||
|
||||
const allowedAttributes = useAllowedAttributes(contentType, slug);
|
||||
const [attribute, setAttribute] = useState(allowedAttributes[0]);
|
||||
|
||||
const attributeType = getAttributeType(attribute, contentType, metaData);
|
||||
const comparators = comparatorsForType(attributeType);
|
||||
const [comparator, setComparator] = useState(comparators[0].value);
|
||||
|
||||
const [value, setValue] = useState();
|
||||
|
||||
return (
|
||||
<Picker
|
||||
renderButtonContent={() => (
|
||||
<>
|
||||
<FilterIcon />
|
||||
<FormattedMessage id="app.utils.filters" />
|
||||
</>
|
||||
)}
|
||||
renderSectionContent={onToggle => {
|
||||
const handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
|
||||
const formattedAttribute = formatAttribute(attribute, metaData);
|
||||
|
||||
/**
|
||||
* When dealing with a "=" comparator, the filter should have a shape of {'attributeName': 'some value}
|
||||
* otherwise, it should look like { 'attributeName_comparatorName' : 'some value' }
|
||||
*/
|
||||
const newFilter =
|
||||
comparator === '='
|
||||
? { [formattedAttribute]: value }
|
||||
: { [`${formattedAttribute}${comparator}`]: value };
|
||||
|
||||
/**
|
||||
* Pushing the filter in the URL for later refreshes or fast access
|
||||
*/
|
||||
const actualQuery = query || {};
|
||||
const _where = actualQuery._where || [];
|
||||
_where.push(newFilter);
|
||||
setQuery({ ...actualQuery, _where, page: 1 });
|
||||
|
||||
/**
|
||||
* Tracking stuff
|
||||
*/
|
||||
const useRelation = _where.some(obj => Object.keys(obj)[0].includes('.'));
|
||||
trackUsage('didFilterEntries', { useRelation });
|
||||
|
||||
/**
|
||||
* Reset to initial state
|
||||
*/
|
||||
setAttribute(allowedAttributes[0]);
|
||||
setComparator(comparators[0].value);
|
||||
setValue(undefined);
|
||||
|
||||
onToggle();
|
||||
};
|
||||
|
||||
return (
|
||||
<FormWrapper onSubmit={handleSubmit}>
|
||||
<Select
|
||||
onChange={e => setAttribute(e.target.value)}
|
||||
name="ct-filter"
|
||||
options={allowedAttributes}
|
||||
value={attribute}
|
||||
/>
|
||||
<Select
|
||||
onChange={e => setComparator(e.target.value)}
|
||||
name="comparator"
|
||||
value={comparator}
|
||||
options={comparators.map(comparator => (
|
||||
<FormattedMessage id={comparator.id} key={comparator.value}>
|
||||
{msg => <option value={comparator.value}>{msg}</option>}
|
||||
</FormattedMessage>
|
||||
))}
|
||||
/>
|
||||
<GenericInput name="input" onChange={setValue} type={attributeType} value={value} />
|
||||
<StyledButton icon type="submit">
|
||||
<FormattedMessage
|
||||
id={getTrad('components.FiltersPickWrapper.PluginHeader.actions.apply')}
|
||||
/>
|
||||
</StyledButton>
|
||||
</FormWrapper>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
AttributeFilter.propTypes = {
|
||||
contentType: PropTypes.object.isRequired,
|
||||
metaData: PropTypes.object.isRequired,
|
||||
slug: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default AttributeFilter;
|
||||
@ -0,0 +1,388 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import AttributeFilter from '..';
|
||||
import addressCt from '../../../../../../../../admin-test-utils/lib/fixtures/collectionTypes/address';
|
||||
import addressMetaData from '../../../../../../../../admin-test-utils/lib/fixtures/metaData/address';
|
||||
|
||||
class MockDate extends Date {
|
||||
constructor() {
|
||||
super(1992, 5, 21);
|
||||
}
|
||||
}
|
||||
|
||||
jest.mock('react-intl', () => ({
|
||||
// eslint-disable-next-line react/prop-types
|
||||
FormattedMessage: ({ id }) => <option value={id}>{id}</option>,
|
||||
}));
|
||||
|
||||
jest.mock('@strapi/helper-plugin', () => ({
|
||||
...jest.requireActual('@strapi/helper-plugin'),
|
||||
useRBACProvider: () => ({
|
||||
allPermissions: [
|
||||
{
|
||||
id: 198,
|
||||
action: 'plugins::content-manager.explorer.create',
|
||||
subject: 'application::address.address',
|
||||
properties: {
|
||||
fields: [
|
||||
'postal_coder',
|
||||
'categories',
|
||||
'cover',
|
||||
'images',
|
||||
'city',
|
||||
'likes',
|
||||
'json',
|
||||
'slug',
|
||||
'notrepeat_req.name',
|
||||
'repeat_req.name',
|
||||
'repeat_req_min.name',
|
||||
],
|
||||
locales: ['en'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 199,
|
||||
action: 'plugins::content-manager.explorer.read',
|
||||
subject: 'application::address.address',
|
||||
properties: {
|
||||
fields: [
|
||||
'postal_coder',
|
||||
'categories',
|
||||
'cover',
|
||||
'images',
|
||||
'city',
|
||||
'likes',
|
||||
'json',
|
||||
'slug',
|
||||
'notrepeat_req.name',
|
||||
'repeat_req.name',
|
||||
'repeat_req_min.name',
|
||||
],
|
||||
locales: ['en'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 200,
|
||||
action: 'plugins::content-manager.explorer.update',
|
||||
subject: 'application::address.address',
|
||||
properties: {
|
||||
fields: [
|
||||
'postal_coder',
|
||||
'categories',
|
||||
'cover',
|
||||
'images',
|
||||
'city',
|
||||
'likes',
|
||||
'json',
|
||||
'slug',
|
||||
'notrepeat_req.name',
|
||||
'repeat_req.name',
|
||||
'repeat_req_min.name',
|
||||
],
|
||||
locales: ['en'],
|
||||
},
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 258,
|
||||
action: 'plugins::content-manager.explorer.delete',
|
||||
subject: 'application::address.address',
|
||||
properties: { locales: ['en'] },
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
id: 269,
|
||||
action: 'plugins::content-manager.explorer.publish',
|
||||
subject: 'application::address.address',
|
||||
properties: { locales: ['en'] },
|
||||
conditions: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
}));
|
||||
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<AttributeFilter
|
||||
contentType={addressCt}
|
||||
metaData={addressMetaData}
|
||||
slug="application::address.address"
|
||||
/>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
describe('AttributeFilter', () => {
|
||||
let realDate;
|
||||
|
||||
beforeEach(() => {
|
||||
realDate = global.Date;
|
||||
global.Date = MockDate;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.Date = realDate;
|
||||
});
|
||||
|
||||
it('snapshots the filter dropdown with a set of valid fields', () => {
|
||||
const { container } = renderComponent();
|
||||
|
||||
expect(container.querySelector('#ct-filter')).toMatchInlineSnapshot(`
|
||||
.c0 {
|
||||
width: 100%;
|
||||
height: 3.4rem;
|
||||
padding: 0 1rem;
|
||||
font-weight: 400;
|
||||
font-size: 1.3rem;
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
border: 1px solid #E3E9F3;
|
||||
border-radius: 2px;
|
||||
color: #333740;
|
||||
background-color: #ffffff;
|
||||
padding-right: 30px;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjMiIGhlaWdodD0iMzIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNLjAxOCAwaDIwYTIgMiAwIDAgMSAyIDJ2MjhhMiAyIDAgMCAxLTIgMmgtMjBWMHoiIGZpbGw9IiNGQUZBRkIiLz48ZyBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiNCM0I1QjkiPjxwYXRoIGQ9Ik0xNC4wMTggMTguMzc1YS4zNi4zNiAwIDAgMS0uMTEyLjI2NGwtMi42MjUgMi42MjVhLjM2LjM2IDAgMCAxLS4yNjMuMTExLjM2LjM2IDAgMCAxLS4yNjQtLjExMWwtMi42MjUtMi42MjVhLjM2LjM2IDAgMCAxLS4xMTEtLjI2NC4zNi4zNiAwIDAgMSAuMTExLS4yNjQuMzYuMzYgMCAwIDEgLjI2NC0uMTExaDUuMjVhLjM2LjM2IDAgMCAxIC4yNjMuMTExLjM2LjM2IDAgMCAxIC4xMTIuMjY0ek04LjAxOCAxNWEuMzYuMzYgMCAwIDEgLjExMS0uMjY0bDIuNjI1LTIuNjI1YS4zNi4zNiAwIDAgMSAuMjY0LS4xMTEuMzYuMzYgMCAwIDEgLjI2My4xMTFsMi42MjUgMi42MjVhLjM2LjM2IDAgMCAxIC4xMTIuMjY0LjM2LjM2IDAgMCAxLS4xMTIuMjY0LjM2LjM2IDAgMCAxLS4yNjMuMTExaC01LjI1YS4zNi4zNiAwIDAgMS0uMjY0LS4xMTEuMzYuMzYgMCAwIDEtLjExMS0uMjY0eiIvPjwvZz48L2c+PC9zdmc+Cg==);
|
||||
background-repeat: no-repeat;
|
||||
background-position: right;
|
||||
}
|
||||
|
||||
.c0::-webkit-input-placeholder {
|
||||
color: #919BAE;
|
||||
}
|
||||
|
||||
.c0:focus {
|
||||
border-color: #78caff;
|
||||
}
|
||||
|
||||
.c0:disabled {
|
||||
background-color: #FAFAFB;
|
||||
cursor: not-allowed;
|
||||
color: #9ea7b8;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
<select
|
||||
autocomplete="off"
|
||||
class="c0"
|
||||
id="ct-filter"
|
||||
name="ct-filter"
|
||||
tabindex="0"
|
||||
>
|
||||
<option
|
||||
value="categories"
|
||||
>
|
||||
categories
|
||||
</option>
|
||||
<option
|
||||
value="city"
|
||||
>
|
||||
city
|
||||
</option>
|
||||
<option
|
||||
value="created_at"
|
||||
>
|
||||
created_at
|
||||
</option>
|
||||
<option
|
||||
value="id"
|
||||
>
|
||||
id
|
||||
</option>
|
||||
<option
|
||||
value="likes"
|
||||
>
|
||||
likes
|
||||
</option>
|
||||
<option
|
||||
value="postal_coder"
|
||||
>
|
||||
postal_coder
|
||||
</option>
|
||||
<option
|
||||
value="slug"
|
||||
>
|
||||
slug
|
||||
</option>
|
||||
<option
|
||||
value="updated_at"
|
||||
>
|
||||
updated_at
|
||||
</option>
|
||||
</select>
|
||||
`);
|
||||
});
|
||||
|
||||
it('snapshots the comparator dropdown with a set of valid comparator for the type', () => {
|
||||
const { container } = renderComponent();
|
||||
|
||||
expect(container.querySelector('#comparator')).toMatchInlineSnapshot(`
|
||||
.c0 {
|
||||
width: 100%;
|
||||
height: 3.4rem;
|
||||
padding: 0 1rem;
|
||||
font-weight: 400;
|
||||
font-size: 1.3rem;
|
||||
cursor: pointer;
|
||||
outline: 0;
|
||||
border: 1px solid #E3E9F3;
|
||||
border-radius: 2px;
|
||||
color: #333740;
|
||||
background-color: #ffffff;
|
||||
padding-right: 30px;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjMiIGhlaWdodD0iMzIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNLjAxOCAwaDIwYTIgMiAwIDAgMSAyIDJ2MjhhMiAyIDAgMCAxLTIgMmgtMjBWMHoiIGZpbGw9IiNGQUZBRkIiLz48ZyBmaWxsLXJ1bGU9Im5vbnplcm8iIGZpbGw9IiNCM0I1QjkiPjxwYXRoIGQ9Ik0xNC4wMTggMTguMzc1YS4zNi4zNiAwIDAgMS0uMTEyLjI2NGwtMi42MjUgMi42MjVhLjM2LjM2IDAgMCAxLS4yNjMuMTExLjM2LjM2IDAgMCAxLS4yNjQtLjExMWwtMi42MjUtMi42MjVhLjM2LjM2IDAgMCAxLS4xMTEtLjI2NC4zNi4zNiAwIDAgMSAuMTExLS4yNjQuMzYuMzYgMCAwIDEgLjI2NC0uMTExaDUuMjVhLjM2LjM2IDAgMCAxIC4yNjMuMTExLjM2LjM2IDAgMCAxIC4xMTIuMjY0ek04LjAxOCAxNWEuMzYuMzYgMCAwIDEgLjExMS0uMjY0bDIuNjI1LTIuNjI1YS4zNi4zNiAwIDAgMSAuMjY0LS4xMTEuMzYuMzYgMCAwIDEgLjI2My4xMTFsMi42MjUgMi42MjVhLjM2LjM2IDAgMCAxIC4xMTIuMjY0LjM2LjM2IDAgMCAxLS4xMTIuMjY0LjM2LjM2IDAgMCAxLS4yNjMuMTExaC01LjI1YS4zNi4zNiAwIDAgMS0uMjY0LS4xMTEuMzYuMzYgMCAwIDEtLjExMS0uMjY0eiIvPjwvZz48L2c+PC9zdmc+Cg==);
|
||||
background-repeat: no-repeat;
|
||||
background-position: right;
|
||||
}
|
||||
|
||||
.c0::-webkit-input-placeholder {
|
||||
color: #919BAE;
|
||||
}
|
||||
|
||||
.c0:focus {
|
||||
border-color: #78caff;
|
||||
}
|
||||
|
||||
.c0:disabled {
|
||||
background-color: #FAFAFB;
|
||||
cursor: not-allowed;
|
||||
color: #9ea7b8;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
<select
|
||||
autocomplete="off"
|
||||
class="c0"
|
||||
id="comparator"
|
||||
name="comparator"
|
||||
tabindex="0"
|
||||
>
|
||||
<option
|
||||
value="components.FilterOptions.FILTER_TYPES.="
|
||||
>
|
||||
components.FilterOptions.FILTER_TYPES.=
|
||||
</option>
|
||||
<option
|
||||
value="components.FilterOptions.FILTER_TYPES._ne"
|
||||
>
|
||||
components.FilterOptions.FILTER_TYPES._ne
|
||||
</option>
|
||||
<option
|
||||
value="components.FilterOptions.FILTER_TYPES._lt"
|
||||
>
|
||||
components.FilterOptions.FILTER_TYPES._lt
|
||||
</option>
|
||||
<option
|
||||
value="components.FilterOptions.FILTER_TYPES._lte"
|
||||
>
|
||||
components.FilterOptions.FILTER_TYPES._lte
|
||||
</option>
|
||||
<option
|
||||
value="components.FilterOptions.FILTER_TYPES._gt"
|
||||
>
|
||||
components.FilterOptions.FILTER_TYPES._gt
|
||||
</option>
|
||||
<option
|
||||
value="components.FilterOptions.FILTER_TYPES._gte"
|
||||
>
|
||||
components.FilterOptions.FILTER_TYPES._gte
|
||||
</option>
|
||||
<option
|
||||
value="components.FilterOptions.FILTER_TYPES._contains"
|
||||
>
|
||||
components.FilterOptions.FILTER_TYPES._contains
|
||||
</option>
|
||||
<option
|
||||
value="components.FilterOptions.FILTER_TYPES._containss"
|
||||
>
|
||||
components.FilterOptions.FILTER_TYPES._containss
|
||||
</option>
|
||||
</select>
|
||||
`);
|
||||
});
|
||||
|
||||
it('changes the input component when selecting an attribute with a different type', () => {
|
||||
const { container } = renderComponent();
|
||||
|
||||
fireEvent.change(container.querySelector('#ct-filter'), { target: { value: 'updated_at' } });
|
||||
|
||||
expect(container.querySelector('#date')).toMatchInlineSnapshot(`
|
||||
.c0 {
|
||||
width: 100%;
|
||||
height: 3.4rem;
|
||||
padding: 0 1rem;
|
||||
font-weight: 400;
|
||||
font-size: 1.3rem;
|
||||
cursor: text;
|
||||
outline: 0;
|
||||
border: 1px solid #E3E9F3;
|
||||
border-radius: 2px;
|
||||
color: #333740;
|
||||
background-color: transparent;
|
||||
padding-left: calc(3.4rem + 1rem);
|
||||
}
|
||||
|
||||
.c0::-webkit-input-placeholder {
|
||||
color: #919BAE;
|
||||
}
|
||||
|
||||
.c0:focus {
|
||||
border-color: #78caff;
|
||||
}
|
||||
|
||||
.c0:disabled {
|
||||
background-color: #FAFAFB;
|
||||
cursor: not-allowed;
|
||||
color: #9ea7b8;
|
||||
}
|
||||
|
||||
<input
|
||||
autocomplete="off"
|
||||
class="c0"
|
||||
id="date"
|
||||
name="start_date"
|
||||
tabindex="0"
|
||||
type="text"
|
||||
value="June 21, 1992"
|
||||
/>
|
||||
`);
|
||||
});
|
||||
|
||||
it('pushes the query in the URl when validating the filter form using the "equal comparator"', () => {
|
||||
const { container } = renderComponent();
|
||||
|
||||
fireEvent.change(container.querySelector('#input'), { target: { value: 'hello world' } });
|
||||
fireEvent.click(container.querySelector('[type="submit"]'));
|
||||
|
||||
expect(window.location.href).toBe(
|
||||
'http://localhost:4000/admin?_where[0][categories.name]=hello%20world&page=1'
|
||||
);
|
||||
});
|
||||
|
||||
it('pushes the query in the URl when validating the filter form using the "not equal comparator"', () => {
|
||||
const { container } = renderComponent();
|
||||
|
||||
fireEvent.change(container.querySelector('#comparator'), {
|
||||
target: { value: 'components.FilterOptions.FILTER_TYPES._ne' },
|
||||
});
|
||||
|
||||
fireEvent.change(container.querySelector('#input'), { target: { value: 'hello world' } });
|
||||
fireEvent.click(container.querySelector('[type="submit"]'));
|
||||
|
||||
expect(window.location.href).toBe(
|
||||
'http://localhost:4000/admin?_where[0][categories.name]=hello%20world&_where[1][categories.namecomponents.FilterOptions.FILTER_TYPES._ne]=hello%20world&page=1'
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,13 @@
|
||||
import get from 'lodash/get';
|
||||
|
||||
const formatAttribute = (attributeName, metaData) => {
|
||||
const mainField = get(metaData, [attributeName, 'list', 'mainField', 'name']);
|
||||
|
||||
if (mainField) {
|
||||
return `${attributeName}.${mainField}`;
|
||||
}
|
||||
|
||||
return attributeName;
|
||||
};
|
||||
|
||||
export default formatAttribute;
|
||||
@ -0,0 +1,13 @@
|
||||
import get from 'lodash/get';
|
||||
|
||||
const getAttributeType = (attributeName, contentType, metaData) => {
|
||||
let attributeType = get(contentType, ['attributes', attributeName, 'type'], '');
|
||||
|
||||
if (attributeType === 'relation') {
|
||||
attributeType = get(metaData, [attributeName, 'list', 'mainField', 'schema', 'type'], 'string');
|
||||
}
|
||||
|
||||
return attributeType === 'string' ? 'text' : attributeType;
|
||||
};
|
||||
|
||||
export default getAttributeType;
|
||||
@ -1,8 +1,8 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { get } from 'lodash';
|
||||
import axios from 'axios';
|
||||
import get from 'lodash/get';
|
||||
import {
|
||||
request,
|
||||
useTracking,
|
||||
useNotification,
|
||||
useQueryParams,
|
||||
@ -12,8 +12,13 @@ import {
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { createDefaultForm, getTrad, removePasswordFieldsFromData } from '../../utils';
|
||||
import pluginId from '../../pluginId';
|
||||
import { axiosInstance } from '../../../core/utils';
|
||||
import {
|
||||
createDefaultForm,
|
||||
getTrad,
|
||||
getRequestUrl,
|
||||
removePasswordFieldsFromData,
|
||||
} from '../../utils';
|
||||
import { useFindRedirectionLink } from '../../hooks';
|
||||
import {
|
||||
getData,
|
||||
@ -25,7 +30,6 @@ import {
|
||||
submitSucceeded,
|
||||
} from '../../sharedReducers/crudReducer/actions';
|
||||
import selectCrudReducer from '../../sharedReducers/crudReducer/selectors';
|
||||
import { getRequestUrl } from './utils';
|
||||
|
||||
// This container is used to handle the CRUD
|
||||
const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }) => {
|
||||
@ -55,7 +59,7 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
|
||||
return null;
|
||||
}
|
||||
|
||||
return getRequestUrl(`${slug}/${origin || id}`);
|
||||
return getRequestUrl(`collection-types/${slug}/${origin || id}`);
|
||||
}, [slug, id, isCreatingEntry, origin]);
|
||||
|
||||
const cleanClonedData = useCallback(
|
||||
@ -127,18 +131,18 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
const { signal } = abortController;
|
||||
const CancelToken = axios.CancelToken;
|
||||
const source = CancelToken.source();
|
||||
|
||||
const fetchData = async signal => {
|
||||
const fetchData = async source => {
|
||||
dispatch(getData());
|
||||
|
||||
try {
|
||||
const data = await request(requestURL, { method: 'GET', signal });
|
||||
const { data } = await axiosInstance.get(requestURL, { cancelToken: source.token });
|
||||
|
||||
dispatch(getDataSucceeded(cleanReceivedData(cleanClonedData(data))));
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') {
|
||||
if (axios.isCancel(err)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -175,13 +179,13 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
|
||||
}
|
||||
|
||||
if (requestURL) {
|
||||
fetchData(signal);
|
||||
fetchData(source);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
source.cancel('Operation canceled by the user.');
|
||||
};
|
||||
}, [
|
||||
cleanClonedData,
|
||||
@ -218,9 +222,9 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
|
||||
try {
|
||||
trackUsageRef.current('willDeleteEntry', trackerProperty);
|
||||
|
||||
const response = await request(getRequestUrl(`${slug}/${id}`), {
|
||||
method: 'DELETE',
|
||||
});
|
||||
const { data } = await axiosInstance.delete(
|
||||
getRequestUrl(`collection-types/${slug}/${id}`)
|
||||
);
|
||||
|
||||
toggleNotification({
|
||||
type: 'success',
|
||||
@ -229,7 +233,7 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
|
||||
|
||||
trackUsageRef.current('didDeleteEntry', trackerProperty);
|
||||
|
||||
return Promise.resolve(response);
|
||||
return Promise.resolve(data);
|
||||
} catch (err) {
|
||||
trackUsageRef.current('didNotDeleteEntry', { error: err, ...trackerProperty });
|
||||
|
||||
@ -245,13 +249,13 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
|
||||
|
||||
const onPost = useCallback(
|
||||
async (body, trackerProperty) => {
|
||||
const endPoint = `${getRequestUrl(slug)}${rawQuery}`;
|
||||
const endPoint = `${getRequestUrl(`collection-types/${slug}`)}${rawQuery}`;
|
||||
|
||||
try {
|
||||
// Show a loading button in the EditView/Header.js && lock the app => no navigation
|
||||
dispatch(setStatus('submit-pending'));
|
||||
|
||||
const response = await request(endPoint, { method: 'POST', body });
|
||||
const { data } = await axiosInstance.post(endPoint, body);
|
||||
|
||||
trackUsageRef.current('didCreateEntry', trackerProperty);
|
||||
toggleNotification({
|
||||
@ -259,11 +263,11 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
|
||||
message: { id: getTrad('success.record.save') },
|
||||
});
|
||||
|
||||
dispatch(submitSucceeded(cleanReceivedData(response)));
|
||||
dispatch(submitSucceeded(cleanReceivedData(data)));
|
||||
// Enable navigation and remove loaders
|
||||
dispatch(setStatus('resolved'));
|
||||
|
||||
replace(`/plugins/${pluginId}/collectionType/${slug}/${response.id}${rawQuery}`);
|
||||
replace(`/content-manager/collectionType/${slug}/${data.id}${rawQuery}`);
|
||||
} catch (err) {
|
||||
trackUsageRef.current('didNotCreateEntry', { error: err, trackerProperty });
|
||||
displayErrors(err);
|
||||
@ -276,11 +280,11 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
|
||||
const onPublish = useCallback(async () => {
|
||||
try {
|
||||
trackUsageRef.current('willPublishEntry');
|
||||
const endPoint = getRequestUrl(`${slug}/${id}/actions/publish`);
|
||||
const endPoint = getRequestUrl(`collection-types/${slug}/${id}/actions/publish`);
|
||||
|
||||
dispatch(setStatus('publish-pending'));
|
||||
|
||||
const data = await request(endPoint, { method: 'POST' });
|
||||
const { data } = await axiosInstance.post(endPoint);
|
||||
|
||||
trackUsageRef.current('didPublishEntry');
|
||||
|
||||
@ -299,14 +303,14 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
|
||||
|
||||
const onPut = useCallback(
|
||||
async (body, trackerProperty) => {
|
||||
const endPoint = getRequestUrl(`${slug}/${id}`);
|
||||
const endPoint = getRequestUrl(`collection-types/${slug}/${id}`);
|
||||
|
||||
try {
|
||||
trackUsageRef.current('willEditEntry', trackerProperty);
|
||||
|
||||
dispatch(setStatus('submit-pending'));
|
||||
|
||||
const response = await request(endPoint, { method: 'PUT', body });
|
||||
const { data } = await axiosInstance.put(endPoint, body);
|
||||
|
||||
trackUsageRef.current('didEditEntry', { trackerProperty });
|
||||
toggleNotification({
|
||||
@ -314,7 +318,7 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
|
||||
message: { id: getTrad('success.record.save') },
|
||||
});
|
||||
|
||||
dispatch(submitSucceeded(cleanReceivedData(response)));
|
||||
dispatch(submitSucceeded(cleanReceivedData(data)));
|
||||
|
||||
dispatch(setStatus('resolved'));
|
||||
} catch (err) {
|
||||
@ -328,14 +332,14 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
|
||||
);
|
||||
|
||||
const onUnpublish = useCallback(async () => {
|
||||
const endPoint = getRequestUrl(`${slug}/${id}/actions/unpublish`);
|
||||
const endPoint = getRequestUrl(`collection-types/${slug}/${id}/actions/unpublish`);
|
||||
|
||||
dispatch(setStatus('unpublish-pending'));
|
||||
|
||||
try {
|
||||
trackUsageRef.current('willUnpublishEntry');
|
||||
|
||||
const response = await request(endPoint, { method: 'POST' });
|
||||
const { data } = await axiosInstance.post(endPoint);
|
||||
|
||||
trackUsageRef.current('didUnpublishEntry');
|
||||
toggleNotification({
|
||||
@ -343,7 +347,7 @@ const CollectionTypeFormWrapper = ({ allLayoutData, children, slug, id, origin }
|
||||
message: { id: getTrad('success.record.unpublish') },
|
||||
});
|
||||
|
||||
dispatch(submitSucceeded(cleanReceivedData(response)));
|
||||
dispatch(submitSucceeded(cleanReceivedData(data)));
|
||||
dispatch(setStatus('resolved'));
|
||||
} catch (err) {
|
||||
dispatch(setStatus('resolved'));
|
||||
@ -2,8 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useContentManagerEditViewDataManager } from '@strapi/helper-plugin';
|
||||
import pluginId from '../../pluginId';
|
||||
|
||||
import { getTrad } from '../../utils';
|
||||
import NonRepeatableWrapper from '../NonRepeatableWrapper';
|
||||
import PlusButton from '../PlusButton';
|
||||
import P from './P';
|
||||
@ -22,7 +21,7 @@ const ComponentInitializer = ({ componentUid, isReadOnly, name }) => {
|
||||
}}
|
||||
>
|
||||
<PlusButton type="button" />
|
||||
<FormattedMessage id={`${pluginId}.components.empty-repeatable`}>
|
||||
<FormattedMessage id={getTrad('components.empty-repeatable')}>
|
||||
{msg => <P style={{ paddingTop: 78 }}>{msg}</P>}
|
||||
</FormattedMessage>
|
||||
</NonRepeatableWrapper>
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { memo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import pluginId from '../../../pluginId';
|
||||
import { getTrad } from '../../../utils';
|
||||
import useListView from '../../../hooks/useListView';
|
||||
import DeleteAll from './DeleteAll';
|
||||
import Delete from './Delete';
|
||||
@ -18,12 +18,12 @@ function ActionCollapse({ colSpan }) {
|
||||
<Wrapper colSpan={colSpan}>
|
||||
<td colSpan={colSpan}>
|
||||
<FormattedMessage
|
||||
id={`${pluginId}.components.TableDelete.entries.${suffix}`}
|
||||
id={getTrad(`components.TableDelete.entries.${suffix}`)}
|
||||
values={{ number }}
|
||||
>
|
||||
{message => <Delete>{message}</Delete>}
|
||||
</FormattedMessage>
|
||||
<FormattedMessage id={`${pluginId}.components.TableDelete.${deleteMessageId}`}>
|
||||
<FormattedMessage id={getTrad(`components.TableDelete.${deleteMessageId}`)}>
|
||||
{message => <DeleteAll onClick={toggleModalDeleteAll}>{message}</DeleteAll>}
|
||||
</FormattedMessage>
|
||||
</td>
|
||||
@ -5,8 +5,8 @@ import { FormattedMessage } from 'react-intl';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Grab, GrabLarge, Pencil } from '@buffetjs/icons';
|
||||
import { CheckPermissions } from '@strapi/helper-plugin';
|
||||
import pluginId from '../../pluginId';
|
||||
import pluginPermissions from '../../permissions';
|
||||
import { getTrad } from '../../utils';
|
||||
import permissions from '../../../permissions';
|
||||
import useLayoutDnd from '../../hooks/useLayoutDnd';
|
||||
import GrabWrapper from './GrabWrapper';
|
||||
import Link from './Link';
|
||||
@ -16,6 +16,8 @@ import SubWrapper from './SubWrapper';
|
||||
import Wrapper from './Wrapper';
|
||||
import Close from './Close';
|
||||
|
||||
const cmPermissions = permissions.contentManager;
|
||||
|
||||
/* eslint-disable */
|
||||
const DraggedField = forwardRef(
|
||||
(
|
||||
@ -132,14 +134,14 @@ const DraggedField = forwardRef(
|
||||
</SubWrapper>
|
||||
)}
|
||||
{type === 'component' && (
|
||||
<CheckPermissions permissions={pluginPermissions.componentsConfigurations}>
|
||||
<FormattedMessage id={`${pluginId}.components.FieldItem.linkToComponentLayout`}>
|
||||
<CheckPermissions permissions={cmPermissions.componentsConfigurations}>
|
||||
<FormattedMessage id={getTrad('components.FieldItem.linkToComponentLayout')}>
|
||||
{msg => (
|
||||
<Link
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
|
||||
goTo(`/plugins/${pluginId}/components/${componentUid}/configurations/edit`);
|
||||
goTo(`/content-manager/components/${componentUid}/configurations/edit`);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon="cog" />
|
||||
@ -2,11 +2,12 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { hasPermissions, useRBACProvider } from '@strapi/helper-plugin';
|
||||
import pluginId from '../../pluginId';
|
||||
import pluginPermissions from '../../permissions';
|
||||
import permissions from '../../../permissions';
|
||||
import DynamicComponentCard from '../DynamicComponentCard';
|
||||
import Tooltip from './Tooltip';
|
||||
|
||||
const cmPermissions = permissions.contentManager;
|
||||
|
||||
const DynamicComponent = ({ componentUid, friendlyName, icon, setIsOverDynamicZone }) => {
|
||||
const [isOver, setIsOver] = useState(false);
|
||||
const [{ isLoading, canAccess }, setState] = useState({ isLoading: true, canAccess: false });
|
||||
@ -18,7 +19,7 @@ const DynamicComponent = ({ componentUid, friendlyName, icon, setIsOverDynamicZo
|
||||
try {
|
||||
const canAccess = await hasPermissions(
|
||||
allPermissions,
|
||||
pluginPermissions.componentsConfigurations
|
||||
cmPermissions.componentsConfigurations
|
||||
);
|
||||
|
||||
setState({ isLoading: false, canAccess });
|
||||
@ -44,7 +45,7 @@ const DynamicComponent = ({ componentUid, friendlyName, icon, setIsOverDynamicZo
|
||||
isOver={isOver}
|
||||
onClick={() => {
|
||||
if (!isLoading && canAccess) {
|
||||
push(`/plugins/${pluginId}/components/${componentUid}/configurations/edit`);
|
||||
push(`/content-manager/components/${componentUid}/configurations/edit`);
|
||||
}
|
||||
}}
|
||||
onMouseEvent={handleMouseEvent}
|
||||