diff --git a/docs/v3.x/plugin-development/frontend-settings-api.md b/docs/v3.x/plugin-development/frontend-settings-api.md index 21daa0e996..5971734782 100644 --- a/docs/v3.x/plugin-development/frontend-settings-api.md +++ b/docs/v3.x/plugin-development/frontend-settings-api.md @@ -42,6 +42,7 @@ export default strapi => { title: 'Setting page 1', to: `${strapi.settingsBaseURL}/${pluginId}/setting1`, name: 'setting1', + permissions: [{ action: 'plugins::my-plugin.action-name', subject: null }], // This key is required }, { // Using i18n with a corresponding translation key @@ -51,6 +52,7 @@ export default strapi => { }, to: `${strapi.settingsBaseURL}/${pluginId}/setting2`, name: 'setting2', + permissions: [{ action: 'plugins::my-plugin.action-name', subject: null }], // This key is required }, ], }; @@ -147,6 +149,7 @@ export default strapi => { title: 'Setting page 1', to: `${strapi.settingsBaseURL}/${pluginId}/setting1`, name: 'setting1', + permissions: [{ action: 'plugins::my-plugin.action-name', subject: null }], // This key is required }, { title: { @@ -155,6 +158,7 @@ export default strapi => { }, to: `${strapi.settingsBaseURL}/${pluginId}/setting2`, name: 'setting2', + permissions: [{ action: 'plugins::my-plugin.action-name', subject: null }], // This key is required }, ], }; @@ -240,16 +244,19 @@ export default strapi => { preventComponentRendering: false, settings: { // Add a link into the global section of the settings view - global: [ - { - title: 'Setting link 1', - to: `${strapi.settingsBaseURL}/setting-link-1`, - name: 'settingLink1', - Component: SettingLink, - // Bool : https://reacttraining.com/react-router/web/api/Route/exact-bool - exact: false, - }, - ], + global: { + links: [ + { + title: 'Setting link 1', + to: `${strapi.settingsBaseURL}/setting-link-1`, + name: 'settingLink1', + Component: SettingLink, + // Bool : https://reacttraining.com/react-router/web/api/Route/exact-bool + exact: false, + permissions: [{ action: 'plugins::my-plugin.action-name', subject: null }], // This key is required + }, + ], + }, mainComponent: Settings, menuSection, }, diff --git a/packages/strapi-admin/admin/src/containers/LeftMenu/init.js b/packages/strapi-admin/admin/src/containers/LeftMenu/init.js index 5c197bc5ea..cc001a6a15 100644 --- a/packages/strapi-admin/admin/src/containers/LeftMenu/init.js +++ b/packages/strapi-admin/admin/src/containers/LeftMenu/init.js @@ -1,22 +1,6 @@ -import { get, omit, set, sortBy } from 'lodash'; +import { get, omit, set } from 'lodash'; import { SETTINGS_BASE_URL } from '../../config'; - -const getPluginsSettingsPermissions = plugins => - Object.values(plugins).reduce((acc, current) => { - const pluginSettings = get(current, 'settings.global', []); - - pluginSettings.forEach(setting => { - const permissions = get(setting, 'permissions', []); - - permissions.forEach(permission => { - acc.push(permission); - }); - }); - - return acc; - }, []); - -const sortLinks = links => sortBy(links, object => object.name); +import { getPluginsSettingsPermissions, sortLinks } from './utils'; const init = (initialState, plugins = {}) => { // For each plugin retrieve the permissions associated to each injected link @@ -50,4 +34,3 @@ const init = (initialState, plugins = {}) => { }; export default init; -export { sortLinks }; diff --git a/packages/strapi-admin/admin/src/containers/LeftMenu/tests/init.test.js b/packages/strapi-admin/admin/src/containers/LeftMenu/tests/init.test.js index e7758e6f95..7b5b4e4351 100644 --- a/packages/strapi-admin/admin/src/containers/LeftMenu/tests/init.test.js +++ b/packages/strapi-admin/admin/src/containers/LeftMenu/tests/init.test.js @@ -1,140 +1,264 @@ -import init, { sortLinks } from '../init'; +import { SETTINGS_BASE_URL } from '../../../config'; +import init from '../init'; describe('ADMIN | LeftMenu | init', () => { - describe('init', () => { - it('should return the initialState if the plugins are empty', () => { - const initialState = { - ok: true, - generalSectionLinks: [], - }; + it('should return the initialState if the plugins are empty', () => { + const initialState = { + ok: true, + generalSectionLinks: [], + }; - expect(init(initialState)).toEqual({ ok: true, generalSectionLinks: [] }); - }); - - it('should create the pluginsSectionLinks correctly', () => { - const plugins = { - documentation: { - menu: { - pluginsSectionLinks: [ - { - destination: '/plugins/documentation', - icon: 'doc', - label: { - id: 'documentation.plugin.name', - defaultMessage: 'Documentation', - }, - name: 'documentation', - permissions: [{ action: 'plugins::documentation.read', subject: null }], - }, - { - destination: '/plugins/documentation/test', - icon: 'doc', - label: { - id: 'documentation.plugin.name.test', - defaultMessage: 'Documentation Test', - }, - name: 'documentation test', - permissions: [], - }, - ], - }, - }, - test: {}, - 'content-type-builder': { - menu: { - pluginsSectionLinks: [ - { - destination: '/plugins/content-type-builder', - icon: 'plug', - label: { - id: 'content-type-builder.plugin.name', - defaultMessage: 'content-type-builder', - }, - name: 'content-type-builder', - permissions: [{ action: 'plugins::content-type-builder.read', subject: null }], - }, - ], - }, - }, - }; - const initialState = { - generalSectionLinks: [], - pluginsSectionLinks: [], - isLoading: true, - }; - const expected = { - generalSectionLinks: [], - pluginsSectionLinks: [ - { - destination: '/plugins/content-type-builder', - icon: 'plug', - label: { - id: 'content-type-builder.plugin.name', - defaultMessage: 'content-type-builder', - }, - isDisplayed: false, - permissions: [{ action: 'plugins::content-type-builder.read', subject: null }], - }, - { - destination: '/plugins/documentation', - icon: 'doc', - label: { - id: 'documentation.plugin.name', - defaultMessage: 'Documentation', - }, - isDisplayed: false, - permissions: [{ action: 'plugins::documentation.read', subject: null }], - }, - { - destination: '/plugins/documentation/test', - icon: 'doc', - label: { - id: 'documentation.plugin.name.test', - defaultMessage: 'Documentation Test', - }, - isDisplayed: false, - permissions: [], - }, - ], - isLoading: true, - }; - - expect(init(initialState, plugins)).toEqual(expected); - }); + expect(init(initialState)).toEqual({ ok: true, generalSectionLinks: [] }); }); - describe('sortLinks', () => { - it('should return an empty array', () => { - expect(sortLinks([])).toEqual([]); - }); + it('should create the pluginsSectionLinks correctly', () => { + const plugins = { + documentation: { + menu: { + pluginsSectionLinks: [ + { + destination: '/plugins/documentation', + icon: 'doc', + label: { + id: 'documentation.plugin.name', + defaultMessage: 'Documentation', + }, + name: 'documentation', + permissions: [{ action: 'plugins::documentation.read', subject: null }], + }, + { + destination: '/plugins/documentation/test', + icon: 'doc', + label: { + id: 'documentation.plugin.name.test', + defaultMessage: 'Documentation Test', + }, + name: 'documentation test', + permissions: [], + }, + ], + }, + }, + test: {}, + 'content-type-builder': { + menu: { + pluginsSectionLinks: [ + { + destination: '/plugins/content-type-builder', + icon: 'plug', + label: { + id: 'content-type-builder.plugin.name', + defaultMessage: 'content-type-builder', + }, + name: 'content-type-builder', + permissions: [{ action: 'plugins::content-type-builder.read', subject: null }], + }, + ], + }, + }, + }; + const initialState = { + generalSectionLinks: [], + pluginsSectionLinks: [], + isLoading: true, + }; + const expected = { + generalSectionLinks: [], + pluginsSectionLinks: [ + { + destination: '/plugins/content-type-builder', + icon: 'plug', + label: { + id: 'content-type-builder.plugin.name', + defaultMessage: 'content-type-builder', + }, + isDisplayed: false, + permissions: [{ action: 'plugins::content-type-builder.read', subject: null }], + }, + { + destination: '/plugins/documentation', + icon: 'doc', + label: { + id: 'documentation.plugin.name', + defaultMessage: 'Documentation', + }, + isDisplayed: false, + permissions: [{ action: 'plugins::documentation.read', subject: null }], + }, + { + destination: '/plugins/documentation/test', + icon: 'doc', + label: { + id: 'documentation.plugin.name.test', + defaultMessage: 'Documentation Test', + }, + isDisplayed: false, + permissions: [], + }, + ], + isLoading: true, + }; - it('should return a sorted array', () => { - const data = [ - { - name: 'un', - }, - { name: 'deux' }, - { name: 'un-un' }, - { name: 'un-deux' }, - { name: 'un un' }, - ]; - const expected = [ - { - name: 'deux', - }, - { - name: 'un', - }, - { name: 'un un' }, - { - name: 'un-deux', - }, - { - name: 'un-un', - }, - ]; + expect(init(initialState, plugins)).toEqual(expected); + }); - expect(sortLinks(data)).toEqual(expected); - }); + it('should set the permissions in the settings link correctly for the plugins', () => { + const initialState = { + generalSectionLinks: [ + { + icon: 'list', + label: 'app.components.LeftMenuLinkContainer.listPlugins', + destination: '/list-plugins', + isDisplayed: false, + permissions: [ + { action: 'admin::marketplace.read', subject: null }, + { action: 'admin::marketplace.plugins.uninstall', subject: null }, + ], + }, + { + icon: 'shopping-basket', + label: 'app.components.LeftMenuLinkContainer.installNewPlugin', + destination: '/marketplace', + isDisplayed: false, + permissions: [ + { action: 'admin::marketplace.read', subject: null }, + { action: 'admin::marketplace.plugins.install', subject: null }, + ], + }, + { + icon: 'cog', + label: 'app.components.LeftMenuLinkContainer.settings', + isDisplayed: false, + destination: SETTINGS_BASE_URL, + permissions: [ + // webhooks + { action: 'admin::webhook.create', subject: null }, + { action: 'admin::webhook.read', subject: null }, + { action: 'admin::webhook.update', subject: null }, + { action: 'admin::webhook.delete', subject: null }, + // users + { action: 'admin::users.create', subject: null }, + { action: 'admin::users.read', subject: null }, + { action: 'admin::users.update', subject: null }, + { action: 'admin::users.delete', subject: null }, + // roles + { action: 'admin::roles.create', subject: null }, + { action: 'admin::roles.update', subject: null }, + { action: 'admin::roles.read', subject: null }, + { action: 'admin::roles.delete', subject: null }, + // Here are added the plugins settings permissions during the init phase + ], + }, + ], + pluginsSectionLinks: [], + isLoading: true, + }; + const plugins = { + test: { + settings: { + global: { + links: [ + { + title: { + id: 'test.plugin.name', + defaultMessage: 'Test', + }, + name: 'test', + to: '/settings/test', + Component: () => null, + permissions: [{ action: 'plugins::test.settings.read', subject: null }], + }, + { + title: { + id: 'test.plugin.name1', + defaultMessage: 'Test1', + }, + name: 'test1', + to: '/settings/test1', + Component: () => null, + permissions: [{ action: 'plugins::test1.settings.read', subject: null }], + }, + ], + }, + }, + }, + other: {}, + upload: { + settings: { + global: { + links: [ + { + title: { + id: 'upload.plugin.name', + defaultMessage: 'Media Library', + }, + name: 'media-library', + to: 'settings/media-library', + Component: () => null, + permissions: [ + { action: 'plugins::upload.settings.read', subject: null }, + { action: 'plugins::upload.settings.read.test', subject: null }, + ], + }, + ], + }, + }, + }, + }; + const expected = { + generalSectionLinks: [ + { + icon: 'list', + label: 'app.components.LeftMenuLinkContainer.listPlugins', + destination: '/list-plugins', + isDisplayed: false, + permissions: [ + { action: 'admin::marketplace.read', subject: null }, + { action: 'admin::marketplace.plugins.uninstall', subject: null }, + ], + }, + { + icon: 'shopping-basket', + label: 'app.components.LeftMenuLinkContainer.installNewPlugin', + destination: '/marketplace', + isDisplayed: false, + permissions: [ + { action: 'admin::marketplace.read', subject: null }, + { action: 'admin::marketplace.plugins.install', subject: null }, + ], + }, + { + icon: 'cog', + label: 'app.components.LeftMenuLinkContainer.settings', + isDisplayed: false, + destination: SETTINGS_BASE_URL, + permissions: [ + // webhooks + { action: 'admin::webhook.create', subject: null }, + { action: 'admin::webhook.read', subject: null }, + { action: 'admin::webhook.update', subject: null }, + { action: 'admin::webhook.delete', subject: null }, + // users + { action: 'admin::users.create', subject: null }, + { action: 'admin::users.read', subject: null }, + { action: 'admin::users.update', subject: null }, + { action: 'admin::users.delete', subject: null }, + // roles + { action: 'admin::roles.create', subject: null }, + { action: 'admin::roles.update', subject: null }, + { action: 'admin::roles.read', subject: null }, + { action: 'admin::roles.delete', subject: null }, + { action: 'plugins::test.settings.read', subject: null }, + { action: 'plugins::test1.settings.read', subject: null }, + { action: 'plugins::upload.settings.read', subject: null }, + { action: 'plugins::upload.settings.read.test', subject: null }, + ], + }, + ], + pluginsSectionLinks: [], + isLoading: true, + }; + + expect(init(initialState, plugins)).toEqual(expected); }); }); diff --git a/packages/strapi-admin/admin/src/containers/LeftMenu/utils/getPluginsSettingsPermissions.js b/packages/strapi-admin/admin/src/containers/LeftMenu/utils/getPluginsSettingsPermissions.js new file mode 100644 index 0000000000..36a1a54bf3 --- /dev/null +++ b/packages/strapi-admin/admin/src/containers/LeftMenu/utils/getPluginsSettingsPermissions.js @@ -0,0 +1,89 @@ +// Retrieve the plugin settings object +// The settings API works as follows for a plugin +// Declare the links that will be injected into the settings menu +// +// Path: my-plugin/admin/src/index.js +/* + ************************************************************ + * 1. Declare a section that will be added to the setting menu + * const menuSection = { + * // Unique id of the section + * id: pluginId, + * // Title of Menu section using i18n + * title: { + * id: `${pluginId}.foo`, + * defaultMessage: 'Super cool setting', + * }, + * // Array of links to be displayed + * links: [ + * { + * // Using string + * title: 'Setting page 1', + * to: `${strapi.settingsBaseURL}/${pluginId}/setting1`, + * name: 'setting1', + * permissions: [{ action: 'plugins::my-plugin.action-name', subject: null }], + * }, + * { + * // Using i18n with a corresponding translation key + * title: { + * id: `${pluginId}.bar`, + * defaultMessage: 'Setting page 2', + * }, + * to: `${strapi.settingsBaseURL}/${pluginId}/setting2`, + * name: 'setting2', + * permissions: [{ action: 'plugins::my-plugin.action-name2', subject: null }], + * }, + * ], + * }; + * ************************************************************ + * 2. Add a setting to the global section of the menu + * const global = { + * links: [ + * { + * title: { + * id: getTrad('plugin.name'), + * defaultMessage: 'Media Library', + * }, + * name: 'media-library', + * to: `${strapi.settingsBaseURL}/media-library`, + * Component: SettingsPage, + * // TODO write documentation + * permissions: [{ action: 'plugins::my-plugin.action-name', subject: null }], + * }, + * ], + * }; + *********************************************************** + * 3. Define the settings in the plugin object + * const settings = { global, menuSection }; + */ + +import { get } from 'lodash'; + +const getPluginsSettingsPermissions = plugins => { + const globalSettingsLinksPermissions = Object.values(plugins).reduce((acc, current) => { + const pluginSettings = get(current, 'settings', {}); + const getSettingsLinkPermissions = settings => { + return Object.values(settings).reduce((acc, current) => { + const links = get(current, 'links', []); + + links.forEach(link => { + const permissions = get(link, 'permissions', []); + + permissions.forEach(permission => { + acc.push(permission); + }); + }); + + return acc; + }, []); + }; + + const pluginPermissions = getSettingsLinkPermissions(pluginSettings); + + return [...acc, ...pluginPermissions]; + }, []); + + return [...globalSettingsLinksPermissions]; +}; + +export default getPluginsSettingsPermissions; diff --git a/packages/strapi-admin/admin/src/containers/LeftMenu/utils/index.js b/packages/strapi-admin/admin/src/containers/LeftMenu/utils/index.js new file mode 100644 index 0000000000..13f0ff0bd4 --- /dev/null +++ b/packages/strapi-admin/admin/src/containers/LeftMenu/utils/index.js @@ -0,0 +1,2 @@ +export { default as getPluginsSettingsPermissions } from './getPluginsSettingsPermissions'; +export { default as sortLinks } from './sortLinks'; diff --git a/packages/strapi-admin/admin/src/containers/LeftMenu/utils/sortLinks.js b/packages/strapi-admin/admin/src/containers/LeftMenu/utils/sortLinks.js new file mode 100644 index 0000000000..4c4f49f1c8 --- /dev/null +++ b/packages/strapi-admin/admin/src/containers/LeftMenu/utils/sortLinks.js @@ -0,0 +1,5 @@ +import { sortBy } from 'lodash'; + +const sortLinks = links => sortBy(links, object => object.name); + +export default sortLinks; diff --git a/packages/strapi-admin/admin/src/containers/LeftMenu/utils/tests/getPluginsSettingsPermissions.js b/packages/strapi-admin/admin/src/containers/LeftMenu/utils/tests/getPluginsSettingsPermissions.js new file mode 100644 index 0000000000..f5ae72fde3 --- /dev/null +++ b/packages/strapi-admin/admin/src/containers/LeftMenu/utils/tests/getPluginsSettingsPermissions.js @@ -0,0 +1,100 @@ +import getPluginsSettingsPermissions from '../getPluginsSettingsPermissions'; + +describe('ADMIN | LeftMenu | utils | getPluginsSettingsPermissions', () => { + it('should return an empty array', () => { + expect(getPluginsSettingsPermissions({})).toEqual([]); + }); + + it('should return an array containing all the permissions of the plugins settings links', () => { + const menuSection = { + // Unique id of the section + id: 'test', + // Title of Menu section using i18n + title: { + id: 'test.foo', + defaultMessage: 'Super cool setting', + }, + // Array of links to be displayed + links: [ + { + // Using string + title: 'Setting page 1', + to: 'settings/test/setting1', + name: 'setting1', + permissions: [{ action: 'plugins::test.action-name', subject: null }], + }, + { + // Using i18n with a corresponding translation key + title: { + id: 'test.bar', + defaultMessage: 'Setting page 2', + }, + to: 'settings/test/setting2', + name: 'setting2', + permissions: [{ action: 'plugins::my-plugin.action-name2', subject: null }], + }, + ], + }; + const plugins = { + test: { + settings: { + global: { + links: [ + { + title: { + id: 'test.plugin.name', + defaultMessage: 'Test', + }, + name: 'test', + to: '/settings/test', + Component: () => null, + permissions: [{ action: 'plugins::test.settings.read', subject: null }], + }, + { + title: { + id: 'test.plugin.name1', + defaultMessage: 'Test1', + }, + name: 'test1', + to: '/settings/test1', + Component: () => null, + permissions: [{ action: 'plugins::test1.settings.read', subject: null }], + }, + ], + }, + menuSection, + }, + }, + other: {}, + upload: { + settings: { + global: { + links: [ + { + title: { + id: 'upload.plugin.name', + defaultMessage: 'Media Library', + }, + name: 'media-library', + to: 'settings/media-library', + Component: () => null, + permissions: [ + { action: 'plugins::upload.settings.read', subject: null }, + { action: 'plugins::upload.settings.read.test', subject: null }, + ], + }, + ], + }, + }, + }, + }; + const expected = [ + { action: 'plugins::test.action-name', subject: null }, + { action: 'plugins::my-plugin.action-name2', subject: null }, + { action: 'plugins::upload.settings.read', subject: null }, + { action: 'plugins::upload.settings.read.test', subject: null }, + ]; + + expect(getPluginsSettingsPermissions(plugins)).toEqual(expected); + }); +}); diff --git a/packages/strapi-admin/admin/src/containers/LeftMenu/utils/tests/sortLinks.test.js b/packages/strapi-admin/admin/src/containers/LeftMenu/utils/tests/sortLinks.test.js new file mode 100644 index 0000000000..63e8764853 --- /dev/null +++ b/packages/strapi-admin/admin/src/containers/LeftMenu/utils/tests/sortLinks.test.js @@ -0,0 +1,36 @@ +import sortLinks from '../sortLinks'; + +describe('ADMIN | LeftMenu | utils | sortLinks', () => { + it('should return an empty array', () => { + expect(sortLinks([])).toEqual([]); + }); + + it('should return a sorted array', () => { + const data = [ + { + name: 'un', + }, + { name: 'deux' }, + { name: 'un-un' }, + { name: 'un-deux' }, + { name: 'un un' }, + ]; + const expected = [ + { + name: 'deux', + }, + { + name: 'un', + }, + { name: 'un un' }, + { + name: 'un-deux', + }, + { + name: 'un-un', + }, + ]; + + expect(sortLinks(data)).toEqual(expected); + }); +}); diff --git a/packages/strapi-admin/admin/src/containers/SettingsPage/utils/retrieveGlobalLinks.js b/packages/strapi-admin/admin/src/containers/SettingsPage/utils/retrieveGlobalLinks.js index d921ecab40..838358fc2c 100644 --- a/packages/strapi-admin/admin/src/containers/SettingsPage/utils/retrieveGlobalLinks.js +++ b/packages/strapi-admin/admin/src/containers/SettingsPage/utils/retrieveGlobalLinks.js @@ -2,7 +2,7 @@ import { get } from 'lodash'; const retrieveGlobalLinks = pluginsObj => { return Object.values(pluginsObj).reduce((acc, current) => { - const links = get(current, ['settings', 'global'], null); + const links = get(current, ['settings', 'global', 'links'], null); if (links) { for (let i = 0; i < links.length; i++) { diff --git a/packages/strapi-admin/admin/src/containers/SettingsPage/utils/tests/retrieveGlobalLinks.test.js b/packages/strapi-admin/admin/src/containers/SettingsPage/utils/tests/retrieveGlobalLinks.test.js index 85509eb280..b5843e089c 100644 --- a/packages/strapi-admin/admin/src/containers/SettingsPage/utils/tests/retrieveGlobalLinks.test.js +++ b/packages/strapi-admin/admin/src/containers/SettingsPage/utils/tests/retrieveGlobalLinks.test.js @@ -10,18 +10,22 @@ describe('ADMIN | containers | SettingsPage | utils', () => { const plugins = { test: { settings: { - global: [], + global: { + links: [], + }, }, }, noSettings: {}, foo: { settings: { - global: ['test'], + global: { + links: ['test'], + }, }, }, bar: { settings: { - global: ['test2'], + global: { links: ['test2'] }, }, }, }; diff --git a/packages/strapi-plugin-upload/admin/src/index.js b/packages/strapi-plugin-upload/admin/src/index.js index a74490d6d1..0f0611b076 100644 --- a/packages/strapi-plugin-upload/admin/src/index.js +++ b/packages/strapi-plugin-upload/admin/src/index.js @@ -21,6 +21,7 @@ export default strapi => { const pluginDescription = pluginPkg.strapi.description || pluginPkg.description; const icon = pluginPkg.strapi.icon; const name = pluginPkg.strapi.name; + const plugin = { blockerComponent: null, blockerComponentProps: {}, @@ -41,19 +42,20 @@ export default strapi => { pluginLogo, preventComponentRendering: false, settings: { - global: [ - { - title: { - id: getTrad('plugin.name'), - defaultMessage: 'Media Library', + global: { + links: [ + { + title: { + id: getTrad('plugin.name'), + defaultMessage: 'Media Library', + }, + name: 'media-library', + to: `${strapi.settingsBaseURL}/media-library`, + Component: SettingsPage, + permissions: [{ action: 'plugins::upload.settings.read', subject: null }], }, - name: 'media-library', - to: `${strapi.settingsBaseURL}/media-library`, - Component: SettingsPage, - // TODO write documentation - permissions: [{ action: 'plugins::upload.settings.read', subject: null }], - }, - ], + ], + }, }, trads, menu: {