From a82ee80c4184e1195d26f6e534435bba9b4bdcb6 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Wed, 27 Oct 2021 14:21:22 -0400 Subject: [PATCH] Various small improvements to FieldtypeRepeater. Plus add a "Storage settings" configuration that lets you configure it to use fewer pages for storage (lazyParents mode), as well as a tool to delete unnecessary pages that already exist. --- .../FieldsetPageInstructions.php | 17 +- .../FieldtypeFieldsetPage.module | 25 +- .../FieldtypeRepeater.module | 573 ++++++++++++------ .../FieldtypeRepeater/RepeaterField.php | 21 + .../FieldtypeRepeater/RepeaterPageArray.php | 76 ++- .../Fieldtype/FieldtypeRepeater/config.php | 142 ++++- 6 files changed, 618 insertions(+), 236 deletions(-) diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/FieldsetPageInstructions.php b/wire/modules/Fieldtype/FieldtypeRepeater/FieldsetPageInstructions.php index 8072a31a..c89965e4 100644 --- a/wire/modules/Fieldtype/FieldtypeRepeater/FieldsetPageInstructions.php +++ b/wire/modules/Fieldtype/FieldtypeRepeater/FieldsetPageInstructions.php @@ -14,6 +14,7 @@ function FieldsetPageInstructions(Field $field) { } } + $span = "span class='description' style='display:block;border-bottom:1px solid #ddd;margin-bottom:4px;padding:8px 0 4px 0'"; $f = $field->wire('modules')->get('InputfieldMarkup'); $f->attr('name', '_instructions'); $f->label = __('Instructions on how to use this field'); @@ -29,29 +30,29 @@ function FieldsetPageInstructions(Field $field) { "

" . "

" . - "" . __('Getting a value:') . "
" . + "<$span>" . __('Getting a value:') . "" . "\$$exampleName = \$page->{$field->name}->$exampleName;" . "

" . "

" . - "" . __('Outputting a value:') . "
" . + "<$span>" . __('Outputting a value:') . "" . "echo \$page->{$field->name}->$exampleName;" . "

" . "

" . - "" . __('Outputting a value when in markup:') . "
" . + "<$span>" . __('Outputting a value when in markup:') . "" . "<div class='example'>
" . "  <?=\$page->{$field->name}->$exampleName?>
" . "</div>" . "

" . "

" . - "" . __('Setting a value:') . "
" . + "<$span>" . __('Setting a value:') . "" . "\$page->{$field->name}->$exampleName = '$exampleText';" . "

" . "

" . - "" . __('Setting and saving a value:') . "
" . + "<$span>" . __('Setting and saving a value:') . "" . "\$page->of(false); // " . __('this turns off output formatting, when necessary') . "
" . "\$page->{$field->name}->$exampleName = '$exampleText';
" . @@ -59,15 +60,15 @@ function FieldsetPageInstructions(Field $field) { "

" . "

" . - "" . __('Assigning fieldset to another (shorter) variable and outputting a value:') . "
" . + "<$span>" . __('Assigning fieldset to another (shorter) variable and outputting a value:') . "" . "\$p = \$page->{$field->name};
" . "echo \$p->$exampleName;" . "

" . "

" . - "" . + "<$span>" . sprintf(__('Finding pages having fieldset with “%s” field containing text “example”:'), $exampleName) . - "
" . + "" . "\$items = \$pages->find('$field->name.$exampleName%=example');" . "

"; diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeFieldsetPage.module b/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeFieldsetPage.module index b1779adf..2de9b9cd 100644 --- a/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeFieldsetPage.module +++ b/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeFieldsetPage.module @@ -7,7 +7,7 @@ require_once(dirname(__FILE__) . '/FieldtypeRepeater.module'); * * Maintains a collection of fields as a fieldset via an independent Page. * - * ProcessWire 3.x, Copyright 2017 by Ryan Cramer + * ProcessWire 3.x, Copyright 2021 by Ryan Cramer * https://processwire.com * * @property int $repeatersRootPageID @@ -137,7 +137,7 @@ class FieldtypeFieldsetPage extends FieldtypeRepeater implements ConfigurableMod $hasField = $page->hasField($field->name); /** @var InputfieldRepeater $inputfield */ - $inputfield = $this->wire('modules')->get($this->getInputfieldClass()); + $inputfield = $this->wire()->modules->get($this->getInputfieldClass()); $inputfield->set('page', $page); $inputfield->set('field', $field); $inputfield->set('repeaterDepth', 0); @@ -261,13 +261,13 @@ class FieldtypeFieldsetPage extends FieldtypeRepeater implements ConfigurableMod } else { - $f = $this->wire('fields')->get($subfield); + $f = $this->wire()->fields->get($subfield); if(!$f) return $query; // unknown subfield // match fields from the repeater template, perform a separate find() operation for the subfield $templateID = $field->get('template_id'); - $value = $this->wire('sanitizer')->selectorValue($value); - $ids = $this->wire('pages')->findIDs("templates_id=$templateID, include=all, $f->name$operator$value"); + $value = $this->wire()->sanitizer->selectorValue($value); + $ids = $this->wire()->pages->findIDs("templates_id=$templateID, include=all, $f->name$operator$value"); // use the IDs found from the separate find() as our getMatchQuery if(count($ids)) { @@ -328,10 +328,11 @@ class FieldtypeFieldsetPage extends FieldtypeRepeater implements ConfigurableMod * * @param Page $page * @param Field $field + * @param bool $create * @return Page * */ - protected function getRepeaterPageParent(Page $page, Field $field) { + protected function getRepeaterPageParent(Page $page, Field $field, $create = true) { return $this->getRepeaterParent($field); } @@ -440,15 +441,16 @@ class FieldtypeFieldsetPage extends FieldtypeRepeater implements ConfigurableMod if($value->isHidden()) $value->removeStatus(Page::statusHidden); } - $this->wire('pages')->save($value, array('uncacheAll' => false)); + $this->wire()->pages->save($value, array('uncacheAll' => false)); - $table = $this->wire('database')->escapeTable($field->table); + $database = $this->wire()->database; + $table = $database->escapeTable($field->table); $sql = "INSERT INTO `$table` (pages_id, data) VALUES(:pages_id, :data) " . "ON DUPLICATE KEY UPDATE `data`=VALUES(`data`)"; - $query = $this->wire('database')->prepare($sql); + $query = $database->prepare($sql); $query->bindValue(':pages_id', $page->id, \PDO::PARAM_INT); $query->bindValue(':data', $value->id, \PDO::PARAM_INT); $result = $query->execute(); @@ -459,14 +461,15 @@ class FieldtypeFieldsetPage extends FieldtypeRepeater implements ConfigurableMod /** * Return configuration fields definable for each FieldtypePage * - * @param Field $field + * @param Field|RepeaterField $field * @return InputfieldWrapper * */ public function ___getConfigInputfields(Field $field) { $inputfields = parent::___getConfigInputfields($field); - + + /** @var InputfieldAsmSelect $f */ $f = $inputfields->getChildByName('repeaterFields'); $f->label = $this->_('Fields in fieldset'); $f->description = $this->_('Define the fields that are used by this fieldset. You may also drag and drop fields to the desired order.'); diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module b/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module index 61d37a7f..454e6649 100644 --- a/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module +++ b/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module @@ -13,7 +13,6 @@ * https://processwire.com * * @todo: automatic sorting. - * @todo: look into possibility of only creating repeater parents when there is at least one item. * * @property int $repeatersRootPageID * @method saveConfigInputfields(Field $field, Template $template, Page $parent) @@ -34,7 +33,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { return array( 'title' => __('Repeater', __FILE__), // Module Title 'summary' => __('Maintains a collection of fields that are repeated for any number of times.', __FILE__), // Module Summary - 'version' => 107, + 'version' => 108, 'autoload' => true, 'installs' => 'InputfieldRepeater' ); @@ -44,6 +43,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { const fieldPageNamePrefix = 'for-field-'; const repeaterPageNamePrefix = 'for-page-'; const defaultRepeaterMaxItems = 0; + const repeatersRootPageName = 'repeaters'; const collapseExisting = 0; const collapseNone = 3; @@ -92,7 +92,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { * */ public function init() { - $this->wire('pages')->addHookAfter('deleteReady', $this, 'hookPagesDelete'); + $this->wire()->pages->addHookAfter('deleteReady', $this, 'hookPagesDelete'); parent::init(); // $this->initFields(); } @@ -106,7 +106,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { // $this->initFields(); // intentionally repeated from init() // make sure that all templates used by repeater pages enforce a Page type of RepeaterPage - foreach($this->wire('fields') as $field) { + foreach($this->wire()->fields as $field) { $fieldtype = $field->type; if(!$fieldtype || !$fieldtype instanceof FieldtypeRepeater) continue; /** @var FieldtypeRepeater $fieldtype */ @@ -120,20 +120,11 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { $template->save(); } - /** @var Page $page */ - $page = $this->wire('page'); - - /** @var Process|null $process */ - $process = $page->process; - - /** @var User $user */ - $user = $this->wire('user'); - - /** @var Config $config */ - $config = $this->wire('config'); - - /** @var WireInput $input */ - $input = $this->wire('input'); + $page = $this->wire()->page; + $process = $page->process; /** @var Process|null $process */ + $user = $this->wire()->user; + $config = $this->wire()->config; + $input = $this->wire()->input; $inEditor = $process == 'ProcessPageEdit' || $process == 'ProcessProfile'; $isSuperuser = $user->isSuperuser(); @@ -145,10 +136,10 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { if($inEditor && $config->ajax) { // handle scenario of repeater within repeater field - $fieldName = $input->get('field'); + $fieldName = (string) $input->get('field'); $pageID = (int) $input->get('id'); if($pageID && strpos($fieldName, '_repeater') && preg_match('/^(.+)_repeater\d+$/', $fieldName, $matches)) { - $editPage = $this->wire('pages')->get($pageID); + $editPage = $this->wire()->pages->get($pageID); if($editPage->id && strpos($editPage->template->name, self::templateNamePrefix) === 0) { // update field name to exclude the _repeater1234 part at the end, so that PageEdit recognizes it $input->get->__set('field', $matches[1]); @@ -159,7 +150,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { if(!$inEditor && !$user->isGuest() && !$isSuperuser && $user->hasPermission('page-edit')) { // allow for front-end editor to also trigger an inEditor=true condition if(strpos($page->url, $config->urls->admin) === false && $page->editable()) { - if($this->wire('modules')->isInstalled('PageFrontEdit')) $inEditor = true; + if($this->wire()->modules->isInstalled('PageFrontEdit')) $inEditor = true; } } @@ -169,7 +160,6 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { } $this->addHookBefore('PageFinder::getQuery', $this, 'hookPageFinderGetQuery'); - } /** @@ -560,11 +550,9 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { $fieldParent = $pages->get($field->parent_id); if(!$fieldParent->id) continue; $p = $fieldParent->child('include=all, name=' . self::repeaterPageNamePrefix . $page->id); - if(!$p->id) continue; - $p->addStatus(Page::statusSystemOverride); - $p->removeStatus(Page::statusSystem); - $this->message("Deleted page {$p->path}", Notice::debug); - $pages->delete($p, true); + if($p->id && $this->deleteRepeaterPage($p, $field, true)) { + $this->bd("Deleted page $p->path for page $page->path", __FUNCTION__); + } } } @@ -586,7 +574,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { * * @param Page $page * @param Field $field - * @return PageArray + * @return PageArray|RepeaterPageArray * */ public function getBlankValue(Page $page, Field $field) { @@ -715,8 +703,8 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { * Also ensures that the parent repeater page exists. * This is public so that the Inputfield can pull from it too. * - * @param Field $field * @param Page $page The page that the repeater field lives on + * @param Field $field * @return Page * */ @@ -766,7 +754,12 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { if(is_array($value) && !empty($value['parent_id'])) { // this is what we get if there was a record in the DB and the parent has been setup - $parent_id = (int) $value['parent_id']; + $parent_id = (int) $value['parent_id']; + + } else if(empty($value['data']) && empty($value['parent_id']) && $this->useLazyParents($field)) { + // no record in the DB yet and parent will not be created till needed + $parent = $this->getRepeaterPageParent($page, $field, false); + $parent_id = $parent->id; } else { // no record in the DB yet, so setup the parent if it isn't already @@ -778,7 +771,10 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { if(!$template_id) $template_id = $this->getRepeaterTemplate($field)->id; // if we were unable to determine a parent for some reason, then just return a blank pageArray - if(!$parent_id || !$template_id) return $this->getBlankValue($page, $field); + if(!$parent_id || !$template_id) { + $pageArray = $this->getBlankValue($page, $field); + return $pageArray; + } // build the selector: find pages with our parent // $selector = "parent_id=$parent_id, templates_id=$template_id, sort=sort, check_access=0"; @@ -813,7 +809,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { ); $pageArray = $this->wire()->pages->find($selector, $options); - $pageArray->resetTrackChanges(true); + $pageArray->resetTrackChanges(true); return $pageArray; @@ -842,25 +838,38 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { /** @var RepeaterPageArray $value */ - $count = 0; + $numPublished = 0; + $numTotal = 0; $ids = array(); // iterate through the array and count how many published we have foreach($value as $p) { + $numTotal++; if(!$p->id || $p->isHidden() || $p->isUnpublished()) continue; $ids[] = $p->id; - $count++; + $numPublished++; } - - // our sleepValue is simply just the total number of repeater pages - // a cache of page IDs in 'data' (for export portability) - // and a quick reference to the parent where they are contained - $sleepValue = array( - 'data' => implode(',', $ids), - 'count' => $count, - 'parent_id' => $this->getRepeaterPageParent($page, $field)->id, - ); - + + if(!$numTotal && $this->useLazyParents($field)) { + $parent = $this->getRepeaterPageParent($page, $field, false); + $sleepValue = array( + 'data' => '', + 'count' => 0, + 'parent_id' => $parent->id + ); + + } else { + // our sleepValue is simply just the total number of repeater pages + // a cache of page IDs in 'data' (for export portability) + // and a quick reference to the parent where they are contained + $parent = $this->getRepeaterPageParent($page, $field); + $sleepValue = array( + 'data' => implode(',', $ids), + 'count' => $numPublished, + 'parent_id' => $parent->id + ); + } + return $sleepValue; } @@ -1079,6 +1088,21 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { return $info; } + /** + * Get repeaters root page + * + * @return Page + * @since 3.0.188 + * + */ + public function getRepeatersRootPage() { + $pages = $this->wire()->pages; + $page = $pages->get((int) $this->repeatersRootPageID); + if($page->id && $page->name === self::repeatersRootPageName) return $page; + $page = $pages->get($this->wire()->config->adminRootPageID)->child('name=repeaters, include=all'); + return $page; + } + /** * Return the parent used by the repeater pages for the given Page and Field * @@ -1086,15 +1110,17 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { * * @param Page $page * @param Field $field - * @return Page + * @param bool $create Create if not exists? (default=true) 3.0.188+ + * @return Page|NullPage * */ - protected function getRepeaterPageParent(Page $page, Field $field) { + protected function getRepeaterPageParent(Page $page, Field $field, $create = true) { $repeaterParent = $this->getRepeaterParent($field); $parentName = self::repeaterPageNamePrefix . $page->id; // for-page-123 $parent = $repeaterParent->child("name=$parentName, include=all"); - if($parent->id) return $parent; + + if($parent->id || !$create) return $parent; $parent = $this->wire()->pages->newPage($repeaterParent->template); $parent->parent = $repeaterParent; @@ -1107,7 +1133,8 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { if($this->deletePageField === $field->get('parent_id')) return $parent; $parent->save(); - $this->message("Created '$field' page parent: " . $parent->path, Notice::debug); + + $this->bd("Created '$field' page parent: $parent->path", __FUNCTION__); return $parent; } @@ -1132,7 +1159,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { if($parent->id) return $parent; } - $repeatersRootPage = $pages->get((int) $this->repeatersRootPageID); + $repeatersRootPage = $this->getRepeatersRootPage(); $parentName = self::fieldPageNamePrefix . $field->id; // for-field-123 // we call this just to ensure it exists, so template is created if it doesn't exist yet @@ -1147,7 +1174,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { $parent->title = $field->name; $parent->addStatus(Page::statusSystem); $parent->save(); - $this->message("Created '$field' parent: $parent->path", Notice::debug); + $this->bd("Created '$field' parent: $parent->path", __FUNCTION__); } if($parent->id) { @@ -1173,7 +1200,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { protected function updateRepeaterTemplateName(Template $template, $name) { if($template->name != $name && !$this->wire()->templates->get($name)) { - $this->message("Renamed repeater template from '$template->name' to '$name'", Notice::debug); + $this->bd("Renamed repeater template from '$template->name' to '$name'", __FUNCTION__); $flags = $template->flags; $template->flags = Template::flagSystemOverride; $template->flags = 0; @@ -1245,7 +1272,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { $field->set('template_id', $template->id); $field->save(); - $this->message("Created '$field' template: $template", Notice::debug); + $this->bd("Created '$field' template: $template", __FUNCTION__); return $template; } @@ -1302,6 +1329,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { } if(!is_array($value)) $value = array($value); + foreach($value as $p) $pageArray->add($p); return $pageArray; @@ -1316,7 +1344,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { * @return Page|PageArray * */ - protected function sanitizeValueString(Page $page, Field $field, $value) { + public function sanitizeValueString(Page $page, Field $field, $value) { $pages = $this->wire()->pages; if($page) {} // ignore @@ -1334,10 +1362,12 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { $v = (int) $v; if($v) $result[] = $v; } - $result = $pages->getById($result, $this->getRepeaterTemplate($field), $field->get('parent_id')); + // @todo confirm this is the parent_id we want (field parent_id vs page parent_id) + $result = $pages->getById($result, $this->getRepeaterTemplate($field), $field->get('parent_id')); } else if(Selectors::stringHasOperator($value)) { // selector + // @todo confirm this is the parent_id we want (field parent_id vs page parent_id) $parentID = $field->get('parent_id'); $templateID = $field->get('template_id'); $result = $pages->find("parent_id=$parentID, templates_id=$templateID, $value"); @@ -1558,9 +1588,10 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { // pages that will be deleted $deletePages = $value->getItemsRemoved(); - $repeaterParent = $this->getRepeaterPageParent($page, $field); - $parent_id = $repeaterParent->id; + $repeaterParent = null; + $parent_id = 0; $template_id = $field->get('template_id'); + $numTotal = count($value); // iterate through each page in the pageArray value // and determine which need to be saved @@ -1568,16 +1599,19 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { /** @var Page|RepeaterPage $p */ if($p->template->id != $template_id) { $value->remove($p); - $this->message("Removed invalid template ({$p->template->name}) page {$p->path} from field $field", Notice::debug); + $this->bd("Removed invalid template ({$p->template->name}) page {$p->path} from field $field", __FUNCTION__); continue; } - + + if(!$repeaterParent) $repeaterParent = $this->getRepeaterPageParent($page, $field); + if(!$parent_id) $parent_id = $repeaterParent->id; + if($p->parent->id != $parent_id) { // clone the individual repeater pages $value->remove($p); $p = $pages->clone($p, $repeaterParent, false, $saveOptions); $value->add($p); - $this->message("Cloned to {$p->path} from field $field", Notice::debug); + $this->bd("Cloned to {$p->path} from field $field", __FUNCTION__); continue; } @@ -1612,7 +1646,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { if($isHidden && $isUnpublished) continue; // this is a 'ready' page, we can ignore $changes = implode(', ', $p->getChanges()); - $this->message("Saved '$field' page: {$p->path} " . ($changes ? "($changes)" : ''), Notice::debug); + $this->bd("Saved '$field' page: {$p->path} " . ($changes ? "($changes)" : ''), __FUNCTION__); if($isUnpublished && $isOn && $isProcessed && !$hasErrors) { // publish requested and allowed @@ -1620,7 +1654,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { } } else { - $this->message("Added new '$field' page", Notice::debug); + $this->bd("Added new '$field' page", __FUNCTION__); } // save the repeater page @@ -1641,6 +1675,18 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { // ensure that any of our cloned page replacements (removes) don't get recorded any follow-up saves $value->resetTrackChanges(); + + if(!$numTotal && $this->useLazyParents($field)) { + // delete repeater page parent if it has no items below it + if(!$repeaterParent) $repeaterParent = $this->getRepeaterPageParent($page, $field, false); + if($repeaterParent && $repeaterParent->id) { + $numChildren = $pages->count("parent_id=$repeaterParent->id, include=all"); + if(!$numChildren) { + $this->bd("Deleted 0-children repeater parent $repeaterParent->path", __FUNCTION__); + $this->deleteRepeaterPage($repeaterParent, $field, true); + } + } + } return $result; } @@ -1682,7 +1728,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { // resursively delete the field parent and everything below it $pages->delete($parent, true); - $this->message("Deleted '$field' parent: $parentPath", Notice::debug); + $this->bd("Deleted '$field' parent: $parentPath", __FUNCTION__); } // delete the template used by this field @@ -1703,7 +1749,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { $fieldgroup = $fieldgroups->get($templateName); if($fieldgroup) $fieldgroups->delete($fieldgroup); - $this->message("Deleted '$field' template: $templateName", Notice::debug); + $this->bd("Deleted '$field' template: $templateName", __FUNCTION__); } return parent::___deleteField($field); @@ -1734,7 +1780,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { // remove system status from repeater page parent $parent->addStatus(Page::statusSystemOverride); $parent->removeStatus(Page::statusSystem); - $this->message("Deleted {$parent->path}", Notice::debug); + $this->bd("Deleted $parent->path", __FUNCTION__); // delete the repeater page parent and all the repeater pages in it $pages->delete($parent, true); } @@ -1742,9 +1788,142 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { return $result; } + + /** + * Create a cloned copy of Field + * + * @param Field $field + * @throws WireException + * + */ + public function ___cloneField(Field $field) { + throw new WireException($this->className() . " does not currently support field cloning."); + /* + $field = parent::___cloneField($field); + $field->parent_id = null; + $field->template_id = null; + */ + } + + /*** EXPORT AND IMPORT **********************************************************/ /** - * Delete old ready pages that are just taking up space + * Export configuration values for external consumption + * + * Use this method to externalize any config values when necessary. + * For example, internal IDs should be converted to GUIDs where possible. + * Most Fieldtype modules can use the default implementation already provided here. + * + * #pw-group-configuration + * + * @param Field $field + * @param array $data + * @return array + * + */ + public function ___exportConfigData(Field $field, array $data) { + + $data = parent::___exportConfigData($field, $data); + + $template = $this->wire()->templates->get((int) $data['template_id']); + + $data['template_id'] = 0; + $data['parent_id'] = 0; + $data['repeaterFields'] = array(); + $data['fieldContexts'] = array(); + + $a = $field->get('repeaterFields'); + if(!is_array($a)) $a = array(); + + foreach($a as $fid) { + $f = $this->wire()->fields->get((int) $fid); + if(!$f) continue; + $data['repeaterFields'][] = $f->name; + $data['fieldContexts'][$f->name] = $template->fieldgroup->getFieldContextArray($f->id); + } + + return $data; + } + + /** + * Convert an array of exported data to a format that will be understood internally + * + * This is the opposite of the exportConfigData() method. + * Most modules can use the default implementation provided here. + * + * #pw-group-configuration + * + * @param Field $field + * @param array $data + * @return array Data as given and modified as needed. Also included is $data[errors], an associative array + * indexed by property name containing errors that occurred during import of config data. + * + */ + public function ___importConfigData(Field $field, array $data) { + + if(!$field->type instanceof FieldtypeRepeater) return $data; + + $fields = $this->wire()->fields; + + $errors = array(); + $repeaterFields = array(); + $saveFieldgroup = false; + $saveFieldgroupContext = false; + $template = $field->id ? $this->getRepeaterTemplate($field) : null; + + if(!empty($data['repeaterFields'])) { + foreach($data['repeaterFields'] as $name) { + $f = $fields->get($name); + if(!$f || !$f instanceof Field) { + $errors[] = "Unable to locate field to add to repeater: $name"; + continue; + } + $repeaterFields[] = $f->id; + } + $data['repeaterFields'] = $repeaterFields; + } + + if($template && !empty($data['fieldContexts'])) { + foreach($data['fieldContexts'] as $name => $contextData) { + $f = $fields->get($name); + if(!$f || !$f instanceof Field) continue; + if($template->fieldgroup->hasField($f)) { + $f = $template->fieldgroup->getFieldContext($f->name); + } + $template->fieldgroup->add($f); + $saveFieldgroup = true; + if(!empty($contextData)) { + $template->fieldgroup->setFieldContextArray($f->id, $contextData); + $saveFieldgroupContext = true; + } + } + } + + if($template) { + if($saveFieldgroupContext) { + $template->fieldgroup->saveContext(); + } + if($saveFieldgroup) { + $template->fieldgroup->save(); + } + } + + unset($data['fieldContexts']); + + $data = parent::___importConfigData($field, $data); + + if(count($errors)) { + $data['errors'] = array_merge($data['errors'], array('repeaterFields' => $errors)); + } + + return $data; + } + + + /*** TOOLS ********************************************************************/ + + /** + * Count or delete old ready pages that are just taking up space * * @param Field $field * @param bool $delete Specify true to delete the old ready pages @@ -1765,7 +1944,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { $items = $this->wire()->pages->find($selector); foreach($items as $item) { try { - if($item->delete()) $cnt++; + if($this->deleteRepeaterPage($item, $field)) $cnt++; } catch(\Exception $e) { $this->error("Error deleting old repeater item $item->path - " . $e->getMessage()); } @@ -1777,133 +1956,136 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { } /** - * Create a cloned copy of Field + * Find unnecessary parent pages that may be deleted * * @param Field $field - * @throws WireException - * + * @param array $options + * @return PageArray + * */ - public function ___cloneField(Field $field) { - throw new WireException($this->className() . " does not currently support field cloning."); - /* - $field = parent::___cloneField($field); - $field->parent_id = null; - $field->template_id = null; - */ + public function findUnnecessaryParents(Field $field, array $options = array()) { + + $defaults = array( + 'useLazyParents' => $this->useLazyParents($field), + 'limit' => 500, + ); + + $options = array_merge($defaults, $options); + $database = $this->wire()->database; + $forFieldParent = $this->getRepeaterParent($field); // for-field-123 + $unnecessaryParents = new PageArray(); + + foreach($forFieldParent->children() as $forPageParent) { + $name = $forPageParent->name; // for-page-456 + if(strpos($name, self::repeaterPageNamePrefix) !== 0) continue; + list(, $forPageId) = explode(self::repeaterPageNamePrefix, $name, 2); + $query = $database->prepare('SELECT COUNT(*) FROM pages WHERE id=:id'); + $query->bindValue(':id', (int) $forPageId, \PDO::PARAM_INT); + $query->execute(); + $exists = (int) $query->fetchColumn(); + $query->closeCursor(); + if(!$exists || ($options['useLazyParents'] && !$forPageParent->numChildren)) { + $unnecessaryParents->add($forPageParent); + if($options['limit'] && $unnecessaryParents->count() >= $options['limit']) break; + } + } + + return $unnecessaryParents; + } + + /** + * Delete a repeater page, removing system statuses first + * + * This is able to delete the following types of pages: + * + * - repeaters/for-field-123/ + * - repeaters/for-field-123/for-page-456/ + * - repeaters/for-field-123/for-page-456/repeater-item/ + * - repeaters/for-field-123/for-page-456/repeater-item/something-else/ + * + * @param Page $page + * @param Field $field Optionally limit to given field or null if not appliable + * @param bool $recursive Descend to children? + * @return int Returns count of pages deleted, or 0 if delete not allowed + * @since 3.0.188 + * + */ + public function deleteRepeaterPage(Page $page, Field $field = null, $recursive = false) { + + static $level = 0; + $numDeleted = 0; + + if(!$page->id) return 0; + + $fieldPageName = self::fieldPageNamePrefix . ($field ? $field->id : ''); + + if(strpos($page->path, '/' . self::repeatersRootPageName . '/') === false) { + $this->bd("Cannot delete $page->path because not in repeaters path", __FUNCTION__); + return 0; + } + + if($field && strpos($page->path, "/$fieldPageName/") === false) { + $this->bd("Cannot delete $page->path because not within /$fieldPageName/", __FUNCTION__); + return 0; + } + + if(strpos($page->name, self::fieldPageNamePrefix) === 0) { + // repeater for-field parent + if($field && $page->name != $fieldPageName) return 0; + + } else if(strpos($page->name, self::repeaterPageNamePrefix) === 0) { + // repeater for-page parent + + } else if(strpos($page->template->name, self::templateNamePrefix) === 0) { + // repeater item + + } else if(strpos($page->path, '/' . self::repeaterPageNamePrefix)) { + // something with /for-page-123 in the path + // child of a repeater item (PageTable or something else?) + + } else { + // some other page, not allowed to delete + $this->bd("Not allowed to delete $page->path", __FUNCTION__); + return 0; + } + + $numChildren = $page->numChildren; + + if($numChildren && !$recursive) { + $this->bd("Cannot delete $page->path because has children", __FUNCTION__); + return 0; + } + + if($numChildren) { + $level++; + foreach($page->children('include=all') as $p) { + $numDeleted += $this->deleteRepeaterPage($p, $field, $recursive); + } + $level--; + } + + // remove system statuses + if($page->hasStatus(Page::statusSystem) || $page->hasStatus(Page::statusSystemID)) { + $page->addStatus(Page::statusSystemOverride); + $page->removeStatus(Page::statusSystem); + $page->removeStatus(Page::statusSystemID); + } + + if($this->wire()->pages->delete($page, $recursive)) $numDeleted++; + + return $numDeleted; } /** - * Export configuration values for external consumption - * - * Use this method to externalize any config values when necessary. - * For example, internal IDs should be converted to GUIDs where possible. - * Most Fieldtype modules can use the default implementation already provided here. - * - * #pw-group-configuration - * - * @param Field $field - * @param array $data - * @return array + * @param mixed $msg + * @param string $func * */ - public function ___exportConfigData(Field $field, array $data) { - - $data = parent::___exportConfigData($field, $data); - - $template = $this->wire()->templates->get((int) $data['template_id']); - - $data['template_id'] = 0; - $data['parent_id'] = 0; - $data['repeaterFields'] = array(); - $data['fieldContexts'] = array(); - - $a = $field->get('repeaterFields'); - if(!is_array($a)) $a = array(); - - foreach($a as $fid) { - $f = $this->wire()->fields->get((int) $fid); - if(!$f) continue; - $data['repeaterFields'][] = $f->name; - $data['fieldContexts'][$f->name] = $template->fieldgroup->getFieldContextArray($f->id); - } - - return $data; + protected function bd($msg, $func = '') { + if(!$this->wire()->config->debug || !class_exists('\\TD')) return; + call_user_func_array('\\TD::barDump', array($msg, $this->className() . "::$func")); } - - /** - * Convert an array of exported data to a format that will be understood internally - * - * This is the opposite of the exportConfigData() method. - * Most modules can use the default implementation provided here. - * - * #pw-group-configuration - * - * @param Field $field - * @param array $data - * @return array Data as given and modified as needed. Also included is $data[errors], an associative array - * indexed by property name containing errors that occurred during import of config data. - * - */ - public function ___importConfigData(Field $field, array $data) { - - if(!$field->type instanceof FieldtypeRepeater) return $data; - - $fields = $this->wire()->fields; - - $errors = array(); - $repeaterFields = array(); - $saveFieldgroup = false; - $saveFieldgroupContext = false; - $template = $field->id ? $this->getRepeaterTemplate($field) : null; - - if(!empty($data['repeaterFields'])) { - foreach($data['repeaterFields'] as $name) { - $f = $fields->get($name); - if(!$f || !$f instanceof Field) { - $errors[] = "Unable to locate field to add to repeater: $name"; - continue; - } - $repeaterFields[] = $f->id; - } - $data['repeaterFields'] = $repeaterFields; - } - - if($template && !empty($data['fieldContexts'])) { - foreach($data['fieldContexts'] as $name => $contextData) { - $f = $fields->get($name); - if(!$f || !$f instanceof Field) continue; - if($template->fieldgroup->hasField($f)) { - $f = $template->fieldgroup->getFieldContext($f->name); - } - $template->fieldgroup->add($f); - $saveFieldgroup = true; - if(!empty($contextData)) { - $template->fieldgroup->setFieldContextArray($f->id, $contextData); - $saveFieldgroupContext = true; - } - } - } - - if($template) { - if($saveFieldgroupContext) { - $template->fieldgroup->saveContext(); - } - if($saveFieldgroup) { - $template->fieldgroup->save(); - } - } - - unset($data['fieldContexts']); - - $data = parent::___importConfigData($field, $data); - - if(count($errors)) { - $data['errors'] = array_merge($data['errors'], array('repeaterFields' => $errors)); - } - - return $data; - } - + /*** CONFIG **************************************************************************/ /** @@ -1912,6 +2094,19 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { */ protected $repeaterConfigHelper = null; + /** + * Use lazy parents mode? + * + * @param RepeaterField|Field $field + * @return bool + * + */ + public function useLazyParents(Field $field) { + /** @var FieldtypeRepeater $fieldtype */ + if(strpos($this->className(), 'Fieldset')) return false; + return ((int) $field->get('lazyParents')) > 0; + } + /** * @param Field $field * @return FieldtypeRepeaterConfigHelper @@ -1986,9 +2181,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { */ public function ___getConfigAdvancedInputfields(Field $field) { $inputfields = parent::___getConfigAdvancedInputfields($field); - // these two are potential troublemakers when it comes to repeaters - $inputfields->remove($inputfields->get('autojoin')); - $inputfields->remove($inputfields->get('global')); + $this->getRepeaterConfigHelper($field)->getConfigAdvancedInputfields($inputfields); return $inputfields; } @@ -2009,7 +2202,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { $page = $pages->newPage('admin'); $page->parent = $adminRoot; $page->status = Page::statusHidden | Page::statusLocked | Page::statusSystemID; - $page->name = 'repeaters'; + $page->name = self::repeatersRootPageName; $page->title = 'Repeaters'; $page->sort = $adminRoot->numChildren; $page->save(); @@ -2030,9 +2223,9 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule { // don't delete repeaters page unless actually for FieldtypeRepeater if($this->className() != 'FieldtypeRepeater') return; - $page = $pages->get($this->repeatersRootPageID); + $page = $this->getRepeatersRootPage(); - if($page->id && $page->name === 'repeaters' && $page->template->name === 'admin') { + if($page->id && $page->name === self::repeatersRootPageName && $page->template->name === 'admin') { $page->addStatus(Page::statusSystemOverride); $page->removeStatus(Page::statusSystem); $page->removeStatus(Page::statusSystemID); diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/RepeaterField.php b/wire/modules/Fieldtype/FieldtypeRepeater/RepeaterField.php index 2bc448c0..67112d50 100644 --- a/wire/modules/Fieldtype/FieldtypeRepeater/RepeaterField.php +++ b/wire/modules/Fieldtype/FieldtypeRepeater/RepeaterField.php @@ -1,5 +1,26 @@ fields API var) @@ -36,8 +36,8 @@ class RepeaterPageArray extends PageArray { * */ public function __construct(Page $parent, Field $field) { - $this->setParent($parent); - $this->setField($field); + $this->setForPage($parent); + $this->setForField($field); parent::__construct(); } @@ -45,20 +45,49 @@ class RepeaterPageArray extends PageArray { * Set parent * * @param Page $parent + * @deprecated use setForPage() instead * */ - public function setParent(Page $parent) { $this->parent = $parent; } + public function setParent(Page $parent) { $this->forPage = $parent; } + + /** + * Set for page + * + * @param Page $forPage + * @since 3.0.188 + * + */ + public function setForPage(Page $forPage) { $this->forPage = $forPage; } /** * Get parent * * @return Page + * @deprecated use getForPage() insteada * */ - public function getParent() { return $this->parent; } + public function getParent() { return $this->forPage; } + /** + * Get for page + * + * @return Page + * @since 3.0.188 + * + */ + public function getForPage() { return $this->forPage; } + /** * Set field + * + * @param Field $field + * @since 3.0.188 + * + */ + public function setForField(Field $field) { $this->field = $field; } + + /** + * Set field (alias of setForField) * * @param Field $field * @@ -69,12 +98,25 @@ class RepeaterPageArray extends PageArray { * Get field * * @return Field + * @since 3.0.188 * */ + public function getForField() { + return $this->field; + } + + /** + * Get field (alias of getForField) + * + * @return Field + * + */ public function getField() { return $this->field; } /** * Alias of getNewItem() kept for backwards compatibility + * + * @return Page * */ public function getNew() { return $this->getNewItem(); } @@ -98,7 +140,7 @@ class RepeaterPageArray extends PageArray { /** @var FieldtypeRepeater $fieldtype */ $fieldtype = $this->field->type; $page = null; - $of = $this->parent->of(false); + $of = $this->forPage->of(false); // first try to get a ready item, if available foreach($this as $item) { @@ -110,7 +152,7 @@ class RepeaterPageArray extends PageArray { if(is_null($page)) { // no ready item available, get a new one - $page = $fieldtype->getBlankRepeaterPage($this->parent, $this->field); + $page = $fieldtype->getBlankRepeaterPage($this->forPage, $this->field); $this->add($page); } else { $this->trackChange('add'); @@ -121,7 +163,7 @@ class RepeaterPageArray extends PageArray { $page->removeStatus(Page::statusHidden); $page->sort = $this->count(); - if($of) $this->parent->of(true); + if($of) $this->forPage->of(true); return $page; } @@ -136,7 +178,7 @@ class RepeaterPageArray extends PageArray { */ public function makeNew() { $class = get_class($this); - $newArray = $this->wire(new $class($this->parent, $this->field)); + $newArray = $this->wire(new $class($this->forPage, $this->field)); return $newArray; } @@ -166,6 +208,18 @@ class RepeaterPageArray extends PageArray { parent::trackRemove($item, $key); } + /** + * Get direct property + * + * @param int|string $key + * @return bool|mixed|Page|Wire|WireData + * + */ + public function __get($key) { + if($key === 'parent') return $this->getForPage(); + return parent::__get($key); + } + /** * Debug info * @@ -176,8 +230,8 @@ class RepeaterPageArray extends PageArray { $info = array( 'field' => $this->field ? $this->field->debugInfoSmall() : '', ); - if($this->parent && $this->parent->id) { - $info['parent'] = $this->parent->debugInfoSmall(); + if($this->forPage && $this->forPage->id) { + $info['forPage'] = $this->forPage->debugInfoSmall(); } $info = array_merge($info, parent::__debugInfo()); return $info; diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/config.php b/wire/modules/Fieldtype/FieldtypeRepeater/config.php index 62816f8a..6fb80132 100644 --- a/wire/modules/Fieldtype/FieldtypeRepeater/config.php +++ b/wire/modules/Fieldtype/FieldtypeRepeater/config.php @@ -116,7 +116,7 @@ class FieldtypeRepeaterConfigHelper extends Wire { $select->addOption($f->id, $f->name, $attrs); } - foreach($this->wire('fields') as $f) { + foreach($this->wire()->fields as $f) { if($template->fieldgroup->has($f)) continue; if($f->name == $this->field->name) continue; if(($f->flags & Field::flagPermanent) && !$this->wire('config')->advanced) continue; @@ -152,6 +152,7 @@ class FieldtypeRepeaterConfigHelper extends Wire { $f->description .= ' ' . $this->_('To use multiple fields, or to specify your own format string, surround field names in {brackets}.'); $f->description .= ' ' . $this->_('To include a repeater index number with each label, add "#n" somewhere in your format string.'); $f->notes = $this->_('Example: #n: {title}'); + $f->themeOffset = 1; $inputfields->add($f); // ------------------------------------------------- @@ -171,7 +172,6 @@ class FieldtypeRepeaterConfigHelper extends Wire { $f->attr('name', 'repeaterDepth'); $f->attr('value', $value > 0 ? $value : ''); $f->label = $this->_('Item depth'); - $f->collapsed = Inputfield::collapsedBlank; $f->description = $this->_('To support items with depth, enter the max allowed depth, or leave blank to disable.'); $f->description .= ' ' . $this->_('When editing a repeater, you can change item depth by clicking the repeater item drag arrows and dragging the item right or left.'); $f->notes = $this->_('Depths are zero-based, meaning a depth of 3 allows depths 0, 1, 2 and 3.'); @@ -187,6 +187,7 @@ class FieldtypeRepeaterConfigHelper extends Wire { $this->_('This setting makes the admin page editor treat item depth as a parent/child relationship.') . ' ' . $this->_('This means that moving/sorting an item includes child items too.') . ' ' . $this->_('It also prevents a child item from being dragged to have a depth that exceeds its parent by more than 1.'); + $f->notes = $this->_('“Yes” recommended.'); $f->val((int) $field->get('familyFriendly')); $f->columnWidth = 50; $fs->add($f); @@ -312,21 +313,11 @@ class FieldtypeRepeaterConfigHelper extends Wire { $fs->add($f); // ------------------------------------------------- - - /** @var FieldtypeRepeater $fieldtype */ - $fieldtype = $this->field->type; - $numOldReady = $fieldtype->countOldReadyPages($field); - if($numOldReady) { - // @todo: should we just do this automatically? - $f = $modules->get('InputfieldCheckbox'); - $f->attr('name', '_deleteOldReady'); - $f->label = $this->_('Delete old/unused repeater items?'); - $f->description = sprintf($this->_('There are **%d** old/unused repeater item(s), check this box to delete them.'), $numOldReady); - $f->notes = $this->_('A repeater item is considered old if it is at least 3 days and not yet been populated or published.'); - $f->icon = 'warning'; - $inputfields->add($f); + + if(strpos($this->field->type->className(), 'Fieldset') === false) { + $this->getConfigInputfieldsStorage($inputfields); } - + // ------------------------------------------------- /* TBA @@ -345,6 +336,114 @@ class FieldtypeRepeaterConfigHelper extends Wire { return $inputfields; } + /** + * @param InputfieldWrapper $inputfields + * @return InputfieldFieldset + * + */ + protected function getConfigInputfieldsStorage(InputfieldWrapper $inputfields) { + + $modules = $this->wire()->modules; + $session = $this->wire()->session; + $input = $this->wire()->input; + $fieldtype = $this->field->type; /** @var FieldtypeRepeater $fieldtype */ + $limit = 1000; + + /** @var InputfieldFieldset $fs */ + $fs = $modules->get('InputfieldFieldset'); + $fs->label = $this->_('Repeater storage'); + $fs->attr('name', '_repeaterStorageSettings'); + $fs->collapsed = Inputfield::collapsedYes; + $fs->icon = 'database'; + $fs->themeOffset = 1; + + /** @var InputfieldCheckbox $f */ + $f = $modules->get('InputfieldCheckbox'); + $f->attr('name', 'lazyParents'); + $f->label = $this->_('Use fewer pages for storage?'); + $f->icon = 'flask'; + $f->description = + $this->_('When checked, repeater page parents will not be created until at least one child repeater item exists.') . ' ' . + $this->_('In addition, repeater page parents with no repeater items will be removed when appropriate.'); + $f->notes = + $this->_('Currently an experimental option for testing (lazyParents), but will later become default.'); + if($this->field->get('lazyParents')) $f->attr('checked', 'checked'); + $fs->add($f); + + /** @var InputfieldCheckbox $f */ + $f = $modules->get('InputfieldCheckbox'); + $f->icon = 'trash'; + $findName = '_findUnnecessaryPages'; + $deleteName = '_deleteUnnecessaryPages'; + + if($session->getFor($this, $findName)) { + $fs->collapsed = Inputfield::collapsedNo; + $inputfields->prepend($fs); + set_time_limit(600); + $parents = $fieldtype->findUnnecessaryParents($this->field, array('limit' => $limit)); + $qty = $parents->count(); + if($qty) { + $f->attr('name', $deleteName); + $f->label = sprintf($this->_('Delete %d unnecessary pages?'), $parents->count()); + $paths = array(); + if($qty < 100) { + foreach($parents as $parent) { + $note = $parent->numChildren ? '(orphan parent)' : '(0 repeater items)'; + $paths[] = $parent->path() . " $note"; + } + $f->description = $this->_('Found the following unnecessary pages:') . "\n" . implode("\n", $paths); + } else { + $f->description = sprintf($this->_('Found %d unnecessary repeater parents that either had 0 repeater items or had no owning page.'), $qty); + } + $f->notes = $this->_('Always backup before performing mass deletions.'); + $this->warning(sprintf($this->_('Found %d unnecessary repeater parent pages'), $qty)); + $fs->prepend($f); + } else { + $this->warning($this->_('No unnecessary pages found')); + } + $session->removeFor($this, $findName); + + } else if($input->post($findName)) { + $session->setFor($this, $findName, 1); + + } else if($input->post($deleteName)) { + set_time_limit(600); + $parents = $fieldtype->findUnnecessaryParents($this->field, array('limit' => $limit)); + $numDeleted = 0; + if($parents->count() >= $limit) { + $this->warning(sprintf($this->_('Max of %d items per request reached, you will want to run this again.'), $limit)); + } + foreach($parents as $parent) { + $numDeleted += $fieldtype->deleteRepeaterPage($parent, $this->field, true); + } + $this->warning(sprintf($this->_('Deleted %d unnecessary pages'), $numDeleted)); + + } else { + $f->attr('name', $findName); + $f->label = $this->_('Find and optionally delete unnecessary pages?'); + $fs->add($f); + $inputfields->add($fs); + } + + // ------------------------------------------------- + + if($input->requestMethod('GET')) { + $numOldReady = $fieldtype->countOldReadyPages($this->field); + if($numOldReady) { + // @todo: should we just do this automatically? + $f = $modules->get('InputfieldCheckbox'); + $f->attr('name', '_deleteOldReady'); + $f->label = $this->_('Delete old/unused repeater items?'); + $f->description = sprintf($this->_('There are **%d** old/unused repeater item(s), check this box to delete them.'), $numOldReady); + $f->notes = $this->_('A repeater item is considered old if it is at least 3 days and not yet been populated or published.'); + $f->icon = 'warning'; + $fs->add($f); + } + } + + return $fs; + } + /** * Helper to getConfigInputfields, handles adding and removing of repeater fields * @@ -395,5 +494,16 @@ class FieldtypeRepeaterConfigHelper extends Wire { */ } + /** + * Advanced config + * + * @param InputfieldWrapper $inputfields + * + */ + public function getConfigAdvancedInputfields(InputfieldWrapper $inputfields) { + // these two are potential troublemakers when it comes to repeaters + $inputfields->remove($inputfields->get('autojoin')); + $inputfields->remove($inputfields->get('global')); + } } \ No newline at end of file