├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── resources └── img │ ├── plugin-logo.png │ ├── step-1-field-and-layout-designer.png │ ├── step-2-set-permissions-in-modal.png │ ├── step-3-dot-indicator.png │ ├── step-4-tabs-are-restricted.png │ ├── step-5-dot-indicator.png │ ├── step-5-no-access-to-field.png │ ├── step-5-set-permissions-menu.png │ ├── step-5-set-permissions.png │ └── step-6-clear-permissions.png └── src ├── FabPermissions.php ├── assetbundles └── fabpermissions │ ├── FabPermissionsAsset.php │ └── dist │ ├── css │ └── FabPermissions.css │ ├── img │ └── FabPermissions-icon.svg │ └── js │ └── FabPermissions.js ├── base ├── Decorator.php └── FieldDecorator.php ├── controllers └── FabPermissionsController.php ├── decorators └── StaticFieldDecorator.php ├── icon.svg ├── migrations ├── Install.php └── m190623_091258_fabpermissions_disabled_and_readonly_columns.php ├── records └── FabPermissionsRecord.php └── services ├── Fab.php └── Fields.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Craft Field and Tab Permissions Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## 2.0.2 - 2021-08-26 8 | ### Fixed 9 | - Upgraded to support Craft 3.7.x (thanks @JoshCoady) 10 | 11 | ## 2.0.1 - 2021-04-05 12 | ### Fixed 13 | - Upgraded to support Craft 3.6.x 14 | ## 2.0.0 - 2020-10-10 15 | ### Changed 16 | - Upgraded to support Craft 3.5.x 17 | - New custom elements are supported (fields only at this stage) 18 | - Replaced dot indicators with locked/unlocked padlock symbols 19 | - Improved error handling 20 | 21 | ## 1.4.0 - 2020-05-02 22 | ### Changed 23 | - Fixed an issue where form unload wasn't working properly due to changes in Craft 3.4. 24 | - Improved the checkbox permissions toggles to disable the "Can Edit" permission when "Can View" is unchecked. 25 | - The plugin now runs on front end POST requests for logged in users. This means permissions will apply to field layouts for logged in users on form submissions etc. 26 | 27 | ## 1.3.0 - 2020-03-08 28 | ### Changed 29 | - Updated the plugin to be compatible with Craft 3.4. 30 | 31 | ## 1.2.3 - 2019-11-07 32 | ### Changed 33 | - Smoothed the plugin un-installation (surely no one will ;-)) when FabService was no longer available, but the overridden Fields service still referenced it. 34 | 35 | ## 1.2.2 - 2019-11-07 36 | ### Fixed 37 | - Fixed an issue where console requests would fail due to a console user being passed to the permissions service. 38 | 39 | ## 1.2.1 - 2019-10-11 [CRITICAL] 40 | ### Fixed 41 | - Fixed an issue where a service wasn't available for front end requests. 42 | 43 | ## 1.2.0 - 2019-10-05 44 | ### Changed 45 | - Craft's Field service must now be overriden in config/app.php to ensure project config works correctly (and doesn't break in the future). 46 | 47 | ## 1.1.5 - 2019-10-05 48 | ### Fixed 49 | - New FieldsInterface method added in the FieldDecorator class. 50 | 51 | ## 1.1.4 - 2019-09-10 52 | ### Fixed 53 | - Plugin is only loaded on CP requests. 54 | - Safety check added for guests. 55 | 56 | ## 1.1.3 - 2019-08-15 57 | ### Fixed 58 | - Tab permissions can now be set correctly. 59 | 60 | ## 1.1.2 - 2019-07-14 61 | ### Fixed 62 | - Updated to support Craft 3.2 (thanks [ajoliveau](https://github.com/ajoliveau)). 63 | 64 | ## 1.1.1 - 2019-06-27 [CRITICAL] 65 | ### Fixed 66 | - Project Config event handlers are now applied to the extended fields service, this resolves an issue where matrix fields weren't being saved 67 | 68 | ## 1.1.0 - 2019-06-25 69 | ### Changed 70 | - Read-only permissions can now be set on fields as well as hide/show 71 | - Updated the modal to use a table format 72 | 73 | ## 1.0.4 - 2019-06-18 74 | ### Fixed 75 | - Fixed a bug where admin permissions weren't respected when no user groups existed 76 | 77 | ### Changed 78 | - Updated Fab service to handle admin permissions 79 | - Updated migration to let userGroupId be NULL 80 | - Updated JS to set admin permissions 81 | 82 | ## 1.0.3 - 2019-06-18 83 | ### Changed 84 | - Fixed LICENSE.md 85 | 86 | ## 1.0.2 - 2019-06-16 87 | ### Changed 88 | - Updated README.md to correct tab example 89 | - Fixed a spelling mistake in README.md 90 | 91 | ## 1.0.1 - 2019-06-16 92 | ### Removed 93 | - Translations that weren't required 94 | 95 | ## 1.0.0 - 2019-06-16 96 | ### Added 97 | - Initial release 98 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © Josh Smith 2 | 3 | Permission is hereby granted to any person obtaining a copy of this software 4 | (the “Software”) to use, copy, modify, merge, publish and/or distribute copies 5 | of the Software, and to permit persons to whom the Software is furnished to do 6 | so, subject to the following conditions: 7 | 8 | 1. **Don’t plagiarize.** The above copyright notice and this license shall be 9 | included in all copies or substantial portions of the Software. 10 | 11 | 2. **Don’t use the same license on more than one project.** Each licensed copy 12 | of the Software shall be actively installed in no more than one production 13 | environment at a time. 14 | 15 | 3. **Don’t mess with the licensing features.** Software features related to 16 | licensing shall not be altered or circumvented in any way, including (but 17 | not limited to) license validation, payment prompts, feature restrictions, 18 | and update eligibility. 19 | 20 | 4. **Pay up.** Payment shall be made immediately upon receipt of any notice, 21 | prompt, reminder, or other message indicating that a payment is owed. 22 | 23 | 5. **Follow the law.** All use of the Software shall not violate any applicable 24 | law or regulation, nor infringe the rights of any other person or entity. 25 | 26 | Failure to comply with the foregoing conditions will automatically and 27 | immediately result in termination of the permission granted hereby. This 28 | license does not include any right to receive updates to the Software or 29 | technical support. Licensees bear all risk related to the quality and 30 | performance of the Software and any modifications made or obtained to it, 31 | including liability for actual and consequential harm, such as loss or 32 | corruption of data, and any necessary service, repair, or correction. 33 | 34 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 37 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 38 | LIABILITY, INCLUDING SPECIAL, INCIDENTAL AND CONSEQUENTIAL DAMAGES, WHETHER IN 39 | AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 40 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Field and Tab Permissions plugin for Craft CMS 3.5 2 | 3 | A plugin that allows you to set field and tab visibility for particular user groups in the CMS. 4 | 5 | ![Screenshot](resources/img/plugin-logo.png) 6 | 7 | ## Requirements 8 | 9 | This plugin requires Craft CMS 3.5 or later. 10 | 11 | ## Installation 12 | 13 | To install the plugin, follow these instructions. 14 | 15 | 1. Open your terminal and go to your Craft project: 16 | 17 | cd /path/to/project 18 | 19 | 2. Then tell Composer to load the plugin: 20 | 21 | composer require thejoshsmith/craft-fab-permissions 22 | 23 | 3. In the Control Panel, go to Settings → Plugins and click the “Install” button for FAB Permissions. 24 | 25 | 4. **Important** - Override the Craft Fields Service with the FAB Permissions Fields Service in `config/app.web.php`: 26 | 27 | return [ 28 | 'components' => [ 29 | 'fields' => [ 30 | 'class' => 'thejoshsmith\fabpermissions\services\Fields' 31 | ] 32 | ] 33 | ] 34 | 35 | ## FAB Permissions Overview 36 | 37 | This plugin allows you to restrict access to certain user groups on a per tab or field basis. 38 | 39 | **NEW** - Read only access can now be set on fields. 40 | 41 | A use case for this would be if you had an SEO tab that only digital marketers and developers should be able to access. You can turn off access to client users whilst keeping access for the marketers and developers—The SEO tab will remain hidden to clients but visible for marketers and developers. 42 | 43 | You can alter permissions for any element that uses the core field layout designer. This includes, but isn't limited to: 44 | 45 | + Entry Types 46 | + Globals 47 | + Users 48 | + Assets 49 | + Categories 50 | + Tags 51 | 52 | ## How does it work? 53 | 54 | The plugin extends the core field and layout designer javascript object, and injects hidden inputs with user group permissons. Once permissions are saved in the database, an extended fields service is able to filter out fields and tabs based on the logged in user and their access. 55 | 56 | Great care has been taken to ensure the bare minimum of core functionality has been extended. You are required to override the base Craft Fields Service with the FAB Permissions Fields Service within your config/app.php file. Until recently this was automatically done, but since plugins are registered after the Project Config listeners, we need to ensure the FAB Permissions Fields Service is loaded at an earlier point, and the only way to do this is to manually override Craft's Fields Service in your app config. 57 | 58 | ## Using FAB Permissions 59 | 60 | 1. After installing the plugin, a new menu item will be available from the settings menu on any field layout: 61 | 62 | ![Field and Layout Designer](resources/img/step-1-field-and-layout-designer.png) 63 | 64 | 2. Clicking the menu item will bring up the permissions modal. Check the user groups you'd like to give access to, and click save. In this screenshot, Clients won't be able to access the SEO tab. 65 | 66 | ![Permissions Modal](resources/img/step-2-set-permissions-in-modal.png) 67 | 68 | 3. A red dot is now shown in the tab, indicating permissions have been set. 69 | 70 | ![Red Dot Indication](resources/img/step-3-dot-indicator.png) 71 | 72 | 4. Clients no longer have access to the SEO tab. 73 | 74 | ![SEO Tab Restricted](resources/img/step-4-tabs-are-restricted.png) 75 | 76 | 5. Fields can be restricted in the same way: 77 | 78 | ![Set Field Permissions Menu](resources/img/step-5-set-permissions-menu.png) 79 | 80 | ![Set Field Permissions Modal](resources/img/step-5-set-permissions.png) 81 | 82 | ![Field Permissions Dot Indicator](resources/img/step-5-dot-indicator.png) 83 | 84 | ![Field Restricted For Users](resources/img/step-5-no-access-to-field.png) 85 | 86 | _In this screenshot, the author user picker and matrix are set to read-only._ 87 | 88 | 6. Permissions can be cleared using the "Clear" button on the permissions modal: 89 | 90 | ![Field Restricted For Users](resources/img/step-6-clear-permissions.png) 91 | 92 | ## FAB Permissions Roadmap 93 | 94 | Some things to do, and ideas for potential features: 95 | 96 | * Page that shows permissions set across all tabs/fields in the CMS 97 | * Ability to set permissions on an individual user basis. 98 | 99 | Brought to you by [Josh Smith](https://joshsmith.dev) 100 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thejoshsmith/craft-fab-permissions", 3 | "description": "Give yourself better control over your sections with Craft Field and Tab (FAB) Permissions. Restrict which tabs and fields are visible to different user groups.", 4 | "type": "craft-plugin", 5 | "version": "2.0.2", 6 | "keywords": [ 7 | "craft", 8 | "cms", 9 | "craftcms", 10 | "craft-plugin", 11 | "field permissions", 12 | "tab permissions" 13 | ], 14 | "support": { 15 | "docs": "https://github.com/thejoshsmith/craft-fab-permissions/blob/master/README.md", 16 | "issues": "https://github.com/thejoshsmith/craft-fab-permissions/issues" 17 | }, 18 | "license": "proprietary", 19 | "authors": [ 20 | { 21 | "name": "Josh Smith", 22 | "homepage": "https://joshsmith.dev" 23 | } 24 | ], 25 | "require": { 26 | "craftcms/cms": "^3.5.0" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "thejoshsmith\\fabpermissions\\": "src/" 31 | } 32 | }, 33 | "extra": { 34 | "name": "Field and Tab Permissions", 35 | "handle": "craft-fab-permissions", 36 | "hasCpSettings": false, 37 | "hasCpSection": false, 38 | "changelogUrl": "https://github.com/thejoshsmith/craft-fab-permissions/blob/master/CHANGELOG.md", 39 | "components": { 40 | "fabPermissionsService": "thejoshsmith\\fabpermissions\\services\\fab" 41 | }, 42 | "class": "thejoshsmith\\fabpermissions\\FabPermissions" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /resources/img/plugin-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moacode/craft-fab-permissions/15fad797bf6a9f1272e1e4388df5ec6d8aa526a8/resources/img/plugin-logo.png -------------------------------------------------------------------------------- /resources/img/step-1-field-and-layout-designer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moacode/craft-fab-permissions/15fad797bf6a9f1272e1e4388df5ec6d8aa526a8/resources/img/step-1-field-and-layout-designer.png -------------------------------------------------------------------------------- /resources/img/step-2-set-permissions-in-modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moacode/craft-fab-permissions/15fad797bf6a9f1272e1e4388df5ec6d8aa526a8/resources/img/step-2-set-permissions-in-modal.png -------------------------------------------------------------------------------- /resources/img/step-3-dot-indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moacode/craft-fab-permissions/15fad797bf6a9f1272e1e4388df5ec6d8aa526a8/resources/img/step-3-dot-indicator.png -------------------------------------------------------------------------------- /resources/img/step-4-tabs-are-restricted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moacode/craft-fab-permissions/15fad797bf6a9f1272e1e4388df5ec6d8aa526a8/resources/img/step-4-tabs-are-restricted.png -------------------------------------------------------------------------------- /resources/img/step-5-dot-indicator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moacode/craft-fab-permissions/15fad797bf6a9f1272e1e4388df5ec6d8aa526a8/resources/img/step-5-dot-indicator.png -------------------------------------------------------------------------------- /resources/img/step-5-no-access-to-field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moacode/craft-fab-permissions/15fad797bf6a9f1272e1e4388df5ec6d8aa526a8/resources/img/step-5-no-access-to-field.png -------------------------------------------------------------------------------- /resources/img/step-5-set-permissions-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moacode/craft-fab-permissions/15fad797bf6a9f1272e1e4388df5ec6d8aa526a8/resources/img/step-5-set-permissions-menu.png -------------------------------------------------------------------------------- /resources/img/step-5-set-permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moacode/craft-fab-permissions/15fad797bf6a9f1272e1e4388df5ec6d8aa526a8/resources/img/step-5-set-permissions.png -------------------------------------------------------------------------------- /resources/img/step-6-clear-permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moacode/craft-fab-permissions/15fad797bf6a9f1272e1e4388df5ec6d8aa526a8/resources/img/step-6-clear-permissions.png -------------------------------------------------------------------------------- /src/FabPermissions.php: -------------------------------------------------------------------------------- 1 | registerComponents(); 67 | 68 | // Ensure we only init the plugin on CP requests. 69 | if( !Craft::$app->getRequest()->getIsCpRequest() ) return false; 70 | 71 | // Show a warning to the user if the component config hasn't been overriden. 72 | $fieldsService = Craft::$app->getFields(); 73 | if( !is_a($fieldsService, 'thejoshsmith\\fabpermissions\\services\\Fields') ){ 74 | Craft::$app->getSession()->setError('Fab Permissions Plugin: Please override the fields service in your app config - Check the README for more information.'); 75 | } 76 | 77 | // Bootstrap this plugin 78 | $this->registerAssetBundles(); 79 | $this->handleEvents(); 80 | 81 | Craft::info( 82 | Craft::t( 83 | self::PLUGIN_HANDLE, 84 | '{name} plugin loaded', 85 | ['name' => $this->name] 86 | ), 87 | __METHOD__ 88 | ); 89 | } 90 | 91 | // Protected Methods 92 | // ========================================================================= 93 | 94 | /** 95 | * Registers asset bundles for the Control Panel 96 | * @author Josh Smith 97 | * @return void 98 | */ 99 | protected function registerAssetBundles() 100 | { 101 | // register anasset bundle on Control Panel requests 102 | FabPermissionsAsset::register(Craft::$app->view); 103 | } 104 | 105 | /** 106 | * Registers Plugin Components 107 | * @author Josh Smith 108 | * @return void 109 | */ 110 | protected function registerComponents() 111 | { 112 | Craft::$app->setComponents(['fabService' => FabService::class]); 113 | } 114 | 115 | /** 116 | * Attach event handlers 117 | * @author Josh Smith 118 | * @return void 119 | */ 120 | protected function handleEvents() 121 | { 122 | // Process the saving of permisisons on tabs and fields 123 | Event::on( 124 | Fields::class, 125 | Fields::EVENT_AFTER_SAVE_FIELD_LAYOUT, 126 | function(FieldLayoutEvent $event) { 127 | $this->fabService->saveFieldLayoutPermissions($event->layout); 128 | } 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/assetbundles/fabpermissions/FabPermissionsAsset.php: -------------------------------------------------------------------------------- 1 | sourcePath = "@vendor/thejoshsmith/craft-fab-permissions/src/assetbundles/fabpermissions/dist"; 34 | 35 | // define the dependencies 36 | $this->depends = [ 37 | CpAsset::class, 38 | ]; 39 | 40 | // define the relative path to CSS/JS files that should be registered with the page 41 | // when this asset bundle is registered 42 | $this->js = [ 43 | 'js/FabPermissions.js', 44 | ]; 45 | 46 | $this->css = [ 47 | 'css/FabPermissions.css', 48 | ]; 49 | 50 | parent::init(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/assetbundles/fabpermissions/dist/css/FabPermissions.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CraftHQ plugin for Craft CMS 3 | * 4 | * CraftHQ CSS 5 | * 6 | * @author Josh Smith 7 | * @copyright Copyright (c) 2019 Josh Smith 8 | * @link https://joshsmith.dev 9 | * @package CraftHQ 10 | * @since 1.0.0 11 | */ 12 | .fab-icon { 13 | margin-right: 7px; 14 | } 15 | .icon.unlocked:before { 16 | content: '\F09C'; 17 | font-size: 16px; 18 | color: #0B69A3; 19 | opacity: 0.5; 20 | margin-top: -0.2px; 21 | width: auto; 22 | height: auto; 23 | } 24 | .icon.locked:before { 25 | content: '\F023'; 26 | font-size: 16px; 27 | color: #0B69A3; 28 | margin-top: -0.2px; 29 | width: auto; 30 | height: auto; 31 | } 32 | .fab-spinner { 33 | background-size: 18px; 34 | height: 18px; 35 | } 36 | .fab-my-0 { 37 | margin-top: 0 !important; 38 | margin-bottom: 0 !important; 39 | } -------------------------------------------------------------------------------- /src/assetbundles/fabpermissions/dist/img/FabPermissions-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 14 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/assetbundles/fabpermissions/dist/js/FabPermissions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FabPermissions plugin for Craft CMS 3 | * 4 | * FabPermissions JS 5 | * 6 | * @author Josh Smith 7 | * @copyright Copyright (c) 2019 Josh Smith 8 | * @link https://joshsmith.dev 9 | * @package FabPermissions 10 | * @since 1.0.0 11 | */ 12 | 13 | /** 14 | * Define a global to hold the FabPermissions instance 15 | */ 16 | var FabPermissions; 17 | var FieldLayoutDesigner; 18 | 19 | /** 20 | * Object that handles the getting and setting of permissions 21 | * @author Josh Smith 22 | */ 23 | Craft.FabPermissions = Garnish.Base.extend({ 24 | 25 | $el: null, 26 | data : { 27 | tabs: [], 28 | fields: [] 29 | }, 30 | isLoading: true, 31 | userGroups: {}, 32 | loadingPromise: new $.Deferred(), 33 | actionFieldAndTabPermissionsUrl: 'craft-fab-permissions/fab-permissions/get-field-and-tab-permissions', 34 | actionUserGroupsUrl: 'craft-fab-permissions/fab-permissions/get-user-groups', 35 | fieldLayoutId: null, 36 | 37 | init(settings){ 38 | 39 | var self = this; 40 | this.settings = $.extend({}, Craft.FabPermissions.defaults, settings); 41 | 42 | // Set object properties 43 | this.$el = $('.fld-tabs'); 44 | this.fieldLayoutId = $('input[name="fieldLayoutId"]').val(); 45 | 46 | // Disable the save button until the requests have finished. 47 | // This prevents changes from being saved before the permissions hidden inputs have been populated. 48 | $('.btn.submit').addClass('disabled'); 49 | 50 | this.isLoading = true; 51 | 52 | // Send information to the server that the loading of field and tab data never completed 53 | Craft.cp.on('beforeSaveShortcut', $.proxy(function() { 54 | if( self.isLoading === true ){ 55 | self.$el.append(''); 56 | } 57 | })); 58 | 59 | // Load the permissions data 60 | this._getFabPermissions().fail(function(){ 61 | console.error('Failed to load Field and Tab permissions.'); 62 | }).always(function(){ 63 | self.isLoading = false; 64 | $('.btn.submit').removeClass('disabled'); 65 | }); 66 | }, 67 | 68 | /** 69 | * Loads fab permission data from the server 70 | * @author Josh Smith 71 | * @return {object} Promise 72 | */ 73 | _getFabPermissions (){ 74 | 75 | var self = this; 76 | 77 | var $userGroupsRequest = this._makeRequest(this.actionUserGroupsUrl), 78 | $fieldAndTabPermissionsRequest = this._makeRequest(this.actionFieldAndTabPermissionsUrl, {fieldLayoutId: this.fieldLayoutId}); 79 | 80 | // Create an array of deferred promises 81 | $.when.apply($, [$fieldAndTabPermissionsRequest, $userGroupsRequest]).done(function(){ 82 | self.data = $fieldAndTabPermissionsRequest.responseJSON.data; 83 | self.userGroups = $userGroupsRequest.responseJSON.data.userGroups; 84 | }).always(function(){ 85 | self.loadingPromise.resolve(); 86 | }); 87 | 88 | return this.loadingPromise; 89 | }, 90 | 91 | /** 92 | * Simple method to make an underlying POST request 93 | * @author Josh Smith 94 | * @param {string} url 95 | * @param {string} data 96 | * @return {object} 97 | */ 98 | _makeRequest(url, data){ 99 | return Craft.postActionRequest(url, data); 100 | }, 101 | 102 | /** 103 | * Method called when a tab is initialised 104 | * @author Josh Smith 105 | * @param {object} $tab jQuery collection 106 | * @return {void} 107 | */ 108 | initTab($tab){ 109 | 110 | // Set the original name on to this tab. 111 | // This is so we can fetch the original selections by name even after a tab is renamed. 112 | var fabPermissionsData = $tab.data('fabPermissions'); 113 | $tab.data('fabPermissions', $.extend({}, fabPermissionsData, {originalName: this._getTabName($tab)})); 114 | 115 | // Show a loading spinner 116 | this.showSpinnerIcon($tab.find('.tab')); 117 | 118 | var self = this; 119 | this.loadingPromise.done(function(){ 120 | self.populateTabInputs($tab); 121 | // Remove the loading class once the Fab Permissions data is loaded 122 | }).always(function(){ 123 | 124 | // Get a handle on the menu data 125 | var $editBtn = $tab.find('.tabs .settings'); 126 | var menuData = $editBtn.data('menubtn'); 127 | var $menu = menuData.menu.$container; 128 | 129 | // Remove the disabled menu item 130 | $menu.find('.js--fab-set-permissions').removeClass('disabled'); 131 | 132 | // Hide the loading spinner 133 | self.hideSpinnerIcon($tab.find('.tab')); 134 | 135 | // Allow users to edit tab permissions by clicking the icon 136 | $tab.find('.js--fab-icon').eq(0).on('click', function(e){ 137 | e.preventDefault(); 138 | FieldLayoutDesigner.setPermissionsTab($tab); 139 | }); 140 | }); 141 | }, 142 | 143 | /** 144 | * Method called when a field is initialised 145 | * @author Josh Smith 146 | * @param {object} $field jQuery collection 147 | * @return {void} 148 | */ 149 | initElement($field){ 150 | var self = this; 151 | var fldElement = $field.data().fldElement; 152 | 153 | // Don't process title fields 154 | if( fldElement.config.type === "craft\\fieldlayoutelements\\EntryTitleField" ){ 155 | return; 156 | } 157 | 158 | // Show a loading spinner 159 | this.showSpinnerIcon($field); 160 | 161 | // Populate the field permissions input data 162 | this.loadingPromise.done(function(){ 163 | self.populateFieldInputs($field); 164 | // Remove the loading class once the Fab Permissions data is loaded 165 | }).always(function(){ 166 | // Hide the loading spinner 167 | self.hideSpinnerIcon($field); 168 | 169 | // Allow users to edit field permissions by clicking the icon 170 | $field.find('.js--fab-icon').on('click', function(e){ 171 | e.preventDefault(); 172 | FieldLayoutDesigner.setPermissionsField($field); 173 | }); 174 | }); 175 | 176 | // Extend the field HUD 177 | fldElement.on('createSettingsHud', function(){ 178 | // Create the "Set Permissions" button and inject into the HUD footer 179 | var $btn = $(` 180 | 183 | `).prependTo(fldElement.hud.$footer.find('.buttons')); 184 | 185 | // Remove the disabled menu item if the loading request has finished 186 | self.loadingPromise.done(function(){ 187 | $btn.removeClass('disabled'); 188 | }); 189 | 190 | // Add an event handler to open the permissions modal when clicked 191 | $btn.on('click', function(e){ 192 | e.preventDefault(); 193 | if( $btn.is('.disabled') ) return; 194 | 195 | // Hide the HUD and open the permission field modal 196 | fldElement.hud.hide(); 197 | FieldLayoutDesigner.setPermissionsField($field); 198 | }); 199 | }); 200 | }, 201 | 202 | /** 203 | * Populates hidden inputs that hold the tab permission data 204 | * Tab names are taken from the latest label text 205 | * @author Josh Smith 206 | * @param {object} $tab jQuery collection 207 | * @return {void} 208 | */ 209 | populateTabInputs($tab){ 210 | 211 | var self = this, 212 | hasSavedPermissions = false, 213 | origTabName = this._getOriginalTabName($tab), 214 | tabPermissions = this.data.tabs[origTabName] || null, 215 | hasPermissions = ($.isPlainObject(tabPermissions) && Object.keys(tabPermissions).length); 216 | 217 | if( hasPermissions ){ 218 | // Show an icon on each tab bar if permissions have been set. 219 | this.setHasPermissionsIcon($tab.find('.tab')); 220 | 221 | // Loop the permissions and add hidden inputs 222 | for(var userGroupHandle in tabPermissions){ 223 | for(var type in tabPermissions[userGroupHandle]){ 224 | var hasPermission = tabPermissions[userGroupHandle][type]; 225 | self.addTabInput($tab, type, userGroupHandle, hasPermission); 226 | } 227 | } 228 | } else { 229 | this.setHasNoPermissionsIcon($tab.find('.tab')); 230 | } 231 | 232 | // Reset the form unload values, as we've injected hidden inputs 233 | self.resetFormUnload(); 234 | }, 235 | 236 | /** 237 | * Populates hidden inputs that hold the field permission data 238 | * Tab names are taken from the latest label text 239 | * @author Josh Smith 240 | * @param {object} $field jQuery collection 241 | * @return {void} 242 | */ 243 | populateFieldInputs($field) { 244 | 245 | var self = this, 246 | hasSavedPermissions = false, 247 | fldElement = $field.data().fldElement, 248 | fieldPermissions = this.data.fields[$field.data('id')] || null, 249 | hasPermissions = ($.isPlainObject(fieldPermissions) && Object.keys(fieldPermissions).length); 250 | 251 | if( hasPermissions ){ 252 | // Show an icon on each field bar if permissions have been set. 253 | fldElement.setHasPermissionsIcon(); 254 | 255 | // Loop the permissions and add hidden inputs 256 | for(var userGroupHandle in fieldPermissions){ 257 | for(var type in fieldPermissions[userGroupHandle]){ 258 | var hasPermission = fieldPermissions[userGroupHandle][type]; 259 | self.addFieldInput($field, type, userGroupHandle, hasPermission); 260 | } 261 | } 262 | } else { 263 | fldElement.setHasNoPermissionsIcon(); 264 | } 265 | 266 | // Reset the form unload values, as we've injected hidden inputs 267 | self.resetFormUnload(); 268 | }, 269 | 270 | /** 271 | * Resets the layout form special forms unload 272 | * @author Josh Smith 273 | * @return void 274 | */ 275 | resetFormUnload(){ 276 | $('#content').find('form').data('initialSerializedValue', false); 277 | Craft.cp.initSpecialForms(); 278 | }, 279 | 280 | /** 281 | * Shows a tab spinner icon 282 | */ 283 | showSpinnerIcon($el){ 284 | var $spinner = $el.find('.js--fab-spinner'); 285 | 286 | if( $spinner.length ){ 287 | $spinner.show(); 288 | } else { 289 | $el.children().first().before('
'); 290 | } 291 | }, 292 | 293 | /** 294 | * Hides a tab spinner icon 295 | */ 296 | hideSpinnerIcon($el){ 297 | var $spinner = $el.find('.js--fab-spinner'); 298 | if( $spinner.length ){ 299 | $spinner.hide(); 300 | } 301 | }, 302 | 303 | /** 304 | * Returns the permissions icon element 305 | * @author Josh Smith 306 | * @return object 307 | */ 308 | getHasPermissionsIconEl() { 309 | return $(``); 310 | }, 311 | 312 | /** 313 | * Returns the no permissions icon element 314 | * @author Josh Smith 315 | * @return object 316 | */ 317 | getHasNoPermissionsIconEl() { 318 | return $(``); 319 | }, 320 | 321 | /** 322 | * Returns the permissions icon class 323 | * @author Josh Smith 324 | * @return string 325 | */ 326 | getHasPermissionsIconClass() { 327 | return 'locked'; 328 | }, 329 | 330 | /** 331 | * Returns the no permissions icon class 332 | * @author Josh Smith 333 | * @return string 334 | */ 335 | getHasNoPermissionsIconClass() { 336 | return 'unlocked'; 337 | }, 338 | 339 | /** 340 | * Shows a tab fab permissions icon 341 | */ 342 | setHasPermissionsIcon($el){ 343 | var $fabIcon = $el.find('.js--fab-icon'), 344 | permissionsIconClass = this.getHasPermissionsIconClass(), 345 | noPermissionsIconClass = this.getHasNoPermissionsIconClass(); 346 | 347 | if( $fabIcon.length ){ 348 | $fabIcon.removeClass(noPermissionsIconClass).addClass(permissionsIconClass); 349 | } else { 350 | $el.prepend(this.getHasPermissionsIconEl()); 351 | } 352 | }, 353 | 354 | /** 355 | * Hides a tab fab permissions icon 356 | */ 357 | setHasNoPermissionsIcon($el){ 358 | var $fabIcon = $el.find('.js--fab-icon'), 359 | permissionsIconClass = this.getHasPermissionsIconClass(), 360 | noPermissionsIconClass = this.getHasNoPermissionsIconClass(); 361 | 362 | if( $fabIcon.length ){ 363 | $fabIcon.removeClass(permissionsIconClass).addClass(noPermissionsIconClass); 364 | } else { 365 | $el.prepend(this.getHasNoPermissionsIconEl()); 366 | } 367 | }, 368 | 369 | /** 370 | * Adds a tab hidden input to the DOM 371 | * @author Josh Smith 372 | * @param {object} $tab jQuery collection 373 | * * @param {string} type Permission type, either canView or canEdit 374 | * @param {string} handle User group handle 375 | * @param {Boolean} value Tab input value 376 | */ 377 | addTabInput($tab, type, handle, value){ 378 | value = typeof value === 'boolean' ? String(+value) : value; // Convert boolean values to string 379 | $tab.append(``); 380 | }, 381 | 382 | /** 383 | * Removes all fab permissions hidden inputs for the given tab 384 | * @author Josh Smith 385 | * @param {object} $tab jQuery collection 386 | * @return {void} 387 | */ 388 | removeTabInputs($tab){ 389 | $tab.find('.js--fab-tab-input').remove(); 390 | }, 391 | 392 | /** 393 | * Adds a field hidden input to the DOM 394 | * @author Josh Smith 395 | * @param {object} $field jQuery collection 396 | * @param {string} type Permission type, either canView or canEdit 397 | * @param {string} handle User group handle 398 | * @param {Boolean} value Field input value 399 | */ 400 | addFieldInput($field, type, handle, value){ 401 | value = typeof value === 'boolean' ? String(+value) : value; // Convert boolean values to string 402 | $field.append(''); 403 | }, 404 | 405 | /** 406 | * Removes all fab permissions hidden inputs for the given field 407 | * @author Josh Smith 408 | * @param {object} $field jQuery collection 409 | * @return {void} 410 | */ 411 | removeFieldInputs($field){ 412 | $field.find('.js--fab-field-input').remove(); 413 | }, 414 | 415 | /** 416 | * Returns the fab permissions hidden field input name 417 | * @author Josh Smith 418 | * @param {object} $tab jQuery collection 419 | * @return {string} 420 | */ 421 | _getTabInputName($tab){ 422 | return `tabPermissions[${this._getTabName($tab)}]`; 423 | // return Craft.FieldLayoutDesigner.prototype.getElementPlacementInputName.call(FieldLayoutDesigner, this._getTabName($tab)); 424 | }, 425 | 426 | /** 427 | * Returns the current tab name 428 | * @author Josh Smith 429 | * @param {object} $tab jQuery collection 430 | * @return {string} Tab name 431 | */ 432 | _getTabName($tab){ 433 | var $labelSpan = $tab.find('.tabs .tab span'); 434 | return $labelSpan.text(); 435 | }, 436 | 437 | /** 438 | * Returns the original tab name, from the tab's data 439 | * @author Josh Smith 440 | * @param {object} $tab jQuery collection 441 | * @return {string} Original tab name 442 | */ 443 | _getOriginalTabName($tab){ 444 | return $tab.data('fabPermissions').originalName || ''; 445 | }, 446 | 447 | /** 448 | * Returns whether a particular permission is set or not 449 | * @author Josh Smith 450 | * @param {string} type Type of permission to check 451 | * @param {string} handle User group handle 452 | * @return {Boolean} 453 | */ 454 | isPermissionSet($tab, type, handle){ 455 | var $permissions = $tab.find('.js--fab-tab-input'); 456 | if( ! $permissions.length ) return true; 457 | 458 | var self = this, 459 | matchedHandle = false; 460 | 461 | // Loop each permission input and return whether a permission is set 462 | $permissions.each(function(i, input){ 463 | 464 | var $input = $(input), 465 | regexp = new RegExp(self.escapeRegExp(self._getTabInputName($tab)+'['+handle+']['+type+']')), 466 | matches = $input.attr('name').match(regexp); 467 | 468 | // Mark this checkbox as checked if it matches the handle and it's marked as selected 469 | if( matches && $input.val() === '1' ) matchedHandle = true; 470 | }); 471 | 472 | return matchedHandle; 473 | }, 474 | 475 | /** 476 | * Returns whether a particular permission is set or not 477 | * @author Josh Smith 478 | * @param {string} type Type of permission to check 479 | * @param {string} handle User group handle 480 | * @return {Boolean} 481 | */ 482 | isFieldPermissionSet($field, type, handle){ 483 | var $permissions = $field.find('.js--fab-field-input'); 484 | if( ! $permissions.length ) return true; 485 | 486 | var self = this, 487 | matchedHandle = false; 488 | 489 | // Loop each permission input and return whether a permission is set 490 | $permissions.each(function(i, input){ 491 | 492 | var $input = $(input); 493 | 494 | if( 495 | $field.data('id') === $input.data('id') && 496 | $input.data('type') === type && 497 | $input.data('handle') === handle && 498 | $input.val() === '1' 499 | ) matchedHandle = true; 500 | }); 501 | 502 | return matchedHandle; 503 | }, 504 | 505 | /** 506 | * Helper function to escape regular expressions 507 | * @author Josh Smith 508 | * @param {string} string 509 | * @return {string} 510 | */ 511 | escapeRegExp (string) { 512 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string 513 | } 514 | }, 515 | { 516 | defaults: { 517 | contentSummary: [], 518 | fieldInputName: 'tabPermissions[__TAB_NAME__]', // Awkwardly named, but kept to utilize the default Craft methods that use this property. 519 | onSubmit: $.noop, 520 | redirect: null 521 | } 522 | }); 523 | 524 | /** 525 | * Initialises FabPermissions 526 | * @see Craft.FieldLayoutDesigner.prototype.init 527 | * @return {void} 528 | */ 529 | var init = Craft.FieldLayoutDesigner.prototype.init; 530 | Craft.FieldLayoutDesigner.prototype.init = function(container, settings) { 531 | 532 | FabPermissions = new Craft.FabPermissions(); 533 | init.apply(this, arguments); 534 | FieldLayoutDesigner = this; 535 | 536 | }; 537 | 538 | /** 539 | * Extend the field layout designer with options to set user permissions on fields and tabs 540 | * @type {function} 541 | */ 542 | var initTab = Craft.FieldLayoutDesigner.prototype.initTab; 543 | Craft.FieldLayoutDesigner.prototype.initTab = function($tab) { 544 | initTab.call(this, $tab); 545 | 546 | if (!this.settings.customizableTabs) return; 547 | 548 | // Extract the edit button settings from the tab 549 | var $editBtn = $tab.find('.tabs .settings'); 550 | var menuData = $editBtn.data('menubtn'); 551 | var $menu = menuData.menu.$container; 552 | 553 | // Create a new menu option element 554 | var $menuOption = $('
  • ' + Craft.t('app', 'Set Permissions') + '
  • '); 555 | 556 | // Add it to the menu collection, and register the option within the Garnish Menu object 557 | $menu.find('ul').eq(1).prepend($menuOption); 558 | menuData.menu.addOptions($menuOption.find('a')); 559 | 560 | FabPermissions.initTab($tab); 561 | }; 562 | 563 | /** 564 | * Method that handles tab option selections 565 | * @see Craft.FieldLayoutDesigner.prototype.onTabOptionSelect 566 | * @param {object} option Option DOM fragment 567 | * @return {void} 568 | */ 569 | var onTabOptionSelect = Craft.FieldLayoutDesigner.prototype.onTabOptionSelect; 570 | Craft.FieldLayoutDesigner.prototype.onTabOptionSelect = function(option) { 571 | 572 | // Call the original method 573 | onTabOptionSelect.call(this, option); 574 | 575 | if (!this.settings.customizableTabs) return; 576 | 577 | var $option = $(option), 578 | $tab = $option.data('menu').$anchor.parent().parent().parent(), 579 | action = $option.data('action'); 580 | 581 | if( $option.hasClass('disabled') ) return; 582 | 583 | switch (action) { 584 | case 'setpermissions': { 585 | this.setPermissionsTab($tab); 586 | break; 587 | } 588 | } 589 | }; 590 | 591 | /** 592 | * Update the tab inputs when a tab is renamed 593 | * @see Craft.FieldLayoutDesigner.prototype.renameTab 594 | * @param {object} $tab jQuery collection 595 | * @return {void} 596 | */ 597 | var renameTab = Craft.FieldLayoutDesigner.prototype.renameTab; 598 | Craft.FieldLayoutDesigner.prototype.renameTab = function($tab) { 599 | renameTab.call(this, $tab); 600 | 601 | // Remove tab inputs, and re-populate. This automatically re-generates inputs with the new tab name. 602 | FabPermissions.removeTabInputs($tab); 603 | FabPermissions.populateTabInputs($tab); 604 | }; 605 | 606 | /** 607 | * Opens a modal to set user permissions on the selected tab. 608 | * @author Josh Smith 609 | * @param {object} $tab jQuery collection 610 | */ 611 | Craft.FieldLayoutDesigner.prototype.setPermissionsTab = function($tab){ 612 | new Craft.TabUserPermissionSelectorModal({$el: $tab, type: 'tab'}); 613 | }; 614 | 615 | /** 616 | * Extend the field layout designer with options to set user permissions on fields and tabs 617 | * @type {function} 618 | */ 619 | var initElement = Craft.FieldLayoutDesigner.prototype.initElement; 620 | Craft.FieldLayoutDesigner.prototype.initElement = function($container) { 621 | initElement.call(this, $container); 622 | 623 | // Store the field element data 624 | var fldElement = $container.data().fldElement; 625 | if( !fldElement.isField ) return; 626 | 627 | FabPermissions.initElement($container); 628 | }; 629 | 630 | /** 631 | * Sets the permissions icon on an element field 632 | * @author Josh Smith 633 | */ 634 | Craft.FieldLayoutDesigner.Element.prototype.setHasPermissionsIcon = function() { 635 | var $fabIcon = this.$container.find('.js--fab-icon'), 636 | permissionsIconClass = FabPermissions.getHasPermissionsIconClass(), 637 | noPermissionsIconClass = FabPermissions.getHasNoPermissionsIconClass(); 638 | 639 | if( $fabIcon.length ) { 640 | $fabIcon.removeClass(noPermissionsIconClass).addClass(permissionsIconClass); 641 | } else { 642 | FabPermissions.getHasPermissionsIconEl().prependTo(this.$container); 643 | } 644 | }; 645 | 646 | /** 647 | * Sets the no permissions icon on an element field 648 | * @author Josh Smith 649 | */ 650 | Craft.FieldLayoutDesigner.Element.prototype.setHasNoPermissionsIcon = function() { 651 | var $fabIcon = this.$container.find('.js--fab-icon'), 652 | permissionsIconClass = FabPermissions.getHasPermissionsIconClass(), 653 | noPermissionsIconClass = FabPermissions.getHasNoPermissionsIconClass(); 654 | 655 | if( $fabIcon.length ) { 656 | $fabIcon.removeClass(permissionsIconClass).addClass(noPermissionsIconClass); 657 | } else { 658 | FabPermissions.getHasNoPermissionsIconEl().prependTo(this.$container); 659 | } 660 | }; 661 | 662 | /** 663 | * Opens a modal to set user permissions on the selected field. 664 | * @author Josh Smith 665 | * @param {object} $field jQuery collection 666 | */ 667 | Craft.FieldLayoutDesigner.prototype.setPermissionsField = function($field){ 668 | new Craft.FieldUserPermissionSelectorModal({$el: $field, type: 'field'}); 669 | }; 670 | 671 | /** 672 | * Base Modal object that allows a user to set permissions on a tab or field 673 | * @author Josh Smith 674 | */ 675 | Craft.BaseUserPermissionSelectorModal = Garnish.Modal.extend({ 676 | 677 | settings: {}, 678 | closeOtherModals: true, 679 | shadeClass: 'modal-shade dark', 680 | userGroups: [], 681 | $form : $(), 682 | $submitBtn: $(), 683 | actionUrl : 'craft-fab-permissions/fab-permissions/get-user-groups', 684 | 685 | init(settings) { 686 | 687 | var self = this; 688 | this.settings = $.extend({}, Craft.BaseUserPermissionSelectorModal.defaults, settings); 689 | 690 | this.$form = $( 691 | '' 694 | ).appendTo(Garnish.$bod); 695 | 696 | var $body = $( 697 | '
    ' + 698 | '
    ' + 699 | '

    '+this.titleFormat(this.settings.type)+' Permissions

    ' + 700 | '

    Choose which user groups have access to this '+this.settings.type+'.

    ' + 701 | '
    ' + 702 | '
    ' + 703 | '' + 704 | '' + 705 | '' + 706 | '' + 707 | '' + 708 | '' + 709 | '' + 710 | '
    ' + 711 | '
    ' + 712 | '
    ' 713 | ).appendTo(this.$form), 714 | 715 | $tableHeadings = this.getTableHeadings().appendTo($body.find('thead tr')), 716 | 717 | $footer = $('