From bd35c02e8153098239461ede82e6f910355afe02 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Wed, 13 Jun 2018 15:31:55 -0400 Subject: [PATCH] Improve support for Field tags by adding a new "Manage Tags" button at the bottom of the fields list screen, enabling you to add or remove fields to/from tags. In addition tags can now be used in $pages->find() searches, i.e. $pages->find("my_tag%=something"); would search all fields in the "my_tag" collection. --- wire/core/AdminThemeFramework.php | 1 + wire/core/Field.php | 113 ++++- wire/core/Fields.php | 99 ++++- wire/core/PageFinder.php | 39 +- .../Process/ProcessField/ProcessField.js | 4 +- .../Process/ProcessField/ProcessField.min.js | 2 +- .../Process/ProcessField/ProcessField.module | 397 ++++++++++++++---- .../ProcessTemplate/ProcessTemplate.module | 5 +- 8 files changed, 558 insertions(+), 102 deletions(-) diff --git a/wire/core/AdminThemeFramework.php b/wire/core/AdminThemeFramework.php index 920a094b..1cff6a4d 100644 --- a/wire/core/AdminThemeFramework.php +++ b/wire/core/AdminThemeFramework.php @@ -657,6 +657,7 @@ abstract class AdminThemeFramework extends AdminTheme { // unencode + re-encode entities, just in case module already entity some or all of output if(strpos($text, '&') !== false) $text = $this->sanitizer->unentities($text); $text = $this->sanitizer->entities($text); + $text = nl2br($text); } if($notice instanceof NoticeError) { diff --git a/wire/core/Field.php b/wire/core/Field.php index 5a7113e4..8b33549a 100644 --- a/wire/core/Field.php +++ b/wire/core/Field.php @@ -12,7 +12,7 @@ * #pw-body Field objects are managed by the `$fields` API variable. * #pw-use-constants * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2018 by Ryan Cramer * https://processwire.com * * @property int $id Numeric ID of field in the database #pw-group-properties @@ -26,6 +26,8 @@ * @property string $description Longer description text for the field #pw-group-properties * @property string $notes Additional notes text about the field #pw-group-properties * @property string $icon Icon name used by the field, if applicable #pw-group-properties + * @property string $tags Tags that represent this field, if applicable (space separated string). #pw-group-properties + * @property-read array $tagList Same as $tags property, but as an array. #pw-group-properties * @property bool $useRoles Whether or not access control is enabled #pw-group-access * @property array $editRoles Role IDs with edit access, applicable only if access control is enabled. #pw-group-access * @property array $viewRoles Role IDs with view access, applicable only if access control is enabled. #pw-group-access @@ -203,6 +205,14 @@ class Field extends WireData implements Saveable, Exportable { */ protected $inputfieldSettings = array(); + /** + * Tags assigned to this field, keys are lowercase version of tag, values can possibly contain mixed case + * + * @var null|array + * + */ + protected $tagList = null; + /** * True if lowercase tables should be enforce, false if not (null = unset). Cached from $config * @@ -341,6 +351,7 @@ class Field extends WireData implements Saveable, Exportable { * */ public function get($key) { + if($key === 'type' && isset($this->settings['type'])) { $value = $this->settings['type']; if($value) $value->setLastAccessField($this); @@ -355,6 +366,8 @@ class Field extends WireData implements Saveable, Exportable { else if($key == 'icon') return $this->getIcon(true); else if($key == 'useRoles') return ($this->settings['flags'] & self::flagAccess) ? true : false; else if($key == 'flags') return $this->settings['flags']; + else if($key == 'tagList') return $this->getTags(); + else if($key == 'tags') return $this->getTags(true); $value = parent::get($key); if($key === 'allowContexts' && !is_array($value)) $value = array(); @@ -1255,6 +1268,104 @@ class Field extends WireData implements Saveable, Exportable { return $this; } + /** + * Get tags + * + * @param bool|string $getString Optionally specify true for space-separated string, or delimiter string (default=false) + * @return array|string Returns array of tags unless $getString option is requested + * @since 3.0.106 + * + */ + public function getTags($getString = false) { + if($this->tagList === null) { + $tagList = $this->setTags(parent::get('tags')); + } else { + $tagList = $this->tagList; + } + if($getString !== false) { + $delimiter = $getString === true ? ' ' : $getString; + return implode($delimiter, $tagList); + } + return $tagList; + } + + /** + * Set all tags + * + * #pw-internal + * + * @param array $tagList Array of tags to add + * @param bool $reindex Set to false to set given $tagsList exactly as-is (assumes it's already in correct format) + * @return array Array of tags that were set + * @since 3.0.106 + * + */ + public function setTags($tagList, $reindex = true) { + if($tagList === null || $tagList === '') { + $tagList = array(); + } else if(!is_array($tagList)) { + $tagList = explode(' ', $tagList); + } + if($reindex && count($tagList)) { + $tags = array(); + foreach($tagList as $tag) { + $tag = trim($tag); + if(strlen($tag)) $tags[strtolower($tag)] = $tag; + } + $tagList = $tags; + } + if($this->tagList !== $tagList) { + $this->tagList = $tagList; + parent::set('tags', implode(' ', $tagList)); + $this->wire('fields')->getTags('reset'); + } + return $tagList; + } + + /** + * Add one or more tags + * + * @param string $tag + * @return array Returns current tag list + * @since 3.0.106 + * + */ + public function addTag($tag) { + $tagList = $this->getTags(); + $tagList[strtolower($tag)] = $tag; + $this->setTags($tagList, false); + return $tagList; + } + + /** + * Return true if this field has the given tag or false if not + * + * @param string $tag + * @return bool + * @since 3.0.106 + * + */ + public function hasTag($tag) { + $tagList = $this->getTags(); + return isset($tagList[strtolower(trim(ltrim($tag, '-')))]); + } + + /** + * Remove a tag + * + * @param string $tag + * @return array Returns current tag list + * @since 3.0.106 + * + */ + public function removeTag($tag) { + $tagList = $this->getTags(); + $tag = strtolower($tag); + if(!isset($tagList[$tag])) return $tagList; + unset($tagList[$tag]); + return $this->setTags($tagList, false); + } + /** * debugInfo PHP 5.6+ magic method * diff --git a/wire/core/Fields.php b/wire/core/Fields.php index 951a29df..b5bdc2cc 100644 --- a/wire/core/Fields.php +++ b/wire/core/Fields.php @@ -5,7 +5,7 @@ * * Manages collection of ALL Field instances, not specific to any particular Fieldgroup * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2018 by Ryan Cramer * https://processwire.com * * #pw-summary Manages all custom fields in ProcessWire @@ -37,6 +37,8 @@ class Fields extends WireSaveableItems { static protected $nativeNamesSystem = array( 'child', 'children', + 'count', + 'check_access', 'created_users_id', 'created', 'createdUser', @@ -44,11 +46,16 @@ class Fields extends WireSaveableItems { 'createdUsersID', 'data', 'description', + 'editUrl', + 'end', 'fieldgroup', 'fields', 'find', 'flags', 'get', + 'has_parent', + 'hasParent', + 'httpUrl', 'id', 'include', 'isNew', @@ -76,20 +83,33 @@ class Fields extends WireSaveableItems { 'templatePrevious', 'templates_id', 'url', + '_custom', ); /** * Field names that are native/permanent to this instance of ProcessWire (configurable at runtime) + * + * Array indexes are the names and values are all boolean true. * */ protected $nativeNamesLocal = array(); + /** + * Cache of all tags for all fields, populated to array when asked for the first time + * + * @var array|null + * + */ + protected $tagList = null; + /** * Construct * */ public function __construct() { $this->fieldsArray = new FieldsArray(); + // convert so that keys are names so that isset() can be used rather than in_array() + if(isset(self::$nativeNamesSystem[0])) self::$nativeNamesSystem = array_flip(self::$nativeNamesSystem); } /** @@ -207,6 +227,8 @@ class Fields extends WireSaveableItems { } } } + + $this->getTags('reset'); return true; } @@ -806,8 +828,8 @@ class Fields extends WireSaveableItems { * */ public function isNative($name) { - if(in_array($name, self::$nativeNamesSystem)) return true; - if(in_array($name, $this->nativeNamesLocal)) return true; + if(isset(self::$nativeNamesSystem[$name])) return true; + if(isset($this->nativeNamesLocal[$name])) return true; return false; } @@ -820,7 +842,76 @@ class Fields extends WireSaveableItems { * */ public function setNative($name) { - $this->nativeNamesLocal[] = $name; + $this->nativeNamesLocal[$name] = true; + } + + /** + * Get list of all tags used by fields + * + * - By default it returns an array of tag names where both keys and values are the tag names. + * - If you specify true for the `$getFields` argument, it returns an array where the keys are + * tag names and the values are arrays of field names in the tag. + * - If you specify "reset" for the `$getFields` argument it returns a blank array and resets + * internal tags cache. + * + * @param bool|string $getFieldNames Specify true to return associative array where keys are tags and values are field names + * …or specify the string "reset" to force getTags() to reset its cache, forcing it to reload on the next call. + * @return array + * @since 3.0.106 + * + */ + public function getTags($getFieldNames = false) { + + if($getFieldNames === 'reset') { + $this->tagList = null; + return array(); + } + + if($this->tagList === null) { + $tagList = array(); + foreach($this as $field) { + /** @var Field $field */ + $fieldTags = $field->getTags(); + foreach($fieldTags as $tag) { + if(!isset($tagList[$tag])) $tagList[$tag] = array(); + $tagList[$tag][] = $field->name; + } + } + ksort($tagList); + $this->tagList = $tagList; + } + + if($getFieldNames) return $this->tagList; + + $tagList = array(); + foreach($this->tagList as $tag => $fieldNames) { + $tagList[$tag] = $tag; + } + + return $tagList; + } + + /** + * Return all fields that have the given $tag + * + * Returns an associative array of `['field_name' => 'field_name']` if `$getFieldNames` argument is true, + * or `['field_name => Field instance]` if not (which is the default). + * + * @param string $tag Tag to find fields for + * @param bool $getFieldNames If true, returns array of field names rather than Field objects (default=false). + * @return array Array of Field objects, or array of field names if requested. Array keys are always field names. + * @since 3.0.106 + * + */ + public function findByTag($tag, $getFieldNames = false) { + $tags = $this->getTags(true); + $items = array(); + if(!isset($tags[$tag])) return $items; + foreach($tags[$tag] as $fieldName) { + $items[$fieldName] = ($getFieldNames ? $fieldName : $this->get($fieldName)); + } + ksort($items); + return $items; } /** diff --git a/wire/core/PageFinder.php b/wire/core/PageFinder.php index aec0f1e8..44ce7a1c 100644 --- a/wire/core/PageFinder.php +++ b/wire/core/PageFinder.php @@ -783,20 +783,45 @@ class PageFinder extends Wire { * */ protected function preProcessSelector(Selector $selector, Selectors $selectors, array $options, $level = 0) { - + + /** @var Fields $fields */ + $fields = $this->wire('fields'); $quote = $selector->quote; - $fields = $selector->fields; + $fieldsArray = $selector->fields; $hasDoubleDot = false; + $tags = null; - foreach($fields as $fn) { + foreach($fieldsArray as $key => $fn) { + $dot = strpos($fn, '.'); + $parts = $dot ? explode('.', $fn) : array($fn); + + // determine if it is a double-dot field (a.b.c) if($dot && strrpos($fn, '.') !== $dot) { if(strpos($fn, '__owner.') !== false) continue; $hasDoubleDot = true; - break; + } + + // determine if it is referencing any tags that should be coverted to field1|field2|field3 + foreach($parts as $partKey => $part) { + if($tags !== null && empty($tags)) continue; + if($fields->get($part)) continue; // maps to Field object + if($fields->isNative($part)) continue; // maps to native property + if($tags === null) $tags = $fields->getTags(true); // determine tags + if(!isset($tags[$part])) continue; // not a tag + $tagFields = $tags[$part]; + foreach($tagFields as $k => $fieldName) { + $_parts = $parts; + $_parts[$partKey] = $fieldName; + $tagFields[$k] = implode('.', $_parts); + } + if(count($tagFields)) { + unset($fieldsArray[$key]); + $selector->fields = array_merge($fieldsArray, $tagFields); + } } } - + if($quote == '[') { // selector contains another embedded selector that we need to convert to page IDs // i.e. field=[id>0, name=something, this=that] @@ -831,11 +856,11 @@ class PageFinder extends Wire { } else if($hasDoubleDot) { // has an "a.b.c" type string in the field, convert to a sub-selector - if(count($fields) > 1) { + if(count($fieldsArray) > 1) { throw new PageFinderSyntaxException("Multi-dot 'a.b.c' type selectors may not be used with OR '|' fields"); } - $fn = reset($fields); + $fn = reset($fieldsArray); $parts = explode('.', $fn); $fieldName = array_shift($parts); $field = $this->isPageField($fieldName); diff --git a/wire/modules/Process/ProcessField/ProcessField.js b/wire/modules/Process/ProcessField/ProcessField.js index 7e9b159e..b87af772 100644 --- a/wire/modules/Process/ProcessField/ProcessField.js +++ b/wire/modules/Process/ProcessField/ProcessField.js @@ -5,7 +5,7 @@ $(document).ready(function() { }; $("#templates_id").change(fieldFilterFormChange); $("#fieldtype").change(fieldFilterFormChange); - $("#show_system").click(fieldFilterFormChange); + $("#wrap_show_system input").click(fieldFilterFormChange); var $asmListItemStatus = $("#asmListItemStatus"); @@ -65,7 +65,7 @@ $(document).ready(function() { // instantiate the WireTabs var $fieldEdit = $("#ProcessFieldEdit"); - if($fieldEdit.size() > 0 && $('li.WireTab').size() > 1) { + if($fieldEdit.length > 0 && $('li.WireTab').length > 1) { $fieldEdit.find('script').remove(); $fieldEdit.WireTabs({ items: $(".Inputfields li.WireTab"), diff --git a/wire/modules/Process/ProcessField/ProcessField.min.js b/wire/modules/Process/ProcessField/ProcessField.min.js index b05db482..561ed709 100644 --- a/wire/modules/Process/ProcessField/ProcessField.min.js +++ b/wire/modules/Process/ProcessField/ProcessField.min.js @@ -1 +1 @@ -$(document).ready(function(){var e=function(){$("#field_filter_form").submit()};$("#templates_id").change(e);$("#fieldtype").change(e);$("#show_system").click(e);var b=$("#asmListItemStatus");var d=$("#columnWidth");function a(){var i=b.attr("data-tpl");if(!i){return}var k=$("#Inputfield_showIf").val();var j=$("#Inputfield_required").is(":checked")?true:false;if(k&&k.length>0){i=""+i}if(j){i=""+i}var h=parseInt(d.val());if(h==100){h=0}if(h>0){h=h+"%"}else{h=""}i=i.replace("%",h);b.val(i)}$("#Inputfield_showIf").change(a);$("#Inputfield_required").change(a);a();if(d.length>0){var f=$("
");var c=parseInt($("#columnWidth").val());d.val(c+"%");d.after(f);f.slider({range:"min",min:10,max:100,value:parseInt(d.val()),slide:function(i,h){var j=h.value+"%";d.val(j).trigger("change");a()}});d.change(function(){var h=parseInt($(this).val());if(h>100){h=100}if(h<10){h=10}$(this).val(h+"%");f.slider("option","value",h)})}var g=$("#ProcessFieldEdit");if(g.size()>0&&$("li.WireTab").size()>1){g.find("script").remove();g.WireTabs({items:$(".Inputfields li.WireTab"),id:"FieldEditTabs",skipRememberTabIDs:["delete"]})}$("#fieldgroupContextSelect").change(function(){var i=$("#Inputfield_id").val();var j=$(this).val();var h="./edit?id="+i;if(j>0){h+="&fieldgroup_id="+j}window.location=h});$("a.fieldFlag").click(function(){return false});$("#export_data").click(function(){$(this).select()});$(".import_toggle input[type=radio]").change(function(){var h=$(this).parents("p.import_toggle").next("table");var i=$(this).closest(".InputfieldFieldset");if($(this).is(":checked")&&$(this).val()==0){h.hide();i.addClass("ui-priority-secondary")}else{h.show();i.removeClass("ui-priority-secondary")}}).change();$("#wrap_Inputfield_send_templates").find(":input").change(function(){$("#_send_templates_changed").val("changed")});$("#viewRoles_37").click(function(){if($(this).is(":checked")){$("input.viewRoles").attr("checked","checked")}});$("input.viewRoles:not(#viewRoles_37)").click(function(){if($("#viewRoles_37").is(":checked")){return false}return true});$("input.editRoles:not(:disabled)").click(function(){if($(this).is(":checked")){$(this).closest("tr").find("input.viewRoles").attr("checked","checked")}});$(".override-select-all").click(function(){var h=$(this).closest("table").find("input[type=checkbox]");if($(this).hasClass("override-checked")){h.removeAttr("checked");$(this).removeClass("override-checked")}else{h.attr("checked","checked");$(this).addClass("override-checked")}return false})}); \ No newline at end of file +$(document).ready(function(){var e=function(){$("#field_filter_form").submit()};$("#templates_id").change(e);$("#fieldtype").change(e);$("#wrap_show_system input").click(e);var b=$("#asmListItemStatus");var d=$("#columnWidth");function a(){var i=b.attr("data-tpl");if(!i){return}var k=$("#Inputfield_showIf").val();var j=$("#Inputfield_required").is(":checked")?true:false;if(k&&k.length>0){i=""+i}if(j){i=""+i}var h=parseInt(d.val());if(h==100){h=0}if(h>0){h=h+"%"}else{h=""}i=i.replace("%",h);b.val(i)}$("#Inputfield_showIf").change(a);$("#Inputfield_required").change(a);a();if(d.length>0){var f=$("
");var c=parseInt($("#columnWidth").val());d.val(c+"%");d.after(f);f.slider({range:"min",min:10,max:100,value:parseInt(d.val()),slide:function(i,h){var j=h.value+"%";d.val(j).trigger("change");a()}});d.change(function(){var h=parseInt($(this).val());if(h>100){h=100}if(h<10){h=10}$(this).val(h+"%");f.slider("option","value",h)})}var g=$("#ProcessFieldEdit");if(g.length>0&&$("li.WireTab").length>1){g.find("script").remove();g.WireTabs({items:$(".Inputfields li.WireTab"),id:"FieldEditTabs",skipRememberTabIDs:["delete"]})}$("#fieldgroupContextSelect").change(function(){var i=$("#Inputfield_id").val();var j=$(this).val();var h="./edit?id="+i;if(j>0){h+="&fieldgroup_id="+j}window.location=h});$("a.fieldFlag").click(function(){return false});$("#export_data").click(function(){$(this).select()});$(".import_toggle input[type=radio]").change(function(){var h=$(this).parents("p.import_toggle").next("table");var i=$(this).closest(".InputfieldFieldset");if($(this).is(":checked")&&$(this).val()==0){h.hide();i.addClass("ui-priority-secondary")}else{h.show();i.removeClass("ui-priority-secondary")}}).change();$("#wrap_Inputfield_send_templates").find(":input").change(function(){$("#_send_templates_changed").val("changed")});$("#viewRoles_37").click(function(){if($(this).is(":checked")){$("input.viewRoles").attr("checked","checked")}});$("input.viewRoles:not(#viewRoles_37)").click(function(){if($("#viewRoles_37").is(":checked")){return false}return true});$("input.editRoles:not(:disabled)").click(function(){if($(this).is(":checked")){$(this).closest("tr").find("input.viewRoles").attr("checked","checked")}});$(".override-select-all").click(function(){var h=$(this).closest("table").find("input[type=checkbox]");if($(this).hasClass("override-checked")){h.removeAttr("checked");$(this).removeClass("override-checked")}else{h.attr("checked","checked");$(this).addClass("override-checked")}return false})}); \ No newline at end of file diff --git a/wire/modules/Process/ProcessField/ProcessField.module b/wire/modules/Process/ProcessField/ProcessField.module index aa4e4221..f93e5afc 100644 --- a/wire/modules/Process/ProcessField/ProcessField.module +++ b/wire/modules/Process/ProcessField/ProcessField.module @@ -48,7 +48,7 @@ class ProcessField extends Process implements ConfigurableModule { return array( 'title' => __('Fields', __FILE__), 'summary' => __('Edit individual fields that hold page data', __FILE__), - 'version' => 112, + 'version' => 113, 'permanent' => true, 'permission' => 'field-admin', // add this permission if you want this Process available for roles other than Superuser 'icon' => 'cube', @@ -128,19 +128,32 @@ class ProcessField extends Process implements ConfigurableModule { * */ public function init() { + if($this->input->urlSegment1 == 'edit') $this->modules->get("JqueryWireTabs"); + $this->moduleInfo = self::getModuleInfo(); $this->headline($this->moduleInfo['title']); + $this->labels = array( 'save' => $this->_('Save'), // Save button label 'import' => $this->_('Import'), 'export' => $this->_('Export'), - 'ok' => $this->_('Ok') - ); + 'ok' => $this->_('Ok'), + 'name' => $this->_x('Name', 'list thead'), + 'label' => $this->_x('Label', 'list thead'), + 'type' => $this->_x('Type', 'list thead'), + 'tag' => $this->_('Tag'), + 'tags' => $this->_('Tags'), + 'manage-tags' => $this->_('Manage Tags'), + 'fields' => $this->_('Fields'), + 'templates' => $this->_x('Templates', 'list thead quantity'), + 'yes' => $this->_x('Yes', 'access'), // General purpose "Yes" label + 'no' => $this->_x('No', 'access') // General purpose "No" label + ); - if($this->input->post->id) $this->id = (int) $this->input->post->id; - else $this->id = $this->input->get->id ? (int) $this->input->get->id : 0; - + $this->id = (int) $this->input->post('id'); + if(!$this->id) $this->id = (int) $this->input->get('id'); + if($this->id < 1) $this->id = 0; if($this->id) $this->field = $this->fields->get($this->id); if(!$this->field) $this->field = new Field(); @@ -196,7 +209,11 @@ class ProcessField extends Process implements ConfigurableModule { * */ public function ___getListFilterForm() { - + + /** @var Session $session */ + $session = $this->wire('session'); + /** @var WireInput $input */ + $input = $this->wire('input'); $showAllLabel = $this->_('Show All'); /** @var InputfieldForm $form */ @@ -223,14 +240,20 @@ class ProcessField extends Process implements ConfigurableModule { if($template->flags & Template::flagSystem) $name .= "*"; $field->addOption($template->id, $name); } - $this->session->ProcessFieldListTemplatesID = (int) $this->input->get->templates_id; - $field->label = $this->_('Filter by Template'); + $inputTemplatesID = $input->get('templates_id'); + if($inputTemplatesID !== null) { + $session->setFor($this, 'filterTemplate', (int) $inputTemplatesID); + $inputTemplatesID = null; + } + // $this->session->ProcessFieldListTemplatesID = (int) $this->input->get->templates_id; + $field->label = $this->_('Filter by template'); $field->description = $this->_("When selected, only the fields from a specific template will be shown. Built-in fields are also shown when filtering by template. Asterisk (*) indicates system templates."); // Filter by template description - $value = (int) $this->session->ProcessFieldListTemplatesID; - $field->attr('value', $value); - if($value && $template = $this->templates->get($value)) { + $field->icon = 'cubes'; + $filterTemplateID = (int) $session->getFor($this, 'filterTemplate'); // $this->session->ProcessFieldListTemplatesID; + $field->attr('value', $filterTemplateID); + if($filterTemplateID && $template = $this->templates->get($filterTemplateID)) { $form->description = sprintf($this->_('Showing fields from template: %s'), $template); - $this->wire('processHeadline', $this->_('Fields by Template')); // Page headline when filtering by template + $this->headline($this->_('Fields by Template')); // Page headline when filtering by template $fieldset->collapsed = Inputfield::collapsedNo; } else { $template = null; @@ -247,12 +270,17 @@ class ProcessField extends Process implements ConfigurableModule { foreach($this->fieldtypes as $fieldtype) { $field->addOption($fieldtype->name, $fieldtype->longName); } - $this->session->set('ProcessFieldListFieldtype', $this->sanitizer->name($this->input->get('fieldtype'))); - $field->label = $this->_('Filter by Field Type'); + $inputFieldtype = $input->get('fieldtype'); + if($inputFieldtype !== null) { + $session->setFor($this, 'filterFieldtype', $this->wire('sanitizer')->name($inputFieldtype)); + $inputFieldtype = null; + } + $field->label = $this->_('Filter by type'); $field->description = $this->_('When specified, only fields of the selected type will be shown. Built-in fields are also shown when filtering by field type.'); // Filter by fieldtype description - $value = $this->session->get('ProcessFieldListFieldtype'); - $field->attr('value', $value); - if($value && $fieldtype = $this->fieldtypes->get($value)) { + $field->icon = 'plug'; + $filterFieldtype = $session->getFor($this, 'filterFieldtype'); + $field->attr('value', $filterFieldtype); + if($filterFieldtype && $fieldtype = $this->fieldtypes->get($filterFieldtype)) { $form->description = sprintf($this->_('Showing fields of type: %s'), $fieldtype->longName); $fieldset->collapsed = Inputfield::collapsedNo; } else { @@ -262,16 +290,24 @@ class ProcessField extends Process implements ConfigurableModule { // ---------------------------------------------------------------- - if(is_null($template) && !$this->session->ProcessFieldListFieldtype) { - /** @var InputfieldCheckbox $field */ - $field = $this->modules->get("InputfieldCheckbox"); + if(is_null($template) && !$session->getFor($this, 'filterFieldtype')) { + /** @var InputfieldRadios $field */ + $field = $this->modules->get("InputfieldRadios"); $field->attr('id+name', 'show_system'); - $field->label = $this->_('Show built-in fields?'); + $field->label = $this->_('Show system fields?'); $field->description = $this->_("When checked, built-in fields will also be shown. These include system fields and permanent fields. System fields are required by the system and cannot be deleted or have their name changed. Permanent fields are those that cannot be removed from a template. These fields are used internally by ProcessWire."); // Show built-in fields description - $field->value = 1; + $field->addOption(1, $this->labels['yes']); + $field->addOption(0, $this->labels['no']); + $field->optionColumns = 1; + $field->icon = 'gear'; $field->collapsed = Inputfield::collapsedYes; - $this->session->ProcessFieldListShowSystem = (int) $this->input->get->show_system; - if($this->session->ProcessFieldListShowSystem) { + $inputShowSystem = $input->get('show_system'); + if($inputShowSystem !== null) { + $session->setFor($this, 'filterShowSystem', (int) $inputShowSystem); + $inputShowSystem = null; + } + $field->value = (int) $session->getFor($this, 'filterShowSystem'); + if($session->getFor($this, 'filterShowSystem')) { $field->attr('checked', 'checked'); $field->collapsed = Inputfield::collapsedNo; $fieldset->collapsed = Inputfield::collapsedNo; @@ -279,9 +315,11 @@ class ProcessField extends Process implements ConfigurableModule { } $fieldset->add($field); } else { - $this->session->ProcessFieldListShowSystem = 1; + // $session->setFor($this, 'filterShowSystem', 1); } + + return $form; } @@ -293,39 +331,40 @@ class ProcessField extends Process implements ConfigurableModule { */ public function ___execute() { if($this->wire('config')->ajax) return $this->renderListJSON(); - + + /** @var Session $session */ + $session = $this->wire('session'); $out = $this->getListFilterForm()->render() . "\n
\n"; $fieldsByTag = array(); $untaggedLabel = $this->_('Untagged'); - $hasFilters = $this->session->ProcessFieldListTemplatesID || $this->session->ProcessFieldListFieldtype; - $collapsedTags = array(); + $hasFilters = $session->getFor($this, 'filterTemplate') || $session->getFor($this, 'filterFieldtype'); + $showSystem = $session->getFor($this, 'filterShowSystem'); + $collapsedTags = $this->wire('modules')->getConfig($this, 'collapsedTags'); $caseTags = array(); // indexed by lowercase version of tag - + if(!is_array($collapsedTags)) $collapsedTags = array(); + $systemTag = $this->_x('System', 'tag'); // Tag applied to the group of built-in/system fields if(!$hasFilters) foreach($this->fields as $field) { - if($this->session->ProcessFieldListShowSystem) { - if($field->flags & Field::flagSystem || $field->flags & Field::flagPermanent) - $field->tags .= " " . $this->_x('Built-In', 'tag'); // Tag applied to the group of built-in/system fields + $tags = $field->getTags(); + if($showSystem || $hasFilters) { + if($field->flags & Field::flagSystem || $field->flags & Field::flagPermanent) { + $tags['system'] = $systemTag; + $caseTags[$systemTag] = $systemTag; + } } - if(empty($field->tags)) { + if(empty($tags)) { $tag = strtolower($untaggedLabel); if(!isset($fieldsByTag[$tag])) $fieldsByTag[$tag] = array(); $fieldsByTag[$tag][$field->name] = $field; $caseTags[$tag] = $untaggedLabel; continue; } - $tags = explode(' ', trim($field->tags)); - foreach($tags as $tag) { - if(empty($tag)) continue; - $caseTag = ltrim($tag, '-'); - $tag = strtolower($tag); - if(substr($tag, 0, 1) == '-') { - $tag = ltrim($tag, '-'); - $collapsedTags[] = $tag; - } + foreach($tags as $name => $tag) { if(!isset($fieldsByTag[$tag])) $fieldsByTag[$tag] = array(); $fieldsByTag[$tag][$field->name] = $field; - if(!isset($caseTags[$tag])) $caseTags[$tag] = $caseTag; + if(!isset($caseTags[$tag])) { + $caseTags[$tag] = sprintf($this->_('Tag: %s'), trim($tag, '_-')); + } } } @@ -339,6 +378,7 @@ class ProcessField extends Process implements ConfigurableModule { $f->entityEncodeLabel = false; $f->label = $caseTags[$tag]; $f->icon = 'tags'; + if($tag == $systemTag) $f->icon = 'gear'; $f->value = $this->getListTable($fields)->render(); if(in_array($tag, $collapsedTags)) $f->collapsed = Inputfield::collapsedYes; $form->add($f); @@ -358,6 +398,14 @@ class ProcessField extends Process implements ConfigurableModule { $button->icon = 'plus-circle'; $button->showInHeader(); $out .= $button->render(); + + $button = $this->modules->get('InputfieldButton'); + $button->id = 'tags_button'; + $button->href = './tags/'; + $button->icon = 'tags'; + $button->value = $this->labels['manage-tags']; + //$button->setSecondary(); + $out .= $button->render(); $button = $this->modules->get('InputfieldButton'); $button->id = 'import_button'; @@ -376,14 +424,173 @@ class ProcessField extends Process implements ConfigurableModule { $out .= $button->render(); if($this->input->nosave) { - $this->session->remove('ProcessFieldListTemplatesID'); - $this->session->remove('ProcessFieldListFieldtype'); - $this->session->remove('ProcessFieldListShowSystem'); + $session->removeFor($this, 'filterTemplate'); + $session->removeFor($this, 'filterFieldtype'); + $session->removeFor($this, 'filterShowSystem'); } return $out; } + /** + * Handle the “Manage Tags” actions + * + * @return string + * + */ + public function executeTags() { + + /** @var WireInput $input */ + $input = $this->wire('input'); + /** @var Modules $modules */ + $modules = $this->wire('modules'); + /** @var Fields $fields */ + $fields = $this->wire('fields'); + /** @var Sanitizer $sanitizer*/ + $sanitizer = $this->wire('sanitizer'); + /** @var InputfieldForm $form */ + $form = $modules->get('InputfieldForm'); + + $out = ''; + $labels = $this->labels; + $headline = $labels['tags']; + $this->headline($headline); + $this->breadcrumb('../', $labels['fields']); + + $tags = $fields->getTags(); + $editTag = $input->get->name('edit_tag'); + $saveTag = $input->post->name('save_tag'); + + $collapsedTags = $modules->getConfig($this, 'collapsedTags'); + if(!is_array($collapsedTags)) $collapsedTags = array(); + + if($editTag) { + // edit which fields are assigned to tag + $this->breadcrumb('./', $headline); + $this->headline("$labels[tag] - " . (isset($tags[$editTag]) ? $tags[$editTag] : $editTag)); + + /** @var InputfieldName $f */ + $f = $modules->get('InputfieldName'); + $f->attr('name', 'rename_tag'); + $f->attr('value', isset($tags[$editTag]) ? $tags[$editTag] : $editTag); + $f->collapsed = Inputfield::collapsedYes; + $f->addClass('InputfieldIsSecondary', 'wrapClass'); + $f->icon = 'tag'; + $form->add($f); + + /** @var InputfieldCheckboxes $f */ + $f = $modules->get('InputfieldCheckboxes'); + $f->attr('name', 'tag_fields'); + $f->label = $this->_('Select all fields that should have this tag'); + $f->table = true; + $f->icon = 'cube'; + $f->thead = "$labels[name]|$labels[label]|$labels[type]|$labels[tags]"; + $value = array(); + foreach($fields as $field) { + /** @var Field $field */ + if($field->flags & Field::flagSystem && !in_array($field->name, array('title', 'email'))) continue; + $f->addOption($field->name, "**$field->name**|$field->label|{$field->type->shortName}|" . $field->getTags(', ')); + if($field->hasTag($editTag)) $value[] = $field->name; + } + $f->attr('value', $value); + $form->add($f); + + /** @var InputfieldCheckbox */ + $f = $modules->get('InputfieldCheckbox'); + $f->attr('name', 'tag_collapsed'); + $f->label = $this->_('Display as collapsed in fields list?'); + if(in_array($editTag, $collapsedTags)) $f->attr('checked', 'checked'); + $form->add($f); + + /** @var InputfieldHidden $f */ + $f = $modules->get('InputfieldHidden'); + $f->attr('name', 'save_tag'); + $f->attr('value', $editTag); + $form->appendMarkup = "

" . wireIconMarkup('trash-o') . ' ' . + $this->_('To delete this tag, remove all fields from it.') . "

"; + $form->add($f); + + } else if($saveTag) { + // save tag + $tagFields = $sanitizer->names($input->post('tag_fields')); + $renameTag = $input->post->fieldName('rename_tag'); + $isCollapsed = (int) $input->post('tag_collapsed'); + $removeTag = ''; + if($renameTag && $renameTag != $saveTag) { + $removeTag = $saveTag; + $saveTag = $renameTag; + } + foreach($fields as $field) { + /** @var Field $field */ + if($removeTag && $field->hasTag($removeTag)) { + $field->removeTag($removeTag); + } + if(in_array($field->name, $tagFields)) { + // field should have the given tag + if($field->hasTag($saveTag)) continue; + $field->addTag($saveTag); + $this->message(sprintf($this->_('Added tag “%1$s” to field: %2$s'), $saveTag, $field->name)); + } else if($field->hasTag($saveTag)) { + // field should not have the given tag + $field->removeTag($saveTag); + $this->message(sprintf($this->_('Removed tag “%1$s” from field: %2$s'), $saveTag, $field->name)); + } + if($field->isChanged('tags')) $field->save(); + } + $_collapsedTags = $collapsedTags; + if($isCollapsed) { + if(!in_array($saveTag, $collapsedTags)) $collapsedTags[] = $saveTag; + } else { + $key = array_search($saveTag, $collapsedTags); + if($key !== false) unset($collapsedTags[$key]); + } + if($collapsedTags !== $_collapsedTags) { + $modules->saveConfig($this, 'collapsedTags', $collapsedTags); + } + $this->wire('session')->redirect('./'); + return ''; + + } else { + // list defined tags + $out .= "

" . + $this->_('Tags enable you to create collections of fields for listing or searching.') . "

"; + /** @var MarkupAdminDataTable $table */ + $table = $modules->get('MarkupAdminDataTable'); + $table->setSortable(false); + $table->setEncodeEntities(false); + $table->headerRow(array($labels['name'], $labels['fields'])); + + foreach($tags as $key => $tag) { + $tagFields = $fields->findByTag($tag, true); + $table->row(array( + $tag => "./?edit_tag=$tag", + implode(', ', $tagFields) + )); + if($fields->get($key)) { + $this->warning(sprintf($this->_('Warning: tag “%s” has the same name as a Field.'), $tag)); + } + } + + if(count($tags)) $out .= $table->render(); + + $form->attr('method', 'get'); + /** @var InputfieldName $f */ + $f = $modules->get('InputfieldName'); + $f->attr('name', 'edit_tag'); + $f->label = $this->_('Add new tag'); + $f->icon = 'tag'; + $f->addClass('InputfieldIsSecondary', 'wrapClass'); + $form->add($f); + } + + $f = $modules->get('InputfieldSubmit'); + $form->add($f); + $out .= $form->render(); + + return $out; + } + + /** * Get the table that lists fields * @@ -395,13 +602,9 @@ class ProcessField extends Process implements ConfigurableModule { /** @var MarkupAdminDataTable $table */ $table = $this->modules->get("MarkupAdminDataTable"); + $labels = $this->labels; - $headerRow = array( - $this->_x('Name', 'list thead'), - $this->_x('Label', 'list thead'), - $this->_x('Type', 'list thead'), - $this->_x('Templates', 'list thead quantity') - ); + $headerRow = array($labels['name'], $labels['label'], $labels['type'], $labels['templates']); $table->headerRow($headerRow); $table->setEncodeEntities(false); @@ -464,11 +667,11 @@ class ProcessField extends Process implements ConfigurableModule { $flags = array(); $builtIn = false; - $templatesID = $this->session->ProcessFieldListTemplatesID; + $templatesID = (int) $this->session->getFor($this, 'filterTemplate'); if($templatesID && $template = $this->templates->get($templatesID)) { if(!$template->fieldgroup->has($field)) return array(); } - if($fieldtype = $this->session->ProcessFieldListFieldtype) { + if($fieldtype = $this->session->getFor($this, 'filterFieldtype')) { if($field->type != $fieldtype) return array(); } @@ -489,8 +692,8 @@ class ProcessField extends Process implements ConfigurableModule { if($field->showIf) $flags[] = 'Dependency'; if($field->required) $flags[] = 'Required'; - if($builtIn && !$templatesID && $field->name != 'title') { - if(!$this->session->ProcessFieldListShowSystem) return array(); + if($builtIn && !$templatesID && $field->name != 'title' && $field->name != 'email') { + if(!$this->session->getFor($this, 'filterShowSystem')) return array(); } foreach($field->getFieldgroups() as $fieldgroup) { @@ -740,7 +943,7 @@ class ProcessField extends Process implements ConfigurableModule { $out = "
" . "