From bbcfcec36d5051ab3128128eae3048e8098d16b1 Mon Sep 17 00:00:00 2001 From: Matthias Schmidt Date: Sun, 10 Jun 2018 11:39:39 +0200 Subject: [PATCH] Add GUI for user menu package installation plugin See #2545 --- ...TXmlGuiPackageInstallationPlugin.class.php | 12 +- ...ACPMenuPackageInstallationPlugin.class.php | 149 ++---------------- ...actMenuPackageInstallationPlugin.class.php | 146 +++++++++++++++-- ...serMenuPackageInstallationPlugin.class.php | 128 ++++++++++++++- wcfsetup/install/lang/en.xml | 8 +- 5 files changed, 290 insertions(+), 153 deletions(-) diff --git a/wcfsetup/install/files/lib/system/devtools/pip/TXmlGuiPackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/devtools/pip/TXmlGuiPackageInstallationPlugin.class.php index fff75c7fad..108fb87a9d 100644 --- a/wcfsetup/install/files/lib/system/devtools/pip/TXmlGuiPackageInstallationPlugin.class.php +++ b/wcfsetup/install/files/lib/system/devtools/pip/TXmlGuiPackageInstallationPlugin.class.php @@ -46,6 +46,9 @@ trait TXmlGuiPackageInstallationPlugin { $document = $xml->getDocument(); $newElement = $this->writeEntry($document, $form); + + $this->saveObject($newElement); + $this->sortDocument($document); /** @var DevtoolsProject $project */ @@ -54,8 +57,6 @@ trait TXmlGuiPackageInstallationPlugin { // TODO: while creating/testing the gui, write into a temporary file // $xml->write($this->getXmlFileLocation($project)); $xml->write($project->path . ($project->getPackage()->package === 'com.woltlab.wcf' ? 'com.woltlab.wcf/' : '') . 'tmp_' . static::getDefaultFilename()); - - $this->saveObject($newElement); } /** @@ -77,6 +78,9 @@ trait TXmlGuiPackageInstallationPlugin { // add updated element $newEntry = $this->writeEntry($document, $form); + + $this->saveObject($newEntry, $element); + $this->sortDocument($document); /** @var DevtoolsProject $project */ @@ -86,8 +90,6 @@ trait TXmlGuiPackageInstallationPlugin { // $xml->write($this->getXmlFileLocation($project)); $xml->write($project->path . ($project->getPackage()->package === 'com.woltlab.wcf' ? 'com.woltlab.wcf/' : '') . 'tmp_' . static::getDefaultFilename()); - $this->saveObject($newEntry, $element); - return $this->getElementIdentifier($newEntry); } @@ -296,7 +298,7 @@ XML; /** * Informs the pip of the identifier of the edited entry if the form to * edit that entry has been submitted. - * + * * @param string $identifier * * @throws \InvalidArgumentException if no such entry exists diff --git a/wcfsetup/install/files/lib/system/package/plugin/ACPMenuPackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/package/plugin/ACPMenuPackageInstallationPlugin.class.php index 23d08c80a9..7a3446be77 100644 --- a/wcfsetup/install/files/lib/system/package/plugin/ACPMenuPackageInstallationPlugin.class.php +++ b/wcfsetup/install/files/lib/system/package/plugin/ACPMenuPackageInstallationPlugin.class.php @@ -1,12 +1,9 @@ getNodeById('data'); - // add parent menu item options + // add menu item icon form field - $acpMenuStructureData = $this->getACPMenuStructureData(); - $acpMenuStructure = $acpMenuStructureData['structure']; - $menuItemLevels = ['' => 0] + $acpMenuStructureData['levels']; + /** @var SingleSelectionFormField $parentMenuItemFormField */ + $parentMenuItemFormField = $form->getNodeById('parentMenuItem'); - // only consider menu items until the third level (thus only parent - // menu items until the second level) as potential parent menu items - $acpMenuStructure = array_filter($acpMenuStructure, function(string $parentMenuItem) use ($menuItemLevels): bool { - return $menuItemLevels[$parentMenuItem] <= 2; - }, ARRAY_FILTER_USE_KEY); + $menuItemLevels = ['' => 0] + $this->getMenuStructureData()['levels']; // icons are only available for menu items on the first or fourth level // thus the parent menu item must be on zeroth level (no parent menu item) @@ -80,38 +71,18 @@ class ACPMenuPackageInstallationPlugin extends AbstractMenuPackageInstallationPl return $value === 0 || $value == 3; })); - $buildOptions = function(string $parent = '', int $level = 0) use ($acpMenuStructure, &$buildOptions): array { - $options = []; - foreach ($acpMenuStructure[$parent] as $menuItem) { - $options[$menuItem->menuItem] = str_repeat('    ', $level) . WCF::getLanguage()->get($menuItem->menuItem); - - if (isset($acpMenuStructure[$menuItem->menuItem])) { - $options += $buildOptions($menuItem->menuItem, $level + 1); - } - } - - return $options; - }; - - /** @var SingleSelectionFormField $parentMenuItemFormField */ - $parentMenuItemFormField = $dataContainer->getNodeById('parentMenuItem'); - $parentMenuItemFormField - ->options(['' => 'wcf.global.noSelection'] + $buildOptions()) - ->value(''); - - // add menu icon form field - // TODO: if an `IconFormField` class should be added, use that class instead $dataContainer->appendChild(SingleSelectionFormField::create('icon') ->label('wcf.acp.pip.acpMenu.icon') ->description('wcf.acp.pip.acpMenu.icon.description') ->filterable() + ->required() ->options(function(): array { $icons = array_map(function(string $icon): string { return 'fa-' . $icon; }, StyleHandler::getInstance()->getIcons()); - return ['' => 'wcf.global.noSelection'] + array_combine($icons, $icons); + return array_combine($icons, $icons); }) ->addDependency( ValueFormFieldDependency::create('parentMenuItem') @@ -128,25 +99,7 @@ class ACPMenuPackageInstallationPlugin extends AbstractMenuPackageInstallationPl ->addValidator(FormFieldValidatorUtil::getRegularExpressionValidator( '[a-z]+\.acp\.menu\.link(\.[A-z0-9])+', 'wcf.acp.pip.acpMenu.menuItem' - )) - ->addValidator(new FormFieldValidator('uniqueness', function(TextFormField $formField) { - if ( - $formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE || - $this->editedEntry->getAttribute('name') !== $formField->getValue() - ) { - $menuItemName = new ACPMenuItemList(); - $menuItemName->getConditionBuilder()->add('menuItem = ?', [$formField->getValue()]); - - if ($menuItemName->countObjects() > 0) { - $formField->addValidationError( - new FormFieldValidationError( - 'notUnique', - 'wcf.acp.pip.abstractMenu.menuItem.error.notUnique' - ) - ); - } - } - })); + )); /** @var TextFormField $menuItemControllerFormField */ $menuItemControllerFormField = $form->getNodeById('menuItemController'); @@ -182,93 +135,19 @@ class ACPMenuPackageInstallationPlugin extends AbstractMenuPackageInstallationPl } } - /** - * Returns data on the structure of the acp menu. - * - * @return array - */ - protected function getACPMenuStructureData(): array { - $acpMenuItemList = new ACPMenuItemList(); - $acpMenuItemList->getConditionBuilder()->add('packageID IN (?)', [array_merge( - [$this->installation->getPackage()->packageID], - array_keys($this->installation->getPackage()->getAllRequiredPackages()) - )]); - $acpMenuItemList->sqlOrderBy = 'parentMenuItem ASC, showOrder ASC'; - $acpMenuItemList->readObjects(); - - /** @var ACPMenuItem[] $acpMenuItems */ - $acpMenuItems = []; - /** @var ACPMenuItem[][] $acpMenuStructure */ - $acpMenuStructure = []; - foreach ($acpMenuItemList as $menuItem) { - if (!isset($acpMenuStructure[$menuItem->parentMenuItem])) { - $acpMenuStructure[$menuItem->parentMenuItem] = []; - } - - $acpMenuStructure[$menuItem->parentMenuItem][$menuItem->menuItem] = $menuItem; - $acpMenuItems[$menuItem->menuItem] = $menuItem; - } - - $menuItemLevels = []; - foreach ($acpMenuStructure as $parentMenuItemName => $menuItems) { - $menuItemsLevel = 1; - - while (($parentMenuItem = $acpMenuItems[$parentMenuItemName] ?? null)) { - $menuItemsLevel++; - $parentMenuItemName = $parentMenuItem->parentMenuItem; - } - - foreach ($menuItems as $menuItem) { - $menuItemLevels[$menuItem->menuItem] = $menuItemsLevel; - } - } - - return [ - 'levels' => $menuItemLevels, - 'structure' => $acpMenuStructure - ]; - } - /** * @inheritDoc * @since 3.2 */ - protected function sortDocument(\DOMDocument $document) { - $acpMenuStructureData = $this->getACPMenuStructureData(); - /** @var ACPMenuItem[][] $menuItemStructure */ - $menuItemStructure = $acpMenuStructureData['structure']; + protected function getElementData(\DOMElement $element): array { + $data = parent::getElementData($element); - $this->sortImportDelete($document); - - // build array containing the ACP menu items saved in the database - // in the order as they would be displayed in the ACP - $buildPositions = function(string $parent = '') use ($menuItemStructure, &$buildPositions): array { - $positions = []; - foreach ($menuItemStructure[$parent] as $menuItem) { - // only consider menu items of the current package for positions - if ($menuItem->packageID === $this->installation->getPackageID()) { - $positions[] = $menuItem->menuItem; - } - - if (isset($menuItemStructure[$menuItem->menuItem])) { - $positions = array_merge($positions, $buildPositions($menuItem->menuItem)); - } - } - - return $positions; - }; - - // flip positions array so that the keys are the menu item names - // and the values become the positions so that the array values - // can be used in the sort function - $positions = array_flip($buildPositions()); - - $compareFunction = function(\DOMElement $element1, \DOMElement $element2) use ($positions) { - return $positions[$element1->getAttribute('name')] <=> $positions[$element2->getAttribute('name')]; - }; + $icon = $element->getElementsByTagName('icon')->item(0); + if ($icon !== null) { + $data['icon'] = $icon->nodeValue; + } - $this->sortChildNodes($document->getElementsByTagName('import'), $compareFunction); - $this->sortChildNodes($document->getElementsByTagName('delete'), $compareFunction); + return $data; } /** diff --git a/wcfsetup/install/files/lib/system/package/plugin/AbstractMenuPackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/package/plugin/AbstractMenuPackageInstallationPlugin.class.php index 6514eda01a..7da7c0c57a 100644 --- a/wcfsetup/install/files/lib/system/package/plugin/AbstractMenuPackageInstallationPlugin.class.php +++ b/wcfsetup/install/files/lib/system/package/plugin/AbstractMenuPackageInstallationPlugin.class.php @@ -1,6 +1,8 @@ * @package WoltLabSuite\Core\System\Package\Plugin @@ -129,12 +131,61 @@ abstract class AbstractMenuPackageInstallationPlugin extends AbstractXMLPackageI $dataContainer->appendChildren([ TextFormField::create('menuItem') ->objectProperty('name') - ->label('wcf.acp.pip.abstractMenu.menuItem'), + ->label('wcf.acp.pip.abstractMenu.menuItem') + ->addValidator(new FormFieldValidator('uniqueness', function(TextFormField $formField) { + if ( + $formField->getDocument()->getFormMode() === IFormDocument::FORM_MODE_CREATE || + $this->editedEntry->getAttribute('name') !== $formField->getValue() + ) { + // replace `Editor` with `List` + $listClassName = substr($this->className, 0, -6) . 'List'; + + /** @var DatabaseObjectList $menuItemList */ + $menuItemList = new $listClassName(); + $menuItemList->getConditionBuilder()->add('menuItem = ?', [$formField->getValue()]); + + if ($menuItemList->countObjects() > 0) { + $formField->addValidationError( + new FormFieldValidationError( + 'notUnique', + 'wcf.acp.pip.abstractMenu.menuItem.error.notUnique' + ) + ); + } + } + })), SingleSelectionFormField::create('parentMenuItem') ->objectProperty('parent') - ->label('wcf.acp.pip.abstractForm.parentMenuItem') - ->filterable(), + ->label('wcf.acp.pip.abstractMenu.parentMenuItem') + ->filterable() + ->options(function(): array { + $acpMenuStructureData = $this->getMenuStructureData(); + $acpMenuStructure = $acpMenuStructureData['structure']; + $menuItemLevels = ['' => 0] + $acpMenuStructureData['levels']; + + // only consider menu items until the third level (thus only parent + // menu items until the second level) as potential parent menu items + $acpMenuStructure = array_filter($acpMenuStructure, function(string $parentMenuItem) use ($menuItemLevels): bool { + return $menuItemLevels[$parentMenuItem] <= 2; + }, ARRAY_FILTER_USE_KEY); + + $buildOptions = function(string $parent = '', int $level = 0) use ($acpMenuStructure, &$buildOptions): array { + $options = []; + foreach ($acpMenuStructure[$parent] as $menuItem) { + $options[$menuItem->menuItem] = str_repeat('    ', $level) . WCF::getLanguage()->get($menuItem->menuItem); + + if (isset($acpMenuStructure[$menuItem->menuItem])) { + $options += $buildOptions($menuItem->menuItem, $level + 1); + } + } + + return $options; + }; + + return ['' => 'wcf.global.noSelection'] + $buildOptions(); + }) + ->value(''), ClassNameFormField::create('menuItemController') ->objectProperty('controller') @@ -260,6 +311,59 @@ abstract class AbstractMenuPackageInstallationPlugin extends AbstractXMLPackageI return $element->getAttribute('name'); } + /** + * Returns data on the structure of the menu. + * + * @return array + */ + protected function getMenuStructureData(): array { + // replace `Editor` with `List` + $listClassName = substr($this->className, 0, -6) . 'List'; + + /** @var DatabaseObjectList $menuItemList */ + $menuItemList = new $listClassName(); + $menuItemList->getConditionBuilder()->add('packageID IN (?)', [array_merge( + [$this->installation->getPackage()->packageID], + array_keys($this->installation->getPackage()->getAllRequiredPackages()) + )]); + $menuItemList->sqlOrderBy = 'parentMenuItem ASC, showOrder ASC'; + $menuItemList->readObjects(); + + // for better IDE auto-completion, we use `ACPMenuItem`, but the + // menu items can also belong to other menus + /** @var ACPMenuItem[] $menuItems */ + $menuItems = []; + /** @var ACPMenuItem[][] $menuStructure */ + $menuStructure = []; + foreach ($menuItemList as $menuItem) { + if (!isset($menuStructure[$menuItem->parentMenuItem])) { + $menuStructure[$menuItem->parentMenuItem] = []; + } + + $menuStructure[$menuItem->parentMenuItem][$menuItem->menuItem] = $menuItem; + $menuItems[$menuItem->menuItem] = $menuItem; + } + + $menuItemLevels = []; + foreach ($menuStructure as $parentMenuItemName => $childMenuItems) { + $menuItemsLevel = 1; + + while (($parentMenuItem = $menuItems[$parentMenuItemName] ?? null)) { + $menuItemsLevel++; + $parentMenuItemName = $parentMenuItem->parentMenuItem; + } + + foreach ($childMenuItems as $menuItem) { + $menuItemLevels[$menuItem->menuItem] = $menuItemsLevel; + } + } + + return [ + 'levels' => $menuItemLevels, + 'structure' => $menuStructure + ]; + } + /** * @inheritDoc * @since 3.2 @@ -276,13 +380,37 @@ abstract class AbstractMenuPackageInstallationPlugin extends AbstractXMLPackageI * @since 3.2 */ protected function sortDocument(\DOMDocument $document) { + $menuStructureData = $this->getMenuStructureData(); + /** @var ACPMenuItem[][] $menuItemStructure */ + $menuItemStructure = $menuStructureData['structure']; + $this->sortImportDelete($document); - $compareFunction = function(\DOMElement $element1, \DOMElement $element2) { - return strcmp( - $element1->getAttribute('name'), - $element2->getAttribute('name') - ); + // build array containing the ACP menu items saved in the database + // in the order as they would be displayed in the ACP + $buildPositions = function(string $parent = '') use ($menuItemStructure, &$buildPositions): array { + $positions = []; + foreach ($menuItemStructure[$parent] as $menuItem) { + // only consider menu items of the current package for positions + if ($menuItem->packageID === $this->installation->getPackageID()) { + $positions[] = $menuItem->menuItem; + } + + if (isset($menuItemStructure[$menuItem->menuItem])) { + $positions = array_merge($positions, $buildPositions($menuItem->menuItem)); + } + } + + return $positions; + }; + + // flip positions array so that the keys are the menu item names + // and the values become the positions so that the array values + // can be used in the sort function + $positions = array_flip($buildPositions()); + + $compareFunction = function(\DOMElement $element1, \DOMElement $element2) use ($positions) { + return $positions[$element1->getAttribute('name')] <=> $positions[$element2->getAttribute('name')]; }; $this->sortChildNodes($document->getElementsByTagName('import'), $compareFunction); diff --git a/wcfsetup/install/files/lib/system/package/plugin/UserMenuPackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/package/plugin/UserMenuPackageInstallationPlugin.class.php index 34a2e9712c..cab1927826 100644 --- a/wcfsetup/install/files/lib/system/package/plugin/UserMenuPackageInstallationPlugin.class.php +++ b/wcfsetup/install/files/lib/system/package/plugin/UserMenuPackageInstallationPlugin.class.php @@ -2,16 +2,26 @@ declare(strict_types=1); namespace wcf\system\package\plugin; use wcf\data\user\menu\item\UserMenuItemEditor; +use wcf\system\devtools\pip\IGuiPackageInstallationPlugin; +use wcf\system\form\builder\container\IFormContainer; +use wcf\system\form\builder\field\ClassNameFormField; +use wcf\system\form\builder\field\dependency\ValueFormFieldDependency; +use wcf\system\form\builder\field\SingleSelectionFormField; +use wcf\system\form\builder\field\TextFormField; +use wcf\system\form\builder\field\validation\FormFieldValidatorUtil; +use wcf\system\form\builder\IFormDocument; +use wcf\system\menu\user\IUserMenuItemProvider; +use wcf\system\style\StyleHandler; /** * Installs, updates and deletes user menu items. * - * @author Alexander Ebert + * @author Alexander Ebert, Matthias Schmidt * @copyright 2001-2018 WoltLab GmbH * @license GNU Lesser General Public License * @package WoltLabSuite\Core\System\Package\Plugin */ -class UserMenuPackageInstallationPlugin extends AbstractMenuPackageInstallationPlugin { +class UserMenuPackageInstallationPlugin extends AbstractMenuPackageInstallationPlugin implements IGuiPackageInstallationPlugin { /** * @inheritDoc */ @@ -45,4 +55,118 @@ class UserMenuPackageInstallationPlugin extends AbstractMenuPackageInstallationP return $result; } + + /** + * @inheritDoc + * @since 3.2 + */ + public function addFormFields(IFormDocument $form) { + parent::addFormFields($form); + + /** @var IFormContainer $dataContainer */ + $dataContainer = $form->getNodeById('data'); + + // add menu item className form field + + $classNameFormField = ClassNameFormField::create() + ->implementedInterface(IUserMenuItemProvider::class); + $dataContainer->insertBefore($classNameFormField, 'menuItemController'); + + // add menu item icon form field + + /** @var SingleSelectionFormField $parentMenuItemFormField */ + $parentMenuItemFormField = $form->getNodeById('parentMenuItem'); + + // TODO: if an `IconFormField` class should be added, use that class instead + $dataContainer->appendChild(SingleSelectionFormField::create('iconClassName') + ->objectProperty('iconclassname') + ->label('wcf.acp.pip.userMenu.iconClassName') + ->description('wcf.acp.pip.userMenu.iconClassName.description') + ->filterable() + ->required() + ->options(function(): array { + $icons = array_map(function(string $icon): string { + return 'fa-' . $icon; + }, StyleHandler::getInstance()->getIcons()); + + return array_combine($icons, $icons); + }) + ->addDependency( + // only first level menu items support icons + ValueFormFieldDependency::create('parentMenuItem') + ->field($parentMenuItemFormField) + ->values(['']) + )); + + // add additional data to default fields + + /** @var TextFormField $menuItemFormField */ + $menuItemFormField = $form->getNodeById('menuItem'); + $menuItemFormField + ->description('wcf.acp.pip.userMenu.menuItem.description') + ->addValidator(FormFieldValidatorUtil::getRegularExpressionValidator( + '[a-z]+\.user.menu(\.[A-z0-9])+', + 'wcf.acp.pip.userMenu.menuItem' + )); + + // add dependencies to default fields + + $menuItemLevels = ['' => 0] + $this->getMenuStructureData()['levels']; + + // menu items on the first and second level do not support links, + // thus the parent menu item must be at least on the second level + // for the menu item to support links + $menuItemsSupportingLinks = array_keys(array_filter($menuItemLevels, function(int $menuItemLevel): bool { + return $menuItemLevel >= 2; + })); + + foreach (['menuItemController', 'menuItemLink'] as $nodeId) { + /** @var TextFormField $formField */ + $formField = $form->getNodeById($nodeId); + $formField->addDependency( + ValueFormFieldDependency::create('parentMenuItem') + ->field($parentMenuItemFormField) + ->values($menuItemsSupportingLinks) + ); + } + } + + /** + * @inheritDoc + * @since 3.2 + */ + protected function getElementData(\DOMElement $element): array { + $data = parent::getElementData($element); + + $className = $element->getElementsByTagName('classname')->item(0); + if ($className !== null) { + $data['className'] = $className->nodeValue; + } + + $icon = $element->getElementsByTagName('iconclassname')->item(0); + if ($icon !== null) { + $data['iconClassName'] = $icon->nodeValue; + } + + return $data; + } + + /** + * @inheritDoc + * @since 3.2 + */ + protected function writeEntry(\DOMDocument $document, IFormDocument $form): \DOMElement { + $formData = $form->getData()['data']; + + $menuItem = parent::writeEntry($document, $form); + + if (!empty($formData['classname'])) { + $menuItem->appendChild($document->createElement('classname', $formData['classname'])); + } + if (!empty($formData['iconclassname'])) { + $menuItem->appendChild($document->createElement('iconclassname', $formData['iconclassname'])); + } + + return $menuItem; + } } diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index c139e23aa2..073803f4e7 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -1902,8 +1902,8 @@ When prompted for the notification URL for the instant payment notifications, pl - - + + @@ -1918,6 +1918,10 @@ When prompted for the notification URL for the instant payment notifications, pl {literal}{app}.acp.menu.link.{additionalIdentifiers}{/literal} where {literal}{app}{/literal} and {literal}{additionalIdentifiers}{/literal} have to be replaced with the appropriate values. {literal}{additionalIdentifiers}{/literal} may only contain letters, numbers, ands dots.]]> {literal}{app}{/literal}\acp\ where {literal}{app}{/literal} is the abbreviation of the relevant app.]]> + + + {literal}{app}.user.menu.{additionalIdentifiers}{/literal} where {literal}{app}{/literal} and {literal}{additionalIdentifiers}{/literal} have to be replaced with the appropriate values. {literal}{additionalIdentifiers}{/literal} may only contain letters, numbers, ands dots.]]> + -- 2.20.1