From 2f908e44e77a3d5dfeea468b06e3142f893079fb Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Wed, 27 Feb 2019 11:55:08 -0500 Subject: [PATCH] Add support for previously unimplemented status Page::statusUnique, which adds support for globally unique page names. This commit also changes the the previous commit Page::statusIncomplete to value 128 because it turns out 256 was used in a 3rd party module and it seemed safer to use 128, which was occuped by Page::statusVersions, which has never been used. I've also changed the name of Page::statusIncomplete to Page::statusFlagged since the status indicates an error occurred during last interactive save rather than specifically incomplete. --- wire/core/Page.php | 46 +++++--- wire/core/PageFinder.php | 12 +- wire/core/PagesEditor.php | 1 + wire/core/PagesNames.php | 106 +++++++++++++++++- .../InputfieldPageAutocomplete.min.js | 2 +- .../InputfieldSelector.module | 15 +++ .../ProcessPageAdd/ProcessPageAdd.module | 20 +++- .../ProcessPageEdit/ProcessPageEdit.module | 36 +++--- .../ProcessPageListActions.php | 2 +- .../ProcessPageListRenderJSON.php | 2 +- 10 files changed, 191 insertions(+), 51 deletions(-) diff --git a/wire/core/Page.php b/wire/core/Page.php index 06ea2af3..81aa71f3 100644 --- a/wire/core/Page.php +++ b/wire/core/Page.php @@ -168,24 +168,21 @@ class Page extends WireData implements \Countable, WireMatchable { * Status levels 1024 and above are excluded from search by the core. Status levels 16384 and above are runtime only and not * stored in the DB unless for logging or page history. * - * If the under 1024 status flags are expanded in the future, it must be ensured that the combined value of the searchable flags - * never exceeds 1024, otherwise issues in Pages::find() will need to be considered. - * * The status levels 16384 and above can safely be changed as needed as they are runtime only. * - * Please note that statuses 2, 32, 256, and 4096 are reserved for future use. + * Please note that all other statuses are reserved for future use. * */ /** - * Base status for pages in use (assigned automatically) + * Base status for pages, represents boolean true (1) or false (0) as flag with other statuses, for internal use purposes only * #pw-internal * */ const statusOn = 1; - + /** - * Reserved status + * Reserved status (internal use) * #pw-internal * */ @@ -226,18 +223,30 @@ class Page extends WireData implements \Countable, WireMatchable { const statusDraft = 64; /** - * Page has version data available (name: "versions"). + * Page is flagged as incomplete, needing review, or having some issue + * ProcessPageEdit uses this status to indicate an error message occurred during last internactive save * #pw-internal + * @since 3.0.127 * */ - const statusVersions = 128; - + const statusFlagged = 128; + const statusIncomplete = 128; // alias of statusFlagged + /** - * Page might have incomplete data because there were errors when last saved interactively or may be missing required fields + * Deprecated, was never used, but kept in case any modules referenced it * #pw-internal + * @deprecated * */ - const statusIncomplete = 256; + const statusVersions = 128; + + /** + * Reserved for internal use + * #pw-internal + * @since 3.0.127 + * + */ + const statusInternal = 256; /** * Page is temporary. 1+ day old unpublished pages with this status may be automatically deleted (name: "temp"). @@ -309,8 +318,8 @@ class Page extends WireData implements \Countable, WireMatchable { 'system' => self::statusSystem, 'unique' => self::statusUnique, 'draft' => self::statusDraft, - 'versions' => self::statusVersions, - 'incomplete' => self::statusIncomplete, + 'flagged' => self::statusFlagged, + 'internal' => self::statusInternal, 'temp' => self::statusTemp, 'hidden' => self::statusHidden, 'unpublished' => self::statusUnpublished, @@ -318,6 +327,8 @@ class Page extends WireData implements \Countable, WireMatchable { 'deleted' => self::statusDeleted, 'systemOverride' => self::statusSystemOverride, 'corrupted' => self::statusCorrupted, + 'max' => self::statusMax, + 'on' => self::statusOn, ); /** @@ -1884,9 +1895,11 @@ class Page extends WireData implements \Countable, WireMatchable { if($this->settings['status'] & Page::statusSystemID) $value = $value | Page::statusSystemID; if($this->settings['status'] & Page::statusSystem) $value = $value | Page::statusSystem; } - if($this->settings['status'] != $value) { + if($this->settings['status'] != $value && $this->isLoaded) { $this->trackChange('status', $this->settings['status'], $value); - $this->statusPrevious = $this->settings['status']; + if($this->statusPrevious === null) { + $this->statusPrevious = $this->settings['status']; + } } $this->settings['status'] = $value; if($value & Page::statusDeleted) { @@ -3744,6 +3757,7 @@ class Page extends WireData implements \Countable, WireMatchable { $names = array(); $remainder = $status; foreach(self::$statuses as $name => $value) { + if($value <= self::statusOn || $value >= self::statusMax) continue; if($status & $value) { $names[$value] = $name; $remainder = $remainder & ~$value; diff --git a/wire/core/PageFinder.php b/wire/core/PageFinder.php index bb6e33ee..82e2a091 100644 --- a/wire/core/PageFinder.php +++ b/wire/core/PageFinder.php @@ -209,14 +209,8 @@ class PageFinder extends Wire { $value = $selector->value; if(!ctype_digit("$value")) { // allow use of some predefined labels for Page statuses - if($value == 'hidden') $selector->value = Page::statusHidden; - else if($value == 'unpublished') $selector->value = Page::statusUnpublished; - else if($value == 'draft') $selector->value = Page::statusDraft; - else if($value == 'versions') $selector->value = Page::statusVersions; - else if($value == 'locked') $selector->value = Page::statusLocked; - else if($value == 'trash') $selector->value = Page::statusTrash; - else if($value == 'max') $selector->value = Page::statusMax; - else $selector->value = 1; + $statuses = Page::getStatuses(); + $selector->value = isset($statuses[$value]) ? $statuses[$value] : 1; } $not = false; if(($selector->operator == '!=' && !$selector->not) || ($selector->not && $selector->operator == '=')) { @@ -229,8 +223,8 @@ class PageFinder extends Wire { $selectors[$key] = $this->wire(new SelectorBitwiseAnd('status', $selector->value)); } else { - $not = $selector->not; // some other operator like: >, <, >=, <= + $not = $selector->not; } if(!$not && (is_null($maxStatus) || $selector->value > $maxStatus)) $maxStatus = (int) $selector->value; diff --git a/wire/core/PagesEditor.php b/wire/core/PagesEditor.php index 488afd7d..0992c03d 100644 --- a/wire/core/PagesEditor.php +++ b/wire/core/PagesEditor.php @@ -436,6 +436,7 @@ class PagesEditor extends Wire { } } + $this->pages->names()->checkNameConflicts($page); if(!$this->savePageQuery($page, $options)) return false; $result = $this->savePageFinish($page, $isNew, $options); if($language) $user->language = $language; // restore language diff --git a/wire/core/PagesNames.php b/wire/core/PagesNames.php index 193c8e2d..c68648e7 100644 --- a/wire/core/PagesNames.php +++ b/wire/core/PagesNames.php @@ -3,9 +3,9 @@ /** * ProcessWire Pages Names * - * ProcessWire 3.x, Copyright 2018 by Ryan Cramer + * ProcessWire 3.x, Copyright 2019 by Ryan Cramer * https://processwire.com - * + * */ class PagesNames extends Wire { @@ -69,7 +69,6 @@ class PagesNames extends Wire { * @param Page $page * @param string $format * @return string Returns page name that was assigned - * @throws WireException * */ public function setupNewPageName(Page $page, $format = '') { @@ -551,6 +550,7 @@ class PagesNames extends Wire { $wheres[] = 'parent_id=:parent_id'; $binds[':parent_id'] = $parentID; } + if($pageID) { $wheres[] = 'id!=:id'; $binds[':id'] = $pageID; @@ -654,4 +654,104 @@ class PagesNames extends Wire { return $this->untitledPageName; } + /** + * Does given page have a name that has a conflict/collision? + * + * In multi-language environment this applies to default language only. + * + * @param Page $page Page to check + * @return string|bool Returns string with conflict reason or boolean false if no conflict + * @throws WireException If given invalid $options argument + * @since 3.0.127 + * + */ + public function pageNameHasConflict(Page $page) { + + $reason = ''; + + $sql = "SELECT id, status, parent_id FROM pages WHERE name=:name AND id!=:id"; + $query = $this->wire('database')->prepare($sql); + $query->bindValue(':name', $page->name); + $query->bindValue(':id', $page->id, \PDO::PARAM_INT); + $query->execute(); + + if(!$query->rowCount()) { + $query->closeCursor(); + return false; + } + + while($row = $query->fetch(\PDO::FETCH_ASSOC)) { + if($row['status'] & Page::statusUnique) { + // name is already required to be unique globally + $reason = sprintf($this->_("Another page is using name “%s” and requires it to be globally unique"), $page->name); + } + if((int) $row['parent_id'] === $page->parent_id) { + // name already consumed by another page with same parent + $reason = sprintf($this->_('Another page with same parent is already using name “%s”'), $page->name); + } + if($reason) break; + } + + // page requires that it be the only one with this name, so if others have it, then disallow + if(!$reason && $page->hasStatus(Page::statusUnique)) { + $reason = sprintf($this->_('Cannot use name “%s” as globally unique because it is already used by other page(s)'), $page->name); + } + + $query->closeCursor(); + + return $reason ? $reason : false; + } + + /** + * Check given page’s name for conflicts and increment as needed while also triggering a warning notice + * + * @param Page $page + * @since 3.0.127 + * + */ + public function checkNameConflicts(Page $page) { + + $checkName = false; + $checkStatus = false; + $namePrevious = $page->namePrevious; + $statusPrevious = $page->statusPrevious; + $isNew = $page->isNew(); + $nameChanged = !$isNew && $namePrevious !== null && $namePrevious !== $page->name; + + if($isNew || $nameChanged) { + // new page or changed name + $checkName = true; + } else if($statusPrevious !== null && $page->hasStatus(Page::statusUnique) && !($statusPrevious & Page::statusUnique)) { + // page just received 'unique' status + $checkStatus = true; + } + + if(!$checkName && !$checkStatus) return; + + do { + + $conflict = $this->pageNameHasConflict($page); + if(!$conflict) break; + + $this->warning($conflict); + + if($checkName) { + if($nameChanged) { + // restore previous name + $page->name = $page->namePrevious; + $nameChanged = false; + } else { + // increment name + $page->name = $this->incrementName($page->name); + } + + } else if($checkStatus) { + // remove 'unique' status + $page->removeStatus(Page::statusUnique); + break; + } + + } while($conflict); + } + } diff --git a/wire/modules/Inputfield/InputfieldPageAutocomplete/InputfieldPageAutocomplete.min.js b/wire/modules/Inputfield/InputfieldPageAutocomplete/InputfieldPageAutocomplete.min.js index 18348bc3..63047353 100644 --- a/wire/modules/Inputfield/InputfieldPageAutocomplete/InputfieldPageAutocomplete.min.js +++ b/wire/modules/Inputfield/InputfieldPageAutocomplete/InputfieldPageAutocomplete.min.js @@ -1 +1 @@ -var InputfieldPageAutocomplete={init:function(p,e,h,o,j){var f=$("#"+p);var k=$("#"+p+"_items");var c=$("#"+p+"_input");var a=c.parent().find(".InputfieldPageAutocompleteStatus");var i=c.parent().find(".InputfieldPageAutocompleteNote");var g=0;var l=0;var q=c.attr("data-disablechars");var b=c.hasClass("no_list");function m(u,v){var x=u.height();if(x){var w=u.parent().height();var t=((w-x)/2);u.css("top",t+"px");if(v=="left"){u.css("left",(t/2)+"px")}else{if(v=="right"){u.css("right",(t/4)+"px")}}}else{}}function n(u){if(!q||!q.length){return false}var t=false;for(var v=0;v-1){t=true;break}}return t}m(a,"left");if(b){c.attr("data-selectedLabel",c.val());var d=c.siblings(".InputfieldPageAutocompleteRemove");m(d,"right");d.click(function(){f.val("").change();c.val("").attr("placeholder","").attr("data-selectedLabel","").change().focus();c.trigger("keydown")});c.change(function(){if($(this).val().length==0){d.hide()}else{d.show()}});c.focus(function(){var t=f.val();if(!t.length){return}if(n(t)){return}if($(this).hasClass("added_item")){return}$(this).attr("placeholder",$(this).attr("data-selectedLabel"));$(this).val("")}).blur(function(){setTimeout(function(){},200)})}a.click(function(){c.focus()});a.attr("data-class",a.attr("class"));function s(){var t=$("#_"+p.replace("Inputfield_","")+"_add_items").size()>0;return t}c.one("focus",function(){c.autocomplete({minLength:2,source:function(v,t){var u=v.term;if(n(u)){t([]);return}a.attr("class","fa fa-fw fa-spin fa-spinner");if(c.hasClass("and_words")&&u.indexOf(" ")>0){u=u.replace(/\s+/,",")}u=encodeURIComponent(u);var w=e+"&"+o+j+u;$.getJSON(w,function(x){a.attr("class",a.attr("data-class"));l=x.total;if(x.total>0){a.attr("class","fa fa-fw fa-angle-double-down")}else{if(s()){a.attr("class","fa fa-fw fa-plus-circle");i.show()}else{a.attr("class","fa fa-fw fa-frown-o")}}t($.map(x.matches,function(y){return{label:y[h],value:y[h],page_id:y.id}}))})},select:function(t,u){if(!u.item){return}var v=$(this);if(v.hasClass("no_list")){v.val(u.item.label).change();v.attr("data-selectedLabel",u.item.label);v.closest(".InputfieldPageAutocomplete").find(".InputfieldPageAutocompleteData").val(u.item.page_id).change();v.blur();return false}else{InputfieldPageAutocomplete.pageSelected(k,u.item);v.val("").focus();return false}}}).blur(function(){var t=$(this);a.attr("class",a.attr("data-class"));i.hide();if(t.hasClass("no_list")){if(f.val().length||t.val().length){if(t.hasClass("allow_any")||t.hasClass("added_item")){}else{t.val(t.attr("data-selectedLabel")).attr("placeholder","")}}else{t.val("").attr("placeholder","").attr("data-selectedLabel","")}}if(t.hasClass("focus-after-blur")){t.removeClass("focus-after-blur");setTimeout(function(){t.focus()},250)}}).keyup(function(){a.attr("class",a.attr("data-class"))}).keydown(function(u){if(u.keyCode==13){u.preventDefault();if(s()){if($.trim(c.val()).length<1){c.blur();return false}g++;var v={page_id:(-1*g),label:c.val()};if(b){f.val(v.page_id);$("#_"+p.replace("Inputfield_","")+"_add_items").val(v.label);c.addClass("added_item").blur();var t=i.siblings(".InputfieldPageAutocompleteNoteAdd");if(!t.length){var t=$("
");i.after(t)}t.text(i.attr("data-adding")+" "+v.label);t.show()}else{InputfieldPageAutocomplete.pageSelected(k,v);c.val("").blur().focus()}i.hide()}else{$(this).addClass("focus-after-blur").blur()}return false}if(g&&b){var t=i.siblings(".InputfieldPageAutocompleteNoteAdd");var w=$("#_"+p.replace("Inputfield_","")+"_add_items");if(t.length&&w.val()!=$(this).val()){t.remove();f.val("");w.val("");$("#_"+p.replace("Inputfield_","")+"_add_items").val("");g--}}})});var r=function(t){t.sortable({axis:"y",update:function(v,u){InputfieldPageAutocomplete.rebuildInput($(this))},start:function(v,u){u.item.addClass("ui-state-highlight")},stop:function(v,u){u.item.removeClass("ui-state-highlight")}});t.addClass("InputfieldPageAutocompleteSortable")};$("#"+k.attr("id")).on("mouseover",">li",function(){$(this).removeClass("ui-state-default").addClass("ui-state-hover");r(k)}).on("mouseout",">li",function(){$(this).removeClass("ui-state-hover").addClass("ui-state-default")})},initFromInputfield:function(a){var b=a.find(".InputfieldPageAutocompleteData");if(!b.length){return}if(b.hasClass("InputfieldPageAutocompleteInit")){return}InputfieldPageAutocomplete.init(b.attr("id"),b.attr("data-url"),b.attr("data-label"),b.attr("data-search"),b.attr("data-operator"));b.addClass("InputfieldPageAutocompleteInit")},pageSelected:function(a,d){var c=false;a.children("li:not(.itemTemplate)").each(function(){var f=parseInt($(this).children(".itemValue").text());if(f==d.page_id){c=$(this)}});var b=$("#"+a.attr("data-id")+"_input");b.blur();if(c){c.effect("highlight");return}var e=a.children(".itemTemplate").clone();e.removeClass("itemTemplate");e.children(".itemValue").text(d.page_id);e.children(".itemLabel").text(d.label);a.append(e);InputfieldPageAutocomplete.rebuildInput(a)},rebuildInput:function(d){var b=d.attr("data-id");var a=d.attr("data-name");var f=$("#"+b);var h="";var c="";var g=parseInt(f.attr("data-max"));var i=d.children(":not(.itemTemplate)");if(g>0&&i.size()>g){while(i.size()>g){i=i.slice(1)}d.children(":not(.itemTemplate)").replaceWith(i)}i.each(function(){var j=parseInt($(this).children(".itemValue").text());if(j>0){h+=","+j}else{if(j<0){h+=","+j;c+=$(this).children(".itemLabel").text()+"\n"}}});f.val(h);var e=$("#_"+a+"_add_items");if(e.size()>0){e.val(c)}}};$(document).ready(function(){$(".InputfieldPageAutocomplete").each(function(){InputfieldPageAutocomplete.initFromInputfield($(this))});$(document).on("reloaded",".InputfieldPageAutocomplete, .InputfieldPage",function(){InputfieldPageAutocomplete.initFromInputfield($(this))});$(document).on("click",".InputfieldPageAutocomplete ol a.itemRemove",function(){var c=$(this).parent();var a=c.parent();var b=c.children(".itemValue").text();c.remove();InputfieldPageAutocomplete.rebuildInput(a);return false})}); \ No newline at end of file +var InputfieldPageAutocomplete={init:function(id,url,labelField,searchField,operator){var $value=$("#"+id);var $ol=$("#"+id+"_items");var $input=$("#"+id+"_input");var $icon=$input.parent().find(".InputfieldPageAutocompleteStatus");var $note=$input.parent().find(".InputfieldPageAutocompleteNote");var numAdded=0;var numFound=0;var disableChars=$input.attr("data-disablechars");var noList=$input.hasClass("no_list");function hasDisableChar(str){if(!disableChars||!disableChars.length)return false;var disable=false;for(var n=0;n-1){disable=true;break}}return disable}InputfieldPageAutocomplete.setIconPosition($icon,"left");if(noList){$input.attr("data-selectedLabel",$input.val());var $remove=$input.siblings(".InputfieldPageAutocompleteRemove");InputfieldPageAutocomplete.setIconPosition($remove,"right");$remove.click(function(){$value.val("").change();$input.val("").attr("placeholder","").attr("data-selectedLabel","").change().focus();$input.trigger("keydown")});$input.change(function(){if($(this).val().length==0){$remove.hide()}else{$remove.show()}});$input.focus(function(){var val=$value.val();if(!val.length)return;if(hasDisableChar(val))return;if($(this).hasClass("added_item"))return;$(this).attr("placeholder",$(this).attr("data-selectedLabel"));$(this).val("")}).blur(function(){setTimeout(function(){},200)})}$icon.click(function(){$input.focus()});$icon.attr("data-class",$icon.attr("class"));function isAddAllowed(){var allowed=$("#_"+id.replace("Inputfield_","")+"_add_items").size()>0;return allowed}$input.one("focus",function(){$input.autocomplete({minLength:2,source:function(request,response){var term=request.term;if(hasDisableChar(term)){response([]);return}$icon.attr("class","fa fa-fw fa-spin fa-spinner");if($input.hasClass("and_words")&&term.indexOf(" ")>0){term=term.replace(/\s+/,",")}term=encodeURIComponent(term);var ajaxURL=url+"&"+searchField+operator+term;$.getJSON(ajaxURL,function(data){$icon.attr("class",$icon.attr("data-class"));numFound=data.total;if(data.total>0){$icon.attr("class","fa fa-fw fa-angle-double-down")}else if(isAddAllowed()){$icon.attr("class","fa fa-fw fa-plus-circle");$note.show()}else{$icon.attr("class","fa fa-fw fa-frown-o")}response($.map(data.matches,function(item){return{label:item[labelField],value:item[labelField],page_id:item.id}}))})},select:function(event,ui){if(!ui.item)return;var $t=$(this);if($t.hasClass("no_list")){$t.val(ui.item.label).change();$t.attr("data-selectedLabel",ui.item.label);$t.closest(".InputfieldPageAutocomplete").find(".InputfieldPageAutocompleteData").val(ui.item.page_id).change();$t.blur();return false}else{InputfieldPageAutocomplete.pageSelected($ol,ui.item);$t.val("").focus();return false}}}).blur(function(){var $input=$(this);$icon.attr("class",$icon.attr("data-class"));$note.hide();if($input.hasClass("no_list")){if($value.val().length||$input.val().length){if($input.hasClass("allow_any")||$input.hasClass("added_item")){}else{$input.val($input.attr("data-selectedLabel")).attr("placeholder","")}}else{$input.val("").attr("placeholder","").attr("data-selectedLabel","")}}if($input.hasClass("focus-after-blur")){$input.removeClass("focus-after-blur");setTimeout(function(){$input.focus()},250)}}).keyup(function(){$icon.attr("class",$icon.attr("data-class"))}).keydown(function(event){if(event.keyCode==13){event.preventDefault();if(isAddAllowed()){if($.trim($input.val()).length<1){$input.blur();return false}numAdded++;var page={page_id:-1*numAdded,label:$input.val()};if(noList){$value.val(page.page_id);$("#_"+id.replace("Inputfield_","")+"_add_items").val(page.label);$input.addClass("added_item").blur();var $addNote=$note.siblings(".InputfieldPageAutocompleteNoteAdd");if(!$addNote.length){var $addNote=$("
");$note.after($addNote)}$addNote.text($note.attr("data-adding")+" "+page.label);$addNote.show()}else{InputfieldPageAutocomplete.pageSelected($ol,page);$input.val("").blur().focus()}$note.hide()}else{$(this).addClass("focus-after-blur").blur()}return false}if(numAdded&&noList){var $addNote=$note.siblings(".InputfieldPageAutocompleteNoteAdd");var $addText=$("#_"+id.replace("Inputfield_","")+"_add_items");if($addNote.length&&$addText.val()!=$(this).val()){$addNote.remove();$value.val("");$addText.val("");$("#_"+id.replace("Inputfield_","")+"_add_items").val("");numAdded--}}})});var makeSortable=function($ol){$ol.sortable({axis:"y",update:function(e,data){InputfieldPageAutocomplete.rebuildInput($(this))},start:function(e,data){data.item.addClass("ui-state-highlight")},stop:function(e,data){data.item.removeClass("ui-state-highlight")}});$ol.addClass("InputfieldPageAutocompleteSortable")};$("#"+$ol.attr("id")).on("mouseover",">li",function(){$(this).removeClass("ui-state-default").addClass("ui-state-hover");makeSortable($ol)}).on("mouseout",">li",function(){$(this).removeClass("ui-state-hover").addClass("ui-state-default")})},initFromInputfield:function($inputfield){var $a=$inputfield.find(".InputfieldPageAutocompleteData");if(!$a.length)return;if($a.hasClass("InputfieldPageAutocompleteInit"))return;InputfieldPageAutocomplete.init($a.attr("id"),$a.attr("data-url"),$a.attr("data-label"),$a.attr("data-search"),$a.attr("data-operator"));$a.addClass("InputfieldPageAutocompleteInit")},setIconPosition:function($icon,side){var iconHeight=$icon.height();if(iconHeight){var pHeight=$icon.parent().height();var iconTop=(pHeight-iconHeight)/2;$icon.css("top",iconTop+"px");if(side=="left"){$icon.css("left",iconTop/2+"px")}else if(side=="right"){$icon.css("right",iconTop/4+"px")}}else{}},pageSelected:function($ol,page){var dup=false;$ol.children("li:not(.itemTemplate)").each(function(){var v=parseInt($(this).children(".itemValue").text());if(v==page.page_id)dup=$(this)});var $inputText=$("#"+$ol.attr("data-id")+"_input");$inputText.blur();if(dup){dup.effect("highlight");return}var $li=$ol.children(".itemTemplate").clone();$li.removeClass("itemTemplate");$li.children(".itemValue").text(page.page_id);$li.children(".itemLabel").text(page.label);$ol.append($li);InputfieldPageAutocomplete.rebuildInput($ol)},rebuildInput:function($ol){var id=$ol.attr("data-id");var name=$ol.attr("data-name");var $input=$("#"+id);var value="";var addValue="";var max=parseInt($input.attr("data-max"));var $children=$ol.children(":not(.itemTemplate)");if(max>0&&$children.size()>max){while($children.size()>max)$children=$children.slice(1);$ol.children(":not(.itemTemplate)").replaceWith($children)}$children.each(function(){var v=parseInt($(this).children(".itemValue").text());if(v>0){value+=","+v}else if(v<0){value+=","+v;addValue+=$(this).children(".itemLabel").text()+"\n"}});$input.val(value);var $addItems=$("#_"+name+"_add_items");if($addItems.size()>0)$addItems.val(addValue)}};$(document).ready(function(){$(".InputfieldPageAutocomplete").each(function(){InputfieldPageAutocomplete.initFromInputfield($(this))});$(document).on("reloaded",".InputfieldPageAutocomplete, .InputfieldPage",function(){InputfieldPageAutocomplete.initFromInputfield($(this))});$(document).on("click",".InputfieldPageAutocomplete ol a.itemRemove",function(){var $li=$(this).parent();var $ol=$li.parent();var id=$li.children(".itemValue").text();$li.remove();InputfieldPageAutocomplete.rebuildInput($ol);return false});$(document).on("wiretabclick",function(a,$tab){var $icon=$tab.find(".InputfieldPageAutocompleteStatus");InputfieldPageAutocomplete.setIconPosition($icon,"left");$icon=$tab.find(".InputfieldPageAutocompleteRemove");InputfieldPageAutocomplete.setIconPosition($icon,"right")})}); \ No newline at end of file diff --git a/wire/modules/Inputfield/InputfieldSelector/InputfieldSelector.module b/wire/modules/Inputfield/InputfieldSelector/InputfieldSelector.module index 144f8d35..5c3e142b 100644 --- a/wire/modules/Inputfield/InputfieldSelector/InputfieldSelector.module +++ b/wire/modules/Inputfield/InputfieldSelector/InputfieldSelector.module @@ -525,6 +525,21 @@ class InputfieldSelector extends Inputfield implements ConfigurableModule { //'parent' => $this->_('parent'), ); + $ignoreStatuses = array( + Page::statusOn, + Page::statusReserved, + Page::statusSystem, + Page::statusSystemID, + ); + + foreach(Page::getStatuses() as $name => $status) { + if($status > Page::statusTrash) continue; + if($status === Page::statusDraft && !$this->wire('modules')->isInstalled('ProDrafts')) continue; + if(in_array($status, $ignoreStatuses)) continue; + if(isset($this->systemFields['status']['options'][$name])) continue; // use existing label + $this->systemFields['status']['options'][$name] = ucfirst($name); + } + if(!count($users)) { unset($this->systemFields['modified_users_id']['options']); unset($this->systemFields['created_users_id']['options']); diff --git a/wire/modules/Process/ProcessPageAdd/ProcessPageAdd.module b/wire/modules/Process/ProcessPageAdd/ProcessPageAdd.module index d808010d..2781825f 100644 --- a/wire/modules/Process/ProcessPageAdd/ProcessPageAdd.module +++ b/wire/modules/Process/ProcessPageAdd/ProcessPageAdd.module @@ -419,18 +419,30 @@ class ProcessPageAdd extends Process implements ConfigurableModule, WirePageEdit * */ public function executeExists() { + + /** @var Pages $pages */ + $pages = $this->wire('pages'); + $parentID = (int) $this->wire('input')->get('parent_id'); if(!$parentID) return ''; + $parent = $this->wire('pages')->get($parentID); if(!$parent->addable()) return ''; + $name = $this->wire('sanitizer')->pageNameUTF8($this->wire('input')->get('name')); if(!strlen($name)) return ''; + $parentID = count($this->predefinedParents) ? $this->predefinedParents : $parentID; - $page = $this->wire('pages')->get("parent_id=$parentID, name=" . $this->wire('sanitizer')->selectorValue($name) . ", include=all"); - if($page->id) { - $out = " " . $this->_('Already taken') . ""; + + $test = new Page(); + $test->parent_id = $parentID; + $test->name = $name; + $reason = $pages->names()->pageNameHasConflict($test); + + if($reason) { + $out = "" . wireIconMarkup('exclamation-triangle') . " $reason"; } else { - $out = " " . $this->_('Ok') . ""; + $out = "" . wireIconMarkup('check-square-o') . ' ' . $this->_('Ok') . ""; } return $out; } diff --git a/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module b/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module index 058d706b..4e41853f 100644 --- a/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module +++ b/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module @@ -350,7 +350,7 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod $this->set('noticeUnknown', $this->_("Unknown page")); // Init error: Unknown page $this->set('noticeLocked', $this->_("This page is locked for edits")); // Init error: Page is locked $this->set('noticeNoAccess', $this->_("You don't have access to edit")); // Init error: User doesn't have access - $this->set('noticeIncomplete', $this->_("This page might have one or more incomplete fields (attempt to save or publish for more info)")); // Init error: User doesn't have access + $this->set('noticeIncomplete', $this->_("This page might have one or more incomplete fields (attempt to save or publish for more info)")); $settings = $this->config->pageEdit; if(is_array($settings)) $this->configSettings = array_merge($this->configSettings, $settings); @@ -541,7 +541,7 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod } else { $this->error($this->noticeLocked); // Page locked error } - } else if(!$this->isPost && $this->page->hasStatus(Page::statusIncomplete) && !$this->input->get('s')) { + } else if(!$this->isPost && $this->page->hasStatus(Page::statusFlagged) && !$this->input->get('s')) { $this->warning($this->noticeIncomplete); } @@ -1607,25 +1607,28 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod $status = (int) $this->page->status; $statuses = array(); + $debug = $this->config->debug; + $advanced = $this->config->advanced; /** @var InputfieldCheckboxes $field */ $field = $this->modules->get('InputfieldCheckboxes'); $field->attr('name', 'status'); $field->icon = 'sliders'; - + + if(!$this->page->template->noUnpublish && $this->page->publishable()) { + $statuses[Page::statusUnpublished] = $this->_('Unpublished: Not visible on site'); // Settings: Unpublished status checkbox label + } if($this->user->hasPermission('page-hide', $this->page)) { $statuses[Page::statusHidden] = $this->_('Hidden: Excluded from lists and searches'); // Settings: Hidden status checkbox label } if($this->user->hasPermission('page-lock', $this->page)) { $statuses[Page::statusLocked] = $this->_('Locked: Not editable'); // Settings: Locked status checkbox label } - if(!$this->page->template->noUnpublish && $this->page->publishable()) { - $statuses[Page::statusUnpublished] = $this->_('Unpublished: Not visible on site'); // Settings: Unpublished status checkbox label - } if($this->user->isSuperuser()) { - // $statuses[Page::statusUnique] = sprintf($this->_('Unique: Name “%s” may not be used by any other page in the system'), $this->page->name); - if($this->config->advanced) { + $statuses[Page::statusUnique] = sprintf($this->_('Unique: Require page name “%s” to be globally unique'), $this->page->name) . + ($this->wire('languages') ? ' ' . $this->_('(in default language only)') : ''); + if($advanced) { $statuses[Page::statusSystemID] = "System: Non-deleteable and locked ID (status not removeable via API)"; $statuses[Page::statusSystem] = "System: Non-deleteable and locked ID, name, template, parent (status not removeable via API)"; } @@ -1635,13 +1638,14 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod foreach($statuses as $s => $label) { if($s & $status) $value[] = $s; + if(strpos($label, ': ')) $label = str_replace(': ', ': [span.detail]', $label) . '[/span]'; $field->addOption($s, $label); } $field->attr('value', $value); $field->label = $this->_('Status'); // Settings: Status field label - if($this->config->debug) $field->notes = $this->page->statusStr; + if($debug) $field->notes = $this->page->statusStr; return $field; } @@ -1916,15 +1920,15 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod if($notice instanceof NoticeError) $formErrors++; } - // if any Inputfields threw errors during processing, give the page an 'incomplete' status + // if any Inputfields threw errors during processing, give the page a 'flagged' status // so that it can later be identified the page may be missing something if($formErrors && count($this->form->getErrors())) { - // add incomplete status when form had errors - $this->page->addStatus(Page::statusIncomplete); - } else if($this->page->hasStatus(Page::statusIncomplete)) { + // add flagged status when form had errors + $this->page->addStatus(Page::statusFlagged); + } else if($this->page->hasStatus(Page::statusFlagged)) { // if no errors, remove incomplete status - $this->page->removeStatus(Page::statusIncomplete); - $this->message($this->_('Removed incomplete status because no errors reported during save')); + $this->page->removeStatus(Page::statusFlagged); + $this->message($this->_('Removed flagged status because no errors reported during save')); } $isUnpublished = $this->page->hasStatus(Page::statusUnpublished); @@ -2262,7 +2266,7 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod if($this->user->hasPermission('page-lock', $this->page)) $statusFlags[] = Page::statusLocked; if($this->user->isSuperuser()) { - // $statusFlags[] = Page::statusUnique; + $statusFlags[] = Page::statusUnique; if($this->config->advanced) { $statusFlags[] = Page::statusSystemID; $statusFlags[] = Page::statusSystem; diff --git a/wire/modules/Process/ProcessPageList/ProcessPageListActions.php b/wire/modules/Process/ProcessPageList/ProcessPageListActions.php index 6105a848..6ee8b85c 100644 --- a/wire/modules/Process/ProcessPageList/ProcessPageListActions.php +++ b/wire/modules/Process/ProcessPageList/ProcessPageListActions.php @@ -125,7 +125,7 @@ class ProcessPageListActions extends Wire { if(!$locked && !$trash && !$noSettings && $statusEditable) { if($page->publishable()) { if($page->isUnpublished()) { - if(!$page->hasStatus(Page::statusIncomplete)) { + if(!$page->hasStatus(Page::statusFlagged)) { $extras['pub'] = array( 'cn' => 'Publish', 'name' => $this->actionLabels['pub'], diff --git a/wire/modules/Process/ProcessPageList/ProcessPageListRenderJSON.php b/wire/modules/Process/ProcessPageList/ProcessPageListRenderJSON.php index e6de93b5..506402dc 100644 --- a/wire/modules/Process/ProcessPageList/ProcessPageListRenderJSON.php +++ b/wire/modules/Process/ProcessPageList/ProcessPageListRenderJSON.php @@ -81,7 +81,7 @@ class ProcessPageListRenderJSON extends ProcessPageListRender { if($page->hasStatus(Page::statusTemp)) $icons[] = 'bolt'; if($page->hasStatus(Page::statusLocked)) $icons[] = 'lock'; if($page->hasStatus(Page::statusDraft)) $icons[] = 'paperclip'; - if($page->hasStatus(Page::statusIncomplete)) $icons[] = 'exclamation-triangle'; + if($page->hasStatus(Page::statusFlagged)) $icons[] = 'exclamation-triangle'; $numChildren = $this->numChildren($page, 1); $numTotal = strpos($this->qtyType, 'total') !== false ? $page->numDescendants : $numChildren; }