From d82194816f0a44df8dee567642b0b3a5cc3c7565 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 15 Dec 2023 13:34:19 -0500 Subject: [PATCH] Move version support in FieldtypeRepeater to separate class, plus add support for nested repeater versions --- .../FieldtypeRepeater.module | 170 +++-------- .../FieldtypeRepeaterVersions.php | 286 ++++++++++++++++++ 2 files changed, 328 insertions(+), 128 deletions(-) create mode 100644 wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeaterVersions.php diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module b/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module index 97d63b58..d83d8b37 100644 --- a/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module +++ b/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeater.module @@ -39,7 +39,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule, Fieldty ); } - const devMode = false; // display verbose TD messages + const devMode = true; // display verbose TD messages const templateNamePrefix = 'repeater_'; const fieldPageNamePrefix = 'for-field-'; @@ -1078,7 +1078,7 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule, Fieldty $parent = $this->wire()->pages->newPage($repeaterParent->template); $parent->parent = $repeaterParent; $parent->name = $parentName; - $parent->title = $page->name . ($version ? " v$version" : ""); + $parent->title = $page->name . ($version ? " > v$version" : ""); $parent->addStatus(Page::statusSystem); // exit early if a field is in the process of being deleted @@ -2202,15 +2202,42 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule, Fieldty return $this->wire(new FieldtypeRepeaterPorter()); } + /************************************************************************* + * VERSION SUPPORT + */ + + protected $versions = null; + /** - * @param Page $page - * @return int + * #pw-internal + * + * @return FieldtypeRepeaterVersions + * @since 3.0.132 + * * */ - protected function getPageVersionNum(Page $page) { - return (int) ((string) $page->get('_repeater_version|_version')); + public function versions() { + if($this->versions === null) { + require_once(__DIR__ . '/FieldtypeRepeaterVersions.php'); + $this->versions = new FieldtypeRepeaterVersions($this); + } + return $this->versions; } - + + /** + * Get page version number + * + * #pw-internal + * + * @param Page $page + * @return int + * @since 3.0.132 + * + */ + public function getPageVersionNum(Page $page) { + return $this->versions()->getPageVersionNum($page); + } + /** * Get the value for given page, field and version * @@ -2220,17 +2247,11 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule, Fieldty * @param Field $field * @param int $version * @return mixed + * @since 3.0.132 * */ public function getPageFieldVersion(Page $page, Field $field, $version) { - $page->setQuietly('_repeater_version', $version); - /** @var PagesVersions $pagesVersions */ - $pagesVersions = $this->wire('pagesVersions'); - $value = $pagesVersions ? $pagesVersions->getPageFieldVersion($page, $field, $version) : null; - // $value['parent_id'] = $this->getRepeaterPageParent($page, $field); - // value = $this->wakeupValue($page, $field, $value); - // $page->__unset('_repeater_version'); - return $value; + return $this->versions()->getPageFieldVersion($page, $field, $version); } /** @@ -2242,37 +2263,11 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule, Fieldty * @param Field $field * @param int $version * @return bool + * @since 3.0.132 * */ public function savePageFieldVersion(Page $page, Field $field, $version) { - - $value = $page->get($field->name); /** @var RepeaterPageArray $value */ - $page = clone $page; - $page->setQuietly('_repeater_version', $version); - - $parent = $this->getRepeaterPageParent($page, $field, false); - - if(!$parent || !$parent->id) { - // setup new version - $pages = $this->wire()->pages; - $parent = $this->getRepeaterPageParent($page, $field, true); // create - $cloneOptions = array('uncacheAll' => false); - $valueClass = $value->className(true); - $valueCopy = new $valueClass($parent, $field); - $this->wire($valueCopy); - foreach($value as $item) { - /** @var RepeaterPage $item */ - $itemCopy = $pages->clone($item, $parent, false, $cloneOptions); - $this->bd("Cloned repeater item $item->name to parent $parent->name"); - $valueCopy->add($itemCopy); - } - $valueCopy->resetTrackChanges(); - $page->set($field->name, $valueCopy); - } - - $result = $this->savePageField($page, $field); - - return $result; + return $this->versions()->savePageFieldVersion($page, $field, $version); } /** @@ -2284,76 +2279,11 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule, Fieldty * @param Field $field * @param int $version * @return bool + * @since 3.0.132 * */ public function restorePageFieldVersion(Page $page, Field $field, $version) { - if(!$version) return false; - - $pages = $this->wire()->pages; - - /** @var PagesVersions $pagesVersions */ - $pagesVersions = $this->wire('pagesVersions'); - if(!$pagesVersions) return false; - - $pageVersionNum = $this->getPageVersionNum($page); - - if($pageVersionNum) { - $livePage = $pages->getFresh($page->id); - if($pageVersionNum == $version) { - $versionPage = $page; - } else { - $versionPage = $pagesVersions->getPageVersion($page, $version); - } - } else { - $versionPage = $pagesVersions->getPageVersion($page, $version); - $livePage = $page; - } - - $versionRepeaterParent = $this->getRepeaterPageParent($versionPage, $field, false); - $liveRepeaterParent = $this->getRepeaterPageParent($livePage, $field, false); - - if(!$versionRepeaterParent) { - $this->error( - "Version repeater parent not found for " . - "page $page field $field version $version" - ); - return false; - } - - if($liveRepeaterParent->id) { - $this->deleteRepeaterPage($liveRepeaterParent, null, true); - } - - list($name,) = explode("-v$version", $versionRepeaterParent->name, 2); - $versionRepeaterParent->addStatus(Page::statusSystemOverride); - $versionRepeaterParent->removeStatus(Page::statusSystem); - $versionRepeaterParent->name = $name; - $versionRepeaterParent->save(); - $pages->editor()->addStatus($versionRepeaterParent, Page::statusSystem); - - $itemIDs = array(); - - foreach($versionRepeaterParent->children('include=all') as $item) { - /** @var RepeaterPage $item */ - if($item->isHidden() || $item->isUnpublished()) continue; - $itemIDs[] = $item->id; - } - - $table = $field->getTable(); - $sql = - "UPDATE $table SET data=:data, count=:count, parent_id=:parent_id " . - "WHERE pages_id=:pages_id"; - - $query = $this->wire()->database->prepare($sql); - $query->bindValue(':data', implode(',', $itemIDs)); - $query->bindValue(':count', count($itemIDs)); - $query->bindValue(':parent_id', $versionRepeaterParent->id, \PDO::PARAM_INT); - $query->bindValue(':pages_id', $versionPage->id, \PDO::PARAM_INT); - $query->execute(); - - $page->offsetUnset($field->name); - - return true; + return $this->versions()->restorePageFieldVersion($page, $field, $version); } /** @@ -2365,27 +2295,11 @@ class FieldtypeRepeater extends Fieldtype implements ConfigurableModule, Fieldty * @param Field $field * @param int $version * @return bool + * @since 3.0.132 * */ public function deletePageFieldVersion(Page $page, Field $field, $version) { - if(!$version) return false; - - /** @var PagesVersions $pagesVersions */ - $pagesVersions = $this->wire('pagesVersions'); - if(!$pagesVersions) return false; - - if($this->getPageVersionNum($page) == $version) { - $versionPage = $page; - } else { - $versionPage = $pagesVersions->getPageVersion($page, $version); - } - - if(!$versionPage->id) return false; - - $versionRepeaterParent = $this->getRepeaterPageParent($versionPage, $field, false); - if(!$versionRepeaterParent->id) return false; - - return $this->deleteRepeaterPage($versionRepeaterParent, null, true) > 0; + return $this->versions()->deletePageFieldVersion($page, $field, $version); } } diff --git a/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeaterVersions.php b/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeaterVersions.php new file mode 100644 index 00000000..b16e277e --- /dev/null +++ b/wire/modules/Fieldtype/FieldtypeRepeater/FieldtypeRepeaterVersions.php @@ -0,0 +1,286 @@ +fieldtype = $fieldtype; + parent::__construct(); + } + + /** + * @param Page $page + * @return int + * + */ + public function getPageVersionNum(Page $page) { + return (int) ((string) $page->get('_repeater_version|_version')); + } + + /** + * Does given page or page+field have nested repeater fields? + * + * #pw-internal + * + * @param Page|null $page + * @param Field|null $field + * @param bool $verify Specify true to also verify actual nested repeater items are present + * @return bool + * @since 3.0.132 + * + */ + public function hasNestedRepeaterFields($page, Field $field = null, $verify = false) { + $has = false; + + if($field === null) { + if(!$page instanceof Page) return false; + foreach($page->fieldgroup as $field) { + /** @var Field $field */ + if(!$field->type instanceof FieldtypeRepeater) continue; + if($page instanceof RepeaterPage) { + $has = true; + } else { + $has = $this->hasNestedRepeaterFields($page, $field, $verify); + } + if($has) break; + } + return $has; + } + + $template = $this->fieldtype->_getRepeaterTemplate($field); + $repeaterNames = array(); + + foreach($template->fieldgroup as $f) { + if($f->type instanceof FieldtypeRepeater) { + $repeaterNames[] = $f->name; + } + } + + if($verify && count($repeaterNames) && $page) { + // page has repeater fields + $items = $this->getRepeaterPageArray($page, $field, $page->get($field->name)); + foreach($items as $item) { + foreach($repeaterNames as $name) { + $nestedItems = $item->get($name); + if($nestedItems && $nestedItems->count()) $has = true; + if($has) break; + } + if($has) break; + } + } else { + $has = count($repeaterNames) > 0; + } + + return $has; + } + + /** + * Get the value for given page, field and version + * + * #pw-internal for FieldtypeDoesVersions interface + * + * @param Page $page + * @param Field $field + * @param int $version + * @return mixed + * + */ + public function getPageFieldVersion(Page $page, Field $field, $version) { + $page->setQuietly('_repeater_version', $version); + /** @var PagesVersions $pagesVersions */ + $pagesVersions = $this->wire('pagesVersions'); + $value = $pagesVersions ? $pagesVersions->getPageFieldVersion($page, $field, $version) : null; + // $value['parent_id'] = $this->getRepeaterPageParent($page, $field); + // value = $this->wakeupValue($page, $field, $value); + // $page->__unset('_repeater_version'); + return $value; + } + + /** + * Save version of given page field + * + * #pw-internal for FieldtypeDoesVersions interface + * + * @param Page $page + * @param Field $field + * @param int $version + * @return bool + * + */ + public function savePageFieldVersion(Page $page, Field $field, $version) { + + $value = $page->get($field->name); /** @var RepeaterPageArray|FieldsetPage $value */ + $value = $this->getRepeaterPageArray($page, $field, $value); + + $page = clone $page; + $page->setQuietly('_repeater_version', $version); + + $parent = $this->fieldtype->getRepeaterPageParent($page, $field, false); + + if(!$parent || !$parent->id) { + // setup new version + $pages = $this->wire()->pages; + $parent = $this->fieldtype->getRepeaterPageParent($page, $field, true); // create + $cloneOptions = array('uncacheAll' => false); + $valueClass = $value->className(true); + $valueCopy = new $valueClass($parent, $field); + $this->wire($valueCopy); + foreach($value as $item) { + /** @var RepeaterPage $item */ + $itemCopy = $pages->clone($item, $parent, false, $cloneOptions); + $valueCopy->add($itemCopy); + } + $valueCopy->resetTrackChanges(); + $page->set($field->name, $valueCopy); + } + + $result = $this->fieldtype->savePageField($page, $field); + + return $result; + } + + /** + * Restore version of given page field + * + * #pw-internal for FieldtypeDoesVersions interface + * + * @param Page $page + * @param Field $field + * @param int $version + * @return bool + * + */ + public function restorePageFieldVersion(Page $page, Field $field, $version) { + if(!$version) return false; + + $pages = $this->wire()->pages; + + /** @var PagesVersions $pagesVersions */ + $pagesVersions = $this->wire('pagesVersions'); + if(!$pagesVersions) return false; + + $pageVersionNum = $this->getPageVersionNum($page); + + if($pageVersionNum) { + $livePage = $pages->getFresh($page->id); + if($pageVersionNum == $version) { + $versionPage = $page; + } else { + $versionPage = $pagesVersions->getPageVersion($page, $version); + } + } else { + $versionPage = $pagesVersions->getPageVersion($page, $version); + $livePage = $page; + } + + $versionRepeaterParent = $this->fieldtype->getRepeaterPageParent($versionPage, $field, false); + $liveRepeaterParent = $this->fieldtype->getRepeaterPageParent($livePage, $field, false); + + if(!$versionRepeaterParent || !$versionRepeaterParent->id) { + $this->error( + "Version repeater parent not found for page:$page field:$field v:$version" + ); + return false; + } + + $itemIDs = array(); + + if($liveRepeaterParent->id) { + $this->fieldtype->deleteRepeaterPage($liveRepeaterParent, null, true); + } + + list($name,) = explode("-v$version", $versionRepeaterParent->name, 2); + $versionRepeaterParent->addStatus(Page::statusSystemOverride); + $versionRepeaterParent->removeStatus(Page::statusSystem); + $versionRepeaterParent->name = $name; + $versionRepeaterParent->title = preg_replace('/> v\d+/', '> 0', $versionRepeaterParent->title); + $versionRepeaterParent->save(); + $pages->editor()->addStatus($versionRepeaterParent, Page::statusSystem); + + foreach($versionRepeaterParent->children('include=all') as $item) { + /** @var RepeaterPage $item */ + if($item->isHidden() && $item->isUnpublished()) continue; + $itemIDs[] = $item->id; + } + + $table = $field->getTable(); + $sql = + "UPDATE $table SET data=:data, count=:count, parent_id=:parent_id " . + "WHERE pages_id=:pages_id"; + + $query = $this->wire()->database->prepare($sql); + $query->bindValue(':data', implode(',', $itemIDs)); + $query->bindValue(':count', count($itemIDs)); + $query->bindValue(':parent_id', $versionRepeaterParent->id, \PDO::PARAM_INT); + $query->bindValue(':pages_id', $versionPage->id, \PDO::PARAM_INT); + $query->execute(); + + $page->offsetUnset($field->name); + + return true; + } + + /** + * Delete version + * + * #pw-internal for FieldtypeDoesVersions interface + * + * @param Page $page + * @param Field $field + * @param int $version + * @return bool + * + */ + public function deletePageFieldVersion(Page $page, Field $field, $version) { + if(!$version) return false; + + /** @var PagesVersions $pagesVersions */ + $pagesVersions = $this->wire('pagesVersions'); + if(!$pagesVersions) return false; + + if($this->getPageVersionNum($page) == $version) { + $versionPage = $page; + } else { + $versionPage = $pagesVersions->getPageVersion($page, $version); + } + + if(!$versionPage->id) return false; + + $versionRepeaterParent = $this->fieldtype->getRepeaterPageParent($versionPage, $field, false); + if(!$versionRepeaterParent->id) return false; + + return $this->fieldtype->deleteRepeaterPage($versionRepeaterParent, null, true) > 0; + } + + /** + * Normalize a value to a RepeaterPageArray + * + * @param Page $page + * @param Field $field + * @param RepeaterPage|RepeaterPageArray $value + * @return PageArray + * + */ + protected function getRepeaterPageArray(Page $page, Field $field, $value) { + $fieldtype = $field->type; /** @var FieldtypeRepeater $fieldtype */ + if($value instanceof PageArray) { + // great + } else if($value instanceof RepeaterPage) { + $item = $value; + $value = $fieldtype->getBlankValue($page, $field); + $value->add($item); + } else { + $value = $fieldtype->getBlankValue($page, $field); + } + return $value; + } + +}