Add GUI for menuItem package installation plugin
authorMatthias Schmidt <gravatronics@live.com>
Sat, 7 Jul 2018 15:27:50 +0000 (17:27 +0200)
committerMatthias Schmidt <gravatronics@live.com>
Sat, 7 Jul 2018 15:27:50 +0000 (17:27 +0200)
See #2545

wcfsetup/install/files/lib/system/package/plugin/MenuItemPackageInstallationPlugin.class.php
wcfsetup/install/lang/en.xml

index 03adc3abd420bd8f4089f747fbe2a42dd8551571..8eb760da83989fdba6c48d556af339a9b4a26885 100644 (file)
@@ -3,20 +3,42 @@ declare(strict_types=1);
 namespace wcf\system\package\plugin;
 use wcf\data\menu\item\MenuItem;
 use wcf\data\menu\item\MenuItemEditor;
-use wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin;
+use wcf\data\menu\item\MenuItemList;
+use wcf\data\menu\item\MenuItemNode;
+use wcf\data\menu\Menu;
+use wcf\data\menu\MenuList;
+use wcf\data\page\PageNode;
+use wcf\data\page\PageNodeTree;
+use wcf\system\devtools\pip\IDevtoolsPipEntryList;
+use wcf\system\devtools\pip\IGuiPackageInstallationPlugin;
+use wcf\system\devtools\pip\TXmlGuiPackageInstallationPlugin;
 use wcf\system\exception\SystemException;
+use wcf\system\form\builder\container\FormContainer;
+use wcf\system\form\builder\field\dependency\ValueFormFieldDependency;
+use wcf\system\form\builder\field\IntegerFormField;
+use wcf\system\form\builder\field\IsDisabledFormField;
+use wcf\system\form\builder\field\RadioButtonFormField;
+use wcf\system\form\builder\field\SingleSelectionFormField;
+use wcf\system\form\builder\field\TextFormField;
+use wcf\system\form\builder\field\TitleFormField;
+use wcf\system\form\builder\field\validation\FormFieldValidationError;
+use wcf\system\form\builder\field\validation\FormFieldValidator;
+use wcf\system\form\builder\IFormDocument;
+use wcf\system\language\LanguageFactory;
 use wcf\system\WCF;
 
 /**
  * Installs, updates and deletes menu items.
  * 
- * @author     Alexander Ebert
+ * @author     Alexander Ebert, Matthias Schmidt
  * @copyright  2001-2018 WoltLab GmbH
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package    WoltLabSuite\Core\Acp\Package\Plugin
  * @since      3.0
  */
-class MenuItemPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IIdempotentPackageInstallationPlugin {
+class MenuItemPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements IGuiPackageInstallationPlugin {
+       use TXmlGuiPackageInstallationPlugin;
+       
        /**
         * @inheritDoc
         */
@@ -75,17 +97,11 @@ class MenuItemPackageInstallationPlugin extends AbstractXMLPackageInstallationPl
        protected function prepareImport(array $data) {
                $menuID = null;
                if (!empty($data['elements']['menu'])) {
-                       $sql = "SELECT  menuID
-                               FROM    wcf".WCF_N."_menu
-                               WHERE   identifier = ?";
-                       $statement = WCF::getDB()->prepareStatement($sql, 1);
-                       $statement->execute([$data['elements']['menu']]);
-                       $row = $statement->fetchSingleRow();
-                       if ($row === false) {
+                       $menuID = $this->getMenuID($data['elements']['menu']);
+                       
+                       if ($menuID === null) {
                                throw new SystemException("Unable to find menu '" . $data['elements']['menu'] . "' for menu item '" . $data['attributes']['identifier'] . "'");
                        }
-                       
-                       $menuID = $row['menuID'];
                }
                
                $parentItemID = null;
@@ -116,17 +132,11 @@ class MenuItemPackageInstallationPlugin extends AbstractXMLPackageInstallationPl
                
                $pageID = null;
                if (!empty($data['elements']['page'])) {
-                       $sql = "SELECT  pageID
-                               FROM    wcf".WCF_N."_page
-                               WHERE   identifier = ?";
-                       $statement = WCF::getDB()->prepareStatement($sql, 1);
-                       $statement->execute([$data['elements']['page']]);
-                       $row = $statement->fetchSingleRow();
-                       if ($row === false) {
+                       $pageID = $this->getPageID($data['elements']['page']);
+                       
+                       if ($pageID === null) {
                                throw new SystemException("Unable to find page '" . $data['elements']['page'] . "' for menu item '" . $data['attributes']['identifier'] . "'");
                        }
-                       
-                       $pageID = $row['pageID'];
                }
                
                $externalURL = (!empty($data['elements']['externalURL'])) ? $data['elements']['externalURL'] : '';
@@ -150,6 +160,40 @@ class MenuItemPackageInstallationPlugin extends AbstractXMLPackageInstallationPl
                ];
        }
        
+       /**
+        * Returns the id of the menu with the given identifier. If no such menu
+        * exists, `null` is returned.
+        * 
+        * @param       string  $identifier
+        * @return      null|int
+        */
+       protected function getMenuID(string $identifier) {
+               $sql = "SELECT  menuID
+                       FROM    wcf" . WCF_N . "_menu
+                       WHERE   identifier = ?";
+               $statement = WCF::getDB()->prepareStatement($sql, 1);
+               $statement->execute([$identifier]);
+               
+               return $statement->fetchSingleColumn();
+       }
+       
+       /**
+        * Returns the id of the page with the given identifier. If no such page
+        * exists, `null` is returned.
+        *
+        * @param       string  $identifier
+        * @return      null|int
+        */
+       protected function getPageID(string $identifier) {
+               $sql = "SELECT  pageID
+                       FROM    wcf" . WCF_N . "_page
+                       WHERE   identifier = ?";
+               $statement = WCF::getDB()->prepareStatement($sql, 1);
+               $statement->execute([$identifier]);
+               
+               return $statement->fetchSingleColumn();
+       }
+       
        /**
         * @inheritDoc
         */
@@ -206,8 +250,361 @@ class MenuItemPackageInstallationPlugin extends AbstractXMLPackageInstallationPl
        
        /**
         * @inheritDoc
+        * @since       3.1
         */
        public static function getSyncDependencies() {
                return ['language', 'menu', 'page'];
        }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function addFormFields(IFormDocument $form) {
+               $menuList = new MenuList();
+               $menuList->readObjects();
+               
+               /** @var FormContainer $dataContainer */
+               $dataContainer = $form->getNodeById('data');
+               
+               $dataContainer->appendChildren([
+                       TextFormField::create('identifier')
+                               ->label('wcf.acp.pip.menuItem.identifier')
+                               ->description('wcf.acp.pip.menuItem.identifier.description')
+                               ->required()
+                               ->addValidator(ObjectTypePackageInstallationPlugin::getObjectTypeAlikeValueValidator(
+                                       'wcf.acp.pip.menuItem.identifier',
+                                       4
+                               ))
+                               ->addValidator(new FormFieldValidator('uniqueness', function(TextFormField $formField) {
+                                       if (
+                                               $formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE ||
+                                               $this->editedEntry->getAttribute('identifier') !== $formField->getValue()
+                                       ) {
+                                               $menuItemList = new MenuItemList();
+                                               $menuItemList->getConditionBuilder()->add('identifier = ?', [$formField->getValue()]);
+                                               
+                                               if ($menuItemList->countObjects() > 0) {
+                                                       $formField->addValidationError(
+                                                               new FormFieldValidationError(
+                                                                       'notUnique',
+                                                                       'wcf.acp.pip.menuItem.identifier.error.notUnique'
+                                                               )
+                                                       );
+                                               }
+                                       }
+                               })),
+                       
+                       TitleFormField::create()
+                               ->required()
+                               ->i18n()
+                               ->i18nRequired()
+                               ->languageItemPattern('__NONE__'),
+                       
+                       SingleSelectionFormField::create('menu')
+                               ->label('wcf.acp.pip.menuItem.menu')
+                               ->description('wcf.acp.pip.menuItem.menu.description')
+                               ->required()
+                               ->options(function() use ($menuList): array {
+                                       $options = [];
+                                       foreach ($menuList as $menu) {
+                                               $options[$menu->identifier] = $menu->identifier;
+                                       }
+                                       
+                                       asort($options);
+                                       
+                                       return $options;
+                               }),
+                       
+                       RadioButtonFormField::create('linkType')
+                               ->label('wcf.acp.pip.menuItem.linkType')
+                               ->required()
+                               ->options([
+                                       'internal' => 'wcf.acp.pip.menuItem.linkType.internal',
+                                       'external' => 'wcf.acp.pip.menuItem.linkType.external'
+                               ])
+                               ->value('internal'),
+                       
+                       SingleSelectionFormField::create('menuItemPage')
+                               ->objectProperty('page')
+                               ->label('wcf.acp.pip.menuItem.page')
+                               ->description('wcf.acp.pip.menuItem.page.description')
+                               ->required()
+                               ->filterable()
+                               ->options(function(): array {
+                                       $pageNodeList = (new PageNodeTree())->getNodeList();
+                                       
+                                       $nestedOptions = [[
+                                               'depth' => 0,
+                                               'label' => 'wcf.global.noSelection',
+                                               'value' => ''
+                                       ]];
+                                       
+                                       $packageIDs = array_merge(
+                                               [$this->installation->getPackage()->packageID],
+                                               array_keys($this->installation->getPackage()->getAllRequiredPackages())
+                                       );
+                                       
+                                       /** @var PageNode $pageNode */
+                                       foreach ($pageNodeList as $pageNode) {
+                                               if (in_array($pageNode->packageID, $packageIDs)) {
+                                                       $nestedOptions[] = [
+                                                               'depth' => $pageNode->getDepth() - 1,
+                                                               'label' => $pageNode->name,
+                                                               'value' => $pageNode->identifier
+                                                       ];
+                                               }
+                                       }
+                                       
+                                       return $nestedOptions;
+                               }, true),
+                       
+                       TextFormField::create('externalURL')
+                               ->label('wcf.acp.pip.menuItem.externalURL')
+                               ->description('wcf.acp.pip.menuItem.externalURL.description')
+                               ->required()
+                               ->i18n(),
+                       
+                       IntegerFormField::create('showOrder')
+                               ->objectProperty('showorder')
+                               ->label('wcf.acp.pip.menuItem.showOrder')
+                               ->description('wcf.acp.pip.menuItem.showOrder.description')
+                               ->objectProperty('showorder')
+                               ->minimum(1)
+               ]);
+               
+               /** @var SingleSelectionFormField $menuField */
+               $menuField = $form->getNodeById('menu');
+               
+               foreach ($menuList as $menu) {
+                       $dataContainer->insertBefore(
+                               SingleSelectionFormField::create('parentMenuItem' . $menu->menuID)
+                                       ->objectProperty('parent')
+                                       ->label('wcf.acp.pip.menuItem.parentMenuItem')
+                                       ->options(function() use($menu): array {
+                                               $options = [[
+                                                       'depth' => 0,
+                                                       'label' => 'wcf.global.noSelection',
+                                                       'value' => ''
+                                               ]];
+                                               
+                                               $packageIDs = array_merge(
+                                                       [$this->installation->getPackage()->packageID],
+                                                       array_keys($this->installation->getPackage()->getAllRequiredPackages())
+                                               );
+                                               
+                                               /** @var MenuItemNode $menuItem */
+                                               foreach ($menu->getMenuItemNodeList() as $menuItem) {
+                                                       if (in_array($menuItem->packageID, $packageIDs)) {
+                                                               $options[] = [
+                                                                       'depth' => $menuItem->getDepth() - 1,
+                                                                       'label' => $menuItem->identifier,
+                                                                       'value' => $menuItem->identifier
+                                                               ];
+                                                       }
+                                               }
+                                               
+                                               if (count($options) === 1) {
+                                                       return [];
+                                               }
+                                               
+                                               return $options;
+                                       }, true)
+                                       ->addDependency(
+                                               ValueFormFieldDependency::create('menu')
+                                                       ->field($menuField)
+                                                       ->values([$menu->identifier])
+                                       ),
+                               'linkType'
+                       );
+               }
+               
+               // dependencies
+               
+               /** @var RadioButtonFormField $linkType */
+               $linkType = $form->getNodeById('linkType');
+               $form->getNodeById('menuItemPage')->addDependency(
+                       ValueFormFieldDependency::create('linkType')
+                               ->field($linkType)
+                               ->values(['internal'])
+                       );
+               
+               $form->getNodeById('externalURL')->addDependency(
+                       ValueFormFieldDependency::create('linkType')
+                               ->field($linkType)
+                               ->values(['external'])
+               );
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function getElementData(\DOMElement $element, bool $saveData = false): array {
+               $data = [
+                       'identifier' => $element->getAttribute('identifier'),
+                       'packageID' => $this->installation->getPackageID(),
+                       'originIsSystem' => 1,
+                       'title' => []
+               ];
+               
+               /** @var \DOMElement $title */
+               foreach ($element->getElementsByTagName('title') as $title) {
+                       $data['title'][LanguageFactory::getInstance()->getLanguageByCode($title->getAttribute('language'))->languageID] = $title->nodeValue;
+               }
+               
+               foreach (['externalURL', 'menu', 'page', 'parent', 'showOrder'] as $optionalElementName) {
+                       $optionalElement = $element->getElementsByTagName($optionalElementName)->item(0);
+                       if ($optionalElement !== null) {
+                               $data[$optionalElementName] = $optionalElement->nodeValue;
+                       }
+               }
+               
+               if (isset($data['parent'])) {
+                       $menuItemList = new MenuItemList();
+                       $menuItemList->getConditionBuilder()->add('identifier = ?', [$data['parent']]);
+                       $menuItemList->getConditionBuilder()->add('packageID IN (?)', [
+                               array_merge(
+                                       [$this->installation->getPackage()->packageID],
+                                       array_keys($this->installation->getPackage()->getAllRequiredPackages())
+                               )
+                       ]);
+                       $menuItemList->readObjects();
+                       
+                       if (count($menuItemList) === 1) {
+                               if ($saveData) {
+                                       $data['menuID'] = $menuItemList->current()->menuID;
+                               }
+                               else {
+                                       $data['menu'] = (new Menu($menuItemList->current()->menuID))->identifier;
+                               }
+                       }
+               }
+               else if (isset($data['menu']) && $saveData) {
+                       $data['menuID'] = $this->getMenuID($data['menu']);
+                       unset($data['menu']);
+               }
+               
+               if ($saveData) {
+                       // updating menu items is not supported thus handling the title
+                       // array causes issues
+                       if ($this->editedEntry !== null) {
+                               unset($data['title']);
+                       }
+                       else {
+                               $titles = [];
+                               foreach ($data['title'] as $languageID => $title) {
+                                       $titles[LanguageFactory::getInstance()->getLanguage($languageID)->languageCode] = $title;
+                               }
+                               
+                               $data['title'] = $titles;
+                       }
+                       
+                       if (isset($data['page'])) {
+                               $data['pageID'] = $this->getPageID($data['page']);
+                               unset($data['page']);
+                       }
+               }
+               
+               return $data;
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       public function getElementIdentifier(\DOMElement $element): string {
+               return $element->getAttribute('identifier');
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function setEntryListKeys(IDevtoolsPipEntryList $entryList) {
+               $entryList->setKeys([
+                       'identifier' => 'wcf.acp.pip.menuItem.identifier',
+                       'menu' => 'wcf.acp.pip.menuItem.menu'
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function sortDocument(\DOMDocument $document) {
+               $this->sortImportDelete($document);
+               
+               $this->sortChildNodes($document->getElementsByTagName('import'), function(\DOMElement $element1, \DOMElement $element2) {
+                       // first, compare by `menu`, then by `parent` ...
+                       foreach (['menu', 'parent'] as $element) {
+                               $compareElement1 = $element1->getElementsByTagName($element)->item(0);
+                               $compareElement2 = $element2->getElementsByTagName($element)->item(0);
+                               
+                               if ($compareElement1 !== null) {
+                                       if ($compareElement2 !== null) {
+                                               $compare = $compareElement1->nodeValue <=> $compareElement2->nodeValue;
+                                               
+                                               if ($compare !== 0) {
+                                                       return $compare;
+                                               }
+                                       }
+                                       
+                                       return -1;
+                               }
+                               else if ($compareElement2 !== null) {
+                                       return 1;
+                               }
+                       }
+                       
+                       // ... and lastly by `identifier`
+                       return strcmp(
+                               $element1->getAttribute('identifier'),
+                               $element2->getAttribute('identifier')
+                       );
+               });
+               
+               $this->sortChildNodes($document->getElementsByTagName('delete'), function(\DOMElement $element1, \DOMElement $element2) {
+                       return strcmp(
+                               $element1->getAttribute('identifier'),
+                               $element2->getAttribute('identifier')
+                       );
+               });
+       }
+       
+       /**
+        * @inheritDoc
+        * @since       3.2
+        */
+       protected function writeEntry(\DOMDocument $document, IFormDocument $form): \DOMElement {
+               $formData = $form->getData();
+               $data = $formData['data'];
+               
+               $menuItem = $document->createElement('item');
+               $menuItem->setAttribute('identifier', $data['identifier']);
+               
+               if (!empty($data['menu'])) {
+                       $menuItem->appendChild($document->createElement('menu', $data['menu']));
+               }
+               else if (!empty($data['parent'])) {
+                       $menuItem->appendChild($document->createElement('parent', $data['parent']));
+               }
+               
+               foreach ($formData['title_i18n'] as $languageID => $title) {
+                       $title = $document->createElement('title', $this->getAutoCdataValue($title));
+                       $title->setAttribute('language', LanguageFactory::getInstance()->getLanguage($languageID)->languageCode);
+                       
+                       $menuItem->appendChild($title);
+               }
+               
+               foreach (['page', 'externalURL', 'showOrder'] as $property) {
+                       if (!empty($data[$property])) {
+                               $menuItem->appendChild($document->createElement($property, (string)$data[$property]));
+                       }
+               }
+               
+               $document->getElementsByTagName('import')->item(0)->appendChild($menuItem);
+               
+               return $menuItem;
+       }
 }
index f464ffbb0e9156714d65b7585423d61144258397..976ec618cf25a7cef37703c180ca65479863fd34 100644 (file)
@@ -1964,6 +1964,24 @@ If you have <strong>already bought the licenses for the listed apps</strong>, th
                <item name="wcf.acp.pip.menu.boxVisibilityExceptions.visibleEverywhere"><![CDATA[Pages on Which the Box Will be Explicitly <strong>Hidden</strong>]]></item>
                <item name="wcf.acp.pip.menu.boxCssClassName"><![CDATA[Box CSS Classes]]></item>
                <item name="wcf.acp.pip.menu.boxCssClassName.description"><![CDATA[The entered comma-separated CSS classes are assigned to the box containing the menu.]]></item>
+               <item name="wcf.acp.pip.menuItem.identifier"><![CDATA[Menu Item Identifier]]></item>
+               <item name="wcf.acp.pip.menuItem.identifier.description"><![CDATA[The menu item identifier is used to update menu items and for menu item-related language item names. The identifier consists of least four segments separated by dots. Each segment must not be empty and may only contain letters, numbers, underscores, and dashes. In general, the first part of the menu identifier is the package identifier. Example: <code>com.foo.bar.menu</code>]]></item>
+               <item name="wcf.acp.pip.menuItem.identifier.error.invalidSegments"><![CDATA[The following segments are invalid: {implode from=$invalidSegments key=segmentNumber item=segment}{if $segment !== ''}<code>{$segment}</code>{else}(empty){/if} (segment {#$segmentNumber + 1}){/implode}.]]></item>
+               <item name="wcf.acp.pip.menuItem.identifier.error.notUnique"><![CDATA[The entered identifier is already used by another menu.]]></item>
+               <item name="wcf.acp.pip.menuItem.identifier.error.tooFewSegments"><![CDATA[The identifier only contains {#$segmentCount} segment{if $segmentCount > 1}s{/if}.]]></item>
+               <item name="wcf.acp.pip.menuItem.menu"><![CDATA[Menu]]></item>
+               <item name="wcf.acp.pip.menuItem.menu.description"><![CDATA[The menu item will belong to the selected menu.]]></item>
+               <item name="wcf.acp.pip.menuItem.linkType"><![CDATA[Menu Item Link Type]]></item>
+               <item name="wcf.acp.pip.menuItem.linkType.internal"><![CDATA[Internal Link]]></item>
+               <item name="wcf.acp.pip.menuItem.linkType.external"><![CDATA[External Link]]></item>
+               <item name="wcf.acp.pip.menuItem.linkType"><![CDATA[Menu Item Link Type]]></item>
+               <item name="wcf.acp.pip.menuItem.page"><![CDATA[Page]]></item>
+               <item name="wcf.acp.pip.menuItem.page.description"><![CDATA[The menu item will link to the selected page.]]></item>
+               <item name="wcf.acp.pip.menuItem.parentMenuItem"><![CDATA[Parent Menu Item]]></item>
+               <item name="wcf.acp.pip.menuItem.showOrder"><![CDATA[Position]]></item>
+               <item name="wcf.acp.pip.menuItem.showOrder.description"><![CDATA[The entered value determines in which order the menu items with the same parent are shown.]]></item>
+               <item name="wcf.acp.pip.menuItem.externalURL"><![CDATA[External URL]]></item>
+               <item name="wcf.acp.pip.menuItem.externalURL.description"><![CDATA[When clicking on the menu item, the user is redirected to the entered website.]]></item>
        </category>
        
        <category name="wcf.acp.rebuildData">