Add clipboard support for articles
authorMatthias Schmidt <gravatronics@live.com>
Sun, 9 Apr 2017 13:13:06 +0000 (15:13 +0200)
committerMatthias Schmidt <gravatronics@live.com>
Sun, 9 Apr 2017 13:13:06 +0000 (15:13 +0200)
Close #2197

com.woltlab.wcf/clipboardAction.xml
com.woltlab.wcf/objectType.xml
wcfsetup/install/files/acp/templates/articleCategoryDialog.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/articleList.tpl
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Article/InlineEditor.js
wcfsetup/install/files/lib/acp/page/ArticleListPage.class.php
wcfsetup/install/files/lib/data/article/ArticleAction.class.php
wcfsetup/install/files/lib/system/clipboard/action/ArticleClipboardAction.class.php [new file with mode: 0644]
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index 125b48104a41d463c483c3f1ede142a765415141..a4b084e707687a05fa66d38dc4ba8b8ee3af7cb9 100644 (file)
                        </pages>
                </action>
                <!-- /com.woltlab.wcf.media -->
+               
+               <!-- com.woltlab.wcf.article -->
+               <action name="trash">
+                       <actionclassname>wcf\system\clipboard\action\ArticleClipboardAction</actionclassname>
+                       <showorder>1</showorder>
+                       <pages>
+                               <page>wcf\acp\page\ArticleListPage</page>
+                       </pages>
+               </action>
+               <action name="delete">
+                       <actionclassname>wcf\system\clipboard\action\ArticleClipboardAction</actionclassname>
+                       <showorder>2</showorder>
+                       <pages>
+                               <page>wcf\acp\page\ArticleListPage</page>
+                       </pages>
+               </action>
+               <action name="restore">
+                       <actionclassname>wcf\system\clipboard\action\ArticleClipboardAction</actionclassname>
+                       <showorder>3</showorder>
+                       <pages>
+                               <page>wcf\acp\page\ArticleListPage</page>
+                       </pages>
+               </action>
+               <action name="unpublish">
+                       <actionclassname>wcf\system\clipboard\action\ArticleClipboardAction</actionclassname>
+                       <showorder>4</showorder>
+                       <pages>
+                               <page>wcf\acp\page\ArticleListPage</page>
+                       </pages>
+               </action>
+               <action name="publish">
+                       <actionclassname>wcf\system\clipboard\action\ArticleClipboardAction</actionclassname>
+                       <showorder>5</showorder>
+                       <pages>
+                               <page>wcf\acp\page\ArticleListPage</page>
+                       </pages>
+               </action>
+               <action name="setCategory">
+                       <actionclassname>wcf\system\clipboard\action\ArticleClipboardAction</actionclassname>
+                       <showorder>6</showorder>
+                       <pages>
+                               <page>wcf\acp\page\ArticleListPage</page>
+                       </pages>
+               </action>
+               <!-- /com.woltlab.wcf.article -->
        </import>
 </data>
index 57c32ba69d6051059f23d65e3fc5083032b802ed..50fdfd17e6e2b7009923f8cebfc1e15bc9af3d21 100644 (file)
                        <definitionname>com.woltlab.wcf.clipboardItem</definitionname>
                        <listclassname>wcf\data\media\ViewableMediaList</listclassname>
                </type>
+               <type>
+                       <name>com.woltlab.wcf.article</name>
+                       <definitionname>com.woltlab.wcf.clipboardItem</definitionname>
+                       <listclassname>wcf\data\article\ArticleList</listclassname>
+               </type>
                <!-- /clipboard items -->
                
                <!-- articles -->
diff --git a/wcfsetup/install/files/acp/templates/articleCategoryDialog.tpl b/wcfsetup/install/files/acp/templates/articleCategoryDialog.tpl
new file mode 100644 (file)
index 0000000..a58197f
--- /dev/null
@@ -0,0 +1,18 @@
+<div class="section">
+       <dl>
+               <dt>{lang}wcf.acp.article.category{/lang}</dt>
+               <dd>
+                       <select name="categoryID" id="categoryID">
+                               <option value="0">{lang}wcf.global.noSelection{/lang}</option>
+                               
+                               {foreach from=$categoryNodeList item=category}
+                                       <option value="{@$category->categoryID}">{if $category->getDepth() > 1}{@"&nbsp;&nbsp;&nbsp;&nbsp;"|str_repeat:($category->getDepth() - 1)}{/if}{$category->getTitle()}</option>
+                               {/foreach}
+                       </select>
+               </dd>
+       </dl>
+</div>
+
+<div class="formSubmit">
+       <button data-type="submit">{lang}wcf.global.button.submit{/lang}</button>
+</div>
index 4d6dbcf8bf0d9a3bb6ab4e360aaa0b001367494b..4c16880cc0f7c330685194149809212f9c921f32 100644 (file)
@@ -1,13 +1,21 @@
 {include file='header' pageTitle='wcf.acp.article.list'}
 
 <script data-relocate="true">
-       require(['Language', 'WoltLabSuite/Core/Ui/User/Search/Input', 'WoltLabSuite/Core/Acp/Ui/Article/InlineEditor'], function(Language, UiUserSearchInput, AcpUiArticleInlineEditor) {
+       require(['Language', 'WoltLabSuite/Core/Controller/Clipboard', 'WoltLabSuite/Core/Ui/User/Search/Input', 'WoltLabSuite/Core/Acp/Ui/Article/InlineEditor'],
+               function(Language, ControllerClipboard, UiUserSearchInput, AcpUiArticleInlineEditor) {
                Language.addObject({
+                       'wcf.acp.article.publicationStatus.unpublished': '{lang}wcf.acp.article.publicationStatus.unpublished{/lang}',
+                       'wcf.acp.article.setCategory': '{lang}wcf.acp.article.setCategory{/lang}',
                        'wcf.message.status.deleted': '{lang}wcf.message.status.deleted{/lang}'
                });
                
                new UiUserSearchInput(elBySel('input[name="username"]'));
                new AcpUiArticleInlineEditor(0);
+               
+               ControllerClipboard.setup({
+                       hasMarkedItems: {if $hasMarkedItems}true{else}false{/if},
+                       pageClassName: 'wcf\\acp\\page\\ArticleListPage'
+               });
        });
 </script>
 
 
 {if $objects|count}
        <div class="section tabularBox">
-               <table class="table">
+               <table data-type="com.woltlab.wcf.article" class="table jsClipboardContainer">
                        <thead>
                                <tr>
+                                       <th class="columnMark"><label><input type="checkbox" class="jsClipboardMarkAll"></label></th>
                                        <th class="columnID columnArticleID{if $sortField == 'articleID'} active {@$sortOrder}{/if}" colspan="2"><a href="{link controller='ArticleList'}pageNo={@$pageNo}&sortField=articleID&sortOrder={if $sortField == 'articleID' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{@$linkParameters}{/link}">{lang}wcf.global.objectID{/lang}</a></th>
                                        <th class="columnText columnArticleTitle{if $sortField == 'title'} active {@$sortOrder}{/if}"><a href="{link controller='ArticleList'}pageNo={@$pageNo}&sortField=title&sortOrder={if $sortField == 'title' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{@$linkParameters}{/link}">{lang}wcf.global.title{/lang}</a></th>
                                        <th class="columnDigits columnComments{if $sortField == 'comments'} active {@$sortOrder}{/if}"><a href="{link controller='ArticleList'}pageNo={@$pageNo}&sortField=comments&sortOrder={if $sortField == 'comments' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{@$linkParameters}{/link}">{lang}wcf.global.comments{/lang}</a></th>
                        
                        <tbody>
                                {foreach from=$objects item=article}
-                                       <tr class="jsArticleRow" data-object-id="{@$article->articleID}">
+                                       <tr class="jsArticleRow jsClipboardObject" data-object-id="{@$article->articleID}">
+                                               <td class="columnMark"><input type="checkbox" class="jsClipboardItem" data-object-id="{@$article->articleID}"></td>
                                                <td class="columnIcon">
                                                        <a href="{link controller='ArticleEdit' id=$article->articleID}{/link}" title="{lang}wcf.global.button.edit{/lang}" class="jsTooltip"><span class="icon icon24 fa-pencil"></span></a>
                                                        {if $article->canDelete()}
                                                                        
                                                                        <h3>
                                                                                {if $article->isDeleted}<span class="badge label red jsIconDeleted">{lang}wcf.message.status.deleted{/lang}</span>{/if}
-                                                                               {if $article->publicationStatus == 0}<span class="badge">{lang}wcf.acp.article.publicationStatus.unpublished{/lang}</span>{/if}
+                                                                               {if $article->publicationStatus == 0}<span class="badge jsUnpublishedArticle">{lang}wcf.acp.article.publicationStatus.unpublished{/lang}</span>{/if}
                                                                                {if $article->publicationStatus == 2}<span class="badge" title="{$article->publicationDate|plainTime}">{lang}wcf.acp.article.publicationStatus.delayed{/lang}</span>{/if}
                                                                                <a href="{link controller='ArticleEdit' id=$article->articleID}{/link}" title="{lang}wcf.acp.article.edit{/lang}" class="jsTooltip">{$article->title}</a>
                                                                        </h3>
                                                                        <ul class="inlineList dotSeparated">
                                                                                {if $article->categoryID}
-                                                                                       <li>{$article->getCategory()->getTitle()}</li>
+                                                                                       <li class="jsArticleCategory">{$article->getCategory()->getTitle()}</li>
                                                                                {/if}
                                                                                
                                                                                {if $article->username}
index d9decaa75130f28efd0372939ef7831a466ca311..a77beaaea471ed86953f086f7c623a08193d69ad 100644 (file)
@@ -6,7 +6,8 @@
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @module     WoltLabSuite/Core/Acp/Ui/Article/InlineEditor
  */
-define(['Ajax', 'Core', 'Dictionary', 'Language', 'Ui/Confirmation', 'Ui/Notification'], function (Ajax, Core, Dictionary, Language, UiConfirmation, UiNotification) {
+define(['Ajax', 'Core', 'Dictionary', 'Dom/Util', 'EventHandler', 'Language', 'Ui/Confirmation', 'Ui/Dialog', 'Ui/Notification', 'WoltLabSuite/Core/Controller/Clipboard'],
+       function (Ajax, Core, Dictionary, DomUtil, EventHandler, Language, UiConfirmation, UiDialog, UiNotification, ControllerClipboard) {
        "use strict";
        
        var _articles = new Dictionary();
@@ -36,6 +37,97 @@ define(['Ajax', 'Core', 'Dictionary', 'Language', 'Ui/Confirmation', 'Ui/Notific
                        }
                        else {
                                elBySelAll('.jsArticleRow', undefined, this._initArticle.bind(this));
+                               
+                               EventHandler.add('com.woltlab.wcf.clipboard', 'com.woltlab.wcf.article', this._clipboardAction.bind(this));
+                       }
+               },
+               
+               /**
+                * Reacts to executed clipboard actions.
+                *
+                * @param       {object<string, *>}     actionData      data of the executed clipboard action
+                */
+               _clipboardAction: function(actionData) {
+                       // only consider events if the action has been executed
+                       if (actionData.responseData !== null) {
+                               var triggerFunction;
+                               switch (actionData.data.actionName) {
+                                       case 'com.woltlab.wcf.article.delete':
+                                               triggerFunction = this._triggerDelete;
+                                               break;
+                                       
+                                       case 'com.woltlab.wcf.article.publish':
+                                               triggerFunction = this._triggerPublish;
+                                               break;
+                                       
+                                       case 'com.woltlab.wcf.article.restore':
+                                               triggerFunction = this._triggerRestore;
+                                               break;
+                                       
+                                       case 'com.woltlab.wcf.article.trash':
+                                               triggerFunction = this._triggerTrash;
+                                               break;
+                                       
+                                       case 'com.woltlab.wcf.article.unpublish':
+                                               triggerFunction = this._triggerUnpublish;
+                                               break;
+                               }
+                               
+                               if (triggerFunction) {
+                                       for (var i = 0, length = actionData.responseData.objectIDs.length; i < length; i++) {
+                                               triggerFunction(actionData.responseData.objectIDs[i]);
+                                       }
+                                       
+                                       UiNotification.show();
+                               }
+                       }
+                       else if (actionData.data.actionName === 'com.woltlab.wcf.article.setCategory') {
+                               try {
+                                       dialog = UiDialog.getDialog('articleCategoryDialog');
+                                       UiDialog.openStatic('articleCategoryDialog');
+                               }
+                               catch (e) {
+                                       UiDialog.openStatic('articleCategoryDialog', actionData.data.internalData.template, {
+                                               title: Language.get('wcf.acp.article.setCategory')
+                                       });
+                                       
+                                       elBySel('[data-type=submit]', UiDialog.getDialog('articleCategoryDialog').content).addEventListener('click', this._submitSetCategory.bind(this));
+                               }
+                       }
+               },
+               
+               /**
+                * Is called, if the set category dialog form is submitted.
+                * 
+                * @param       {Event}         event           form submit button click event
+                */
+               _submitSetCategory: function(event) {
+                       var dialog = UiDialog.getDialog('articleCategoryDialog').content;
+                       var innerErrors = elByClass('innerError', dialog);
+                       var select = elBySel('select[name=categoryID]', dialog);
+                       
+                       var categoryId = ~~elBySel('select[name=categoryID]', event.currentTarget.parentNode.parentNode).value;
+                       if (categoryId) {
+                               Ajax.api(this, {
+                                       actionName: 'setCategory',
+                                       parameters: {
+                                               categoryID: categoryId,
+                                               useMarkedArticles: true
+                                       }
+                               });
+                               
+                               if (innerErrors.length === 1) {
+                                       elRemove(innerErrors.item(0));
+                               }
+                               
+                               UiDialog.close('articleCategoryDialog');
+                       }
+                       else if (innerErrors.length === 0) {
+                               var innerError = elCreate('small');
+                               innerError.className = 'innerError';
+                               innerError.innerText = Language.get('wcf.global.form.error.empty');
+                               
+                               DomUtil.insertAfter(innerError, select);
                        }
                },
                
@@ -48,7 +140,7 @@ define(['Ajax', 'Core', 'Dictionary', 'Language', 'Ui/Confirmation', 'Ui/Notific
                 */
                _initArticle: function (article, objectId) {
                        var isArticleEdit = false;
-                       if (~~objectId > 0) {
+                       if (!article && ~~objectId > 0) {
                                isArticleEdit = true;
                                article = undefined;
                        }
@@ -158,35 +250,127 @@ define(['Ajax', 'Core', 'Dictionary', 'Language', 'Ui/Confirmation', 'Ui/Notific
                        });
                },
                
+               /**
+                * Handles an article being deleted.
+                * 
+                * @param       {int}           articleId       id of the deleted article
+                */
+               _triggerDelete: function(articleId) {
+                       var article = _articles.get(articleId);
+                       
+                       if (article.isArticleEdit) {
+                               //noinspection JSUnresolvedVariable
+                               window.location = data.returnValues.redirectURL;
+                       }
+                       else {
+                               var tbody = article.element.parentNode;
+                               elRemove(article.element);
+                               
+                               if (elBySel('tr', tbody) === null) {
+                                       window.location.reload();
+                               }
+                       }
+               },
+               
+               /**
+                * Handles publishing an article via clipboard.
+                *
+                * @param       {int}           articleId       id of the published article
+                */
+               _triggerPublish: function(articleId) {
+                       var article = _articles.get(articleId);
+                       
+                       if (article.isArticleEdit) {
+                               // unsupported
+                       }
+                       else {
+                               elRemove(elBySel('.jsUnpublishedArticle', article.element));
+                       }
+               },
+               
+               /**
+                * Handles an article being restored.
+                *
+                * @param       {int}           articleId       id of the deleted article
+                */
+               _triggerRestore: function(articleId) {
+                       var article = _articles.get(articleId);
+                       
+                       elHide(article.buttons.delete);
+                       elHide(article.buttons.restore);
+                       elShow(article.buttons.trash);
+                       
+                       if (article.isArticleEdit) {
+                               elHide(elBySel('.jsArticleNoticeTrash'));
+                       }
+                       else {
+                               elRemove(elBySel('.jsIconDeleted', article.element));
+                       }
+               },
+               
+               /**
+                * Handles an article being trashed.
+                *
+                * @param       {int}           articleId       id of the deleted article
+                */
+               _triggerTrash: function(articleId) {
+                       var article = _articles.get(articleId);
+                       
+                       elShow(article.buttons.delete);
+                       elShow(article.buttons.restore);
+                       elHide(article.buttons.trash);
+                       
+                       if (article.isArticleEdit) {
+                               elShow(elBySel('.jsArticleNoticeTrash'));
+                       }
+                       else {
+                               var badge = elCreate('span');
+                               badge.className = 'badge label red jsIconDeleted';
+                               badge.textContent = Language.get('wcf.message.status.deleted');
+                               
+                               var h3 = elBySel('.containerHeadline > h3', article.element);
+                               h3.insertBefore(badge, h3.firstChild);
+                       }
+               },
+               
+               /**
+                * Handles unpublishing an article via clipboard.
+                *
+                * @param       {int}           articleId       id of the unpublished article
+                */
+               _triggerUnpublish: function(articleId) {
+                       var article = _articles.get(articleId);
+                       
+                       if (article.isArticleEdit) {
+                               // unsupported
+                       }
+                       else {
+                               var badge = elCreate('span');
+                               badge.className = 'badge jsUnpublishedArticle';
+                               badge.textContent = Language.get('wcf.acp.article.publicationStatus.unpublished');
+                               
+                               var h3 = elBySel('.containerHeadline > h3', article.element);
+                               var a = elBySel('a', h3);
+                               
+                               h3.insertBefore(badge, a);
+                               h3.insertBefore(document.createTextNode(" "), a);
+                       }
+               },
+               
                _ajaxSuccess: function (data) {
-                       var article = _articles.get(data.objectIDs[0]);
+                       var notificationCallback;
+                       
                        switch (data.actionName) {
                                case 'delete':
-                                       if (article.isArticleEdit) {
-                                               //noinspection JSUnresolvedVariable
-                                               window.location = data.returnValues.redirectURL;
-                                       }
-                                       else {
-                                               var tbody = article.element.parentNode;
-                                               elRemove(article.element);
-                                               
-                                               if (elBySel('tr', tbody) === null) {
-                                                       window.location.reload();
-                                               }
-                                       }
+                                       this._triggerDelete(data.objectIDs[0]);
                                        break;
                                        
                                case 'restore':
-                                       elHide(article.buttons.delete);
-                                       elHide(article.buttons.restore);
-                                       elShow(article.buttons.trash);
+                                       this._triggerRestore(data.objectIDs[0]);
+                                       break;
                                        
-                                       if (article.isArticleEdit) {
-                                               elHide(elBySel('.jsArticleNoticeTrash'));
-                                       }
-                                       else {
-                                               elRemove(elBySel('.jsIconDeleted', article.element));
-                                       }
+                               case 'setCategory':
+                                       notificationCallback = window.location.reload.bind(window.location);
                                        break;
                                        
                                case 'toggleI18n':
@@ -194,26 +378,12 @@ define(['Ajax', 'Core', 'Dictionary', 'Language', 'Ui/Confirmation', 'Ui/Notific
                                        break;
                                        
                                case 'trash':
-                                       elShow(article.buttons.delete);
-                                       elShow(article.buttons.restore);
-                                       elHide(article.buttons.trash);
-                                       
-                                       if (article.isArticleEdit) {
-                                               elShow(elBySel('.jsArticleNoticeTrash'));
-                                       }
-                                       else {
-                                               var badge = elCreate('span');
-                                               badge.className = 'badge label red jsIconDeleted';
-                                               badge.textContent = Language.get('wcf.message.status.deleted');
-                                               
-                                               var h3 = elBySel('.containerHeadline > h3', article.element);
-                                               h3.insertBefore(badge, h3.firstChild);
-                                       }
-                                       
+                                       this._triggerTrash(data.objectIDs[0]);
                                        break;
                        }
                        
-                       UiNotification.show();
+                       UiNotification.show(undefined, notificationCallback);
+                       ControllerClipboard.reload();
                },
                
                _ajaxSetup: function () {
index e29b4b9c877cad6bc8023b639f97d6c3f431b845..ce66c44f2e56add4f1f9d03b5a467e6b4e019f0e 100644 (file)
@@ -6,6 +6,7 @@ use wcf\data\article\ViewableArticleList;
 use wcf\data\category\CategoryNodeTree;
 use wcf\data\user\User;
 use wcf\page\SortablePage;
+use wcf\system\clipboard\ClipboardHandler;
 use wcf\system\language\LanguageFactory;
 use wcf\system\WCF;
 use wcf\util\StringUtil;
@@ -159,7 +160,8 @@ class ArticleListPage extends SortablePage {
                        'showArticleAddDialog' => $this->showArticleAddDialog,
                        'availableLanguages' => LanguageFactory::getInstance()->getLanguages(),
                        'categoryNodeList' => (new CategoryNodeTree('com.woltlab.wcf.article.category'))->getIterator(),
-                       'publicationStatus' => $this->publicationStatus
+                       'publicationStatus' => $this->publicationStatus,
+                       'hasMarkedItems' => ClipboardHandler::getInstance()->hasMarkedItems(ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.article')),
                ]);
        }
 }
index f0295742aee57a904227620eda0be12d6bc89a04..802da5204fc5afa2132a1206e3ffadc326236887 100644 (file)
@@ -1,10 +1,12 @@
 <?php
 namespace wcf\data\article;
+use wcf\data\article\category\ArticleCategory;
 use wcf\data\article\content\ArticleContent;
 use wcf\data\article\content\ArticleContentAction;
 use wcf\data\article\content\ArticleContentEditor;
 use wcf\data\language\Language;
 use wcf\data\AbstractDatabaseObjectAction;
+use wcf\system\clipboard\ClipboardHandler;
 use wcf\system\comment\CommentHandler;
 use wcf\system\exception\UserInputException;
 use wcf\system\language\LanguageFactory;
@@ -240,6 +242,29 @@ class ArticleAction extends AbstractDatabaseObjectAction {
                }
        }
        
+       /**
+        * Validates parameters to delete articles.
+        *
+        * @throws      UserInputException
+        */
+       public function validateDelete() {
+               WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
+               
+               if (empty($this->objects)) {
+                       $this->readObjects();
+                       
+                       if (empty($this->objects)) {
+                               throw new UserInputException('objectIDs');
+                       }
+               }
+               
+               foreach ($this->getObjects() as $article) {
+                       if (!$article->isDeleted) {
+                               throw new UserInputException('objectIDs');
+                       }
+               }
+       }
+       
        /**
         * @inheritDoc
         */
@@ -266,51 +291,70 @@ class ArticleAction extends AbstractDatabaseObjectAction {
                        SearchIndexManager::getInstance()->delete('com.woltlab.wcf.article', $articleContentIDs);
                }
                
+               $this->unmarkItems();
+               
                return [
+                       'objectIDs' => $this->objectIDs,
                        'redirectURL' => LinkHandler::getInstance()->getLink('ArticleList', ['isACP' => true])
                ];
        }
        
        /**
-        * Validates parameters to move an article to the trash bin.
+        * Validates parameters to move articles to the trash bin.
         * 
-        * @throws UserInputException
+        * @throws      UserInputException
         */
        public function validateTrash() {
                WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
                
-               $this->articleEditor = $this->getSingleObject();
-               if ($this->articleEditor->isDeleted) {
-                       throw new UserInputException('objectIDs');
+               if (empty($this->objects)) {
+                       $this->readObjects();
+                       
+                       if (empty($this->objects)) {
+                               throw new UserInputException('objectIDs');
+                       }
+               }
+               
+               foreach ($this->getObjects() as $article) {
+                       if ($article->isDeleted) {
+                               throw new UserInputException('objectIDs');
+                       }
                }
        }
        
        /**
-        * Moves an article to the trash bin.
+        * Moves articles to the trash bin.
         */
        public function trash() {
-               $this->articleEditor->update(['isDeleted' => 1]);
+               foreach ($this->getObjects() as $articleEditor) {
+                       $articleEditor->update(['isDeleted' => 1]);
+               }
+               
+               $this->unmarkItems();
+               
+               return ['objectIDs' => $this->objectIDs];
        }
        
        /**
-        * Validates parameters o restore an article.
+        * Validates parameters to restore articles.
         * 
-        * @throws UserInputException
+        * @throws      UserInputException
         */
        public function validateRestore() {
-               WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
-               
-               $this->articleEditor = $this->getSingleObject();
-               if (!$this->articleEditor->isDeleted) {
-                       throw new UserInputException('objectIDs');
-               }
+               $this->validateDelete();
        }
        
        /**
-        * Restores an article.
+        * Restores articles.
         */
        public function restore() {
-               $this->articleEditor->update(['isDeleted' => 0]);
+               foreach ($this->getObjects() as $articleEditor) {
+                       $articleEditor->update(['isDeleted' => 0]);
+               }
+               
+               $this->unmarkItems();
+               
+               return ['objectIDs' => $this->objectIDs];
        }
        
        /**
@@ -431,4 +475,133 @@ class ArticleAction extends AbstractDatabaseObjectAction {
        public function validateMarkAllAsRead() {
                // does nothing
        }
+       
+       /**
+        * Validates the `setCategory` action.
+        * 
+        * @throws      UserInputException
+        */
+       public function validateSetCategory() {
+               WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
+               
+               $this->readBoolean('useMarkedArticles', true);
+               
+               // if no object ids are given, use clipboard handler
+               if (empty($this->objectIDs) && $this->parameters['useMarkedArticles']) {
+                       $this->objectIDs = array_keys(ClipboardHandler::getInstance()->getMarkedItems(ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.article')));
+               }
+               
+               if (empty($this->objects)) {
+                       $this->readObjects();
+                       
+                       if (empty($this->objects)) {
+                               throw new UserInputException('objectIDs');
+                       }
+               }
+               
+               $this->readInteger('categoryID');
+               if (ArticleCategory::getCategory($this->parameters['categoryID']) === null) {
+                       throw new UserInputException('categoryID');
+               }
+       }
+       
+       /**
+        * Sets the category of articles.
+        */
+       public function setCategory() {
+               foreach ($this->getObjects() as $articleEditor) {
+                       $articleEditor->update(['categoryID' => $this->parameters['categoryID']]);
+               }
+               
+               $this->unmarkItems();
+       }
+       
+       /**
+        * Validates the `publish` action.
+        * 
+        * @throws      UserInputException
+        */
+       public function validatePublish() {
+               WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
+               
+               if (empty($this->objects)) {
+                       $this->readObjects();
+                       
+                       if (empty($this->objects)) {
+                               throw new UserInputException('objectIDs');
+                       }
+               }
+               
+               foreach ($this->getObjects() as $article) {
+                       if ($article->publicationStatus == Article::PUBLISHED) {
+                               throw new UserInputException('objectIDs');
+                       }
+               }
+       }
+       
+       /**
+        * Publishes articles.
+        */
+       public function publish() {
+               foreach ($this->getObjects() as $articleEditor) {
+                       $articleEditor->update([
+                               'time' => TIME_NOW,
+                               'publicationStatus' => Article::PUBLISHED,
+                               'publicationDate' => 0
+                       ]);
+               }
+               
+               $this->unmarkItems();
+       }
+       
+       /**
+        * Validates the `unpublish` action.
+        *
+        * @throws      UserInputException
+        */
+       public function validateUnpublish() {
+               WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
+               
+               if (empty($this->objects)) {
+                       $this->readObjects();
+                       
+                       if (empty($this->objects)) {
+                               throw new UserInputException('objectIDs');
+                       }
+               }
+               
+               foreach ($this->getObjects() as $article) {
+                       if ($article->publicationStatus != Article::PUBLISHED) {
+                               throw new UserInputException('objectIDs');
+                       }
+               }
+       }
+       
+       /**
+        * Unpublishes articles.
+        */
+       public function unpublish() {
+               foreach ($this->getObjects() as $articleEditor) {
+                       $articleEditor->update(['publicationStatus' => Article::UNPUBLISHED]);
+               }
+               
+               $this->unmarkItems();
+       }
+       
+       /**
+        * Unmarks articles.
+        * 
+        * @param       integer[]       $articleIDs
+        */
+       protected function unmarkItems(array $articleIDs = []) {
+               if (empty($articleIDs)) {
+                       foreach ($this->getObjects() as $article) {
+                               $articleIDs[] = $article->articleID;
+                       }
+               }
+               
+               if (!empty($articleIDs)) {
+                       ClipboardHandler::getInstance()->unmark($articleIDs, ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.article'));
+               }
+       }
 }
diff --git a/wcfsetup/install/files/lib/system/clipboard/action/ArticleClipboardAction.class.php b/wcfsetup/install/files/lib/system/clipboard/action/ArticleClipboardAction.class.php
new file mode 100644 (file)
index 0000000..fca1abb
--- /dev/null
@@ -0,0 +1,199 @@
+<?php
+namespace wcf\system\clipboard\action;
+use wcf\data\article\Article;
+use wcf\data\article\ArticleAction;
+use wcf\data\category\CategoryNodeTree;
+use wcf\data\clipboard\action\ClipboardAction;
+use wcf\system\WCF;
+
+/**
+ * Clipboard action implementation for articles.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Clipboard\Action
+ * @since      3.1
+ */
+class ArticleClipboardAction extends AbstractClipboardAction {
+       /**
+        * @inheritDoc
+        */
+       protected $actionClassActions = [
+               'delete',
+               'publish',
+               'restore',
+               'trash',
+               'unpublish'
+       ];
+       
+       /**
+        * @inheritDoc
+        */
+       protected $supportedActions = [
+               'delete',
+               'publish',
+               'restore',
+               'setCategory',
+               'trash',
+               'unpublish'
+       ];
+       
+       /**
+        * @inheritDoc
+        */
+       public function execute(array $objects, ClipboardAction $action) {
+               $item = parent::execute($objects, $action);
+               
+               if ($item === null) {
+                       return null;
+               }
+               
+               // handle actions
+               switch ($action->actionName) {
+                       case 'delete':
+                               $item->addInternalData('confirmMessage', WCF::getLanguage()->getDynamicVariable('wcf.clipboard.item.com.woltlab.wcf.article.delete.confirmMessage', [
+                                       'count' => $item->getCount()
+                               ]));
+                               break;
+                               
+                       case 'setCategory':
+                               $item->addInternalData('template', WCF::getTPL()->fetch('articleCategoryDialog', 'wcf', [
+                                       'categoryNodeList' => (new CategoryNodeTree('com.woltlab.wcf.article.category'))->getIterator()
+                               ]));
+                               break;
+                       
+                       case 'trash':
+                               $item->addInternalData('confirmMessage', WCF::getLanguage()->getDynamicVariable('wcf.clipboard.item.com.woltlab.wcf.article.trash.confirmMessage', [
+                                       'count' => $item->getCount()
+                               ]));
+                               break;
+               }
+               
+               return $item;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getClassName() {
+               return ArticleAction::class;
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getTypeName() {
+               return 'com.woltlab.wcf.article';
+       }
+       
+       /**
+        * Returns the ids of the articles that can be deleted.
+        *
+        * @return      integer[]
+        */
+       public function validateDelete() {
+               if (!WCF::getSession()->getPermission('admin.content.article.canManageArticle')) {
+                       return [];
+               }
+               
+               $objectIDs = [];
+               
+               /** @var Article $article */
+               foreach ($this->objects as $article) {
+                       if ($article->isDeleted) {
+                               $objectIDs[] = $article->articleID;
+                       }
+               }
+               
+               return $objectIDs;
+       }
+       
+       /**
+        * Returns the ids of the articles that can be published.
+        * 
+        * @return      integer[]
+        */
+       public function validatePublish() {
+               if (!WCF::getSession()->getPermission('admin.content.article.canManageArticle')) {
+                       return [];
+               }
+               
+               $objectIDs = [];
+               
+               /** @var Article $article */
+               foreach ($this->objects as $article) {
+                       if ($article->publicationStatus == Article::UNPUBLISHED) {
+                               $objectIDs[] = $article->articleID;
+                       }
+               }
+               
+               return $objectIDs;
+       }
+       
+       /**
+        * Returns the ids of the articles that can be restored.
+        *
+        * @return      integer[]
+        */
+       public function validateRestore() {
+               return $this->validateDelete();
+       }
+       
+       /**
+        * Returns the ids of the articles whose category can be set.
+        * 
+        * @return      integer[]
+        */
+       public function validateSetCategory() {
+               if (!WCF::getSession()->getPermission('admin.content.article.canManageArticle')) {
+                       return [];
+               }
+               
+               return array_keys($this->objects);
+       }
+       
+       /**
+        * Returns the ids of the articles that can be trashed.
+        * 
+        * @return      integer[]
+        */
+       public function validateTrash() {
+               if (!WCF::getSession()->getPermission('admin.content.article.canManageArticle')) {
+                       return [];
+               }
+               
+               $objectIDs = [];
+               
+               /** @var Article $article */
+               foreach ($this->objects as $article) {
+                       if (!$article->isDeleted) {
+                               $objectIDs[] = $article->articleID;
+                       }
+               }
+               
+               return $objectIDs;
+       }
+       
+       /**
+        * Returns the ids of the articles that can be unpublished.
+        *
+        * @return      integer[]
+        */
+       public function validateUnpublish() {
+               if (!WCF::getSession()->getPermission('admin.content.article.canManageArticle')) {
+                       return [];
+               }
+               
+               $objectIDs = [];
+               
+               /** @var Article $article */
+               foreach ($this->objects as $article) {
+                       if ($article->publicationStatus == Article::PUBLISHED) {
+                               $objectIDs[] = $article->articleID;
+                       }
+               }
+               
+               return $objectIDs;
+       }
+}
index 07ed4d3abc7c9af3437e14c842288cdf26275659..63703e7560dad2c1656c5bf97c63de16936c1317 100644 (file)
@@ -96,6 +96,7 @@
                <item name="wcf.acp.article.publicationStatus.published"><![CDATA[Veröffentlicht]]></item>
                <item name="wcf.acp.article.publicationStatus.delayed"><![CDATA[Zeitgesteuerte Veröffentlichung]]></item>
                <item name="wcf.acp.article.restore.confirmMessage"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} {if $isArticleEdit|empty}den Artikel <span class="confirmationObject">{$article->getTitle()}</span>{else}diesen Artikel{/if} wirklich wiederherstellen?]]></item>
+               <item name="wcf.acp.article.setCategory"><![CDATA[Kategorie ändern]]></item>
                <item name="wcf.acp.article.teaser"><![CDATA[Einleitungstext]]></item>
                <item name="wcf.acp.article.trash.confirmMessage"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} {if $isArticleEdit|empty}den Artikel <span class="confirmationObject">{$article->getTitle()}</span>{else}diesen Artikel{/if} wirklich in den Papierkorb verschieben?]]></item>
                <item name="wcf.acp.article.trash.notice"><![CDATA[Dieser Artikel befindet sich im Papierkorb und wird gegenwärtig nicht angezeigt.]]></item>
@@ -2166,6 +2167,15 @@ Fehler sind beispielsweise:
        <category name="wcf.clipboard">
                <item name="wcf.clipboard.item.unmarkAll"><![CDATA[Demarkieren]]></item>
                
+               <item name="wcf.clipboard.item.com.woltlab.wcf.article.delete"><![CDATA[Löschen ({#$count})]]></item>
+               <item name="wcf.clipboard.item.com.woltlab.wcf.article.delete.confirmMessage"><![CDATA[{if $count == 1}Eine{else}{#$count}{/if} Artikel löschen?]]></item>
+               <item name="wcf.clipboard.item.com.woltlab.wcf.article.publish"><![CDATA[Veröffentlichen ({#$count})]]></item>
+               <item name="wcf.clipboard.item.com.woltlab.wcf.article.restore"><![CDATA[Wiederherstellen ({#$count})]]></item>
+               <item name="wcf.clipboard.item.com.woltlab.wcf.article.setCategory"><![CDATA[Kategorie ändern ({#$count})]]></item>
+               <item name="wcf.clipboard.item.com.woltlab.wcf.article.trash"><![CDATA[Löschen ({#$count})]]></item>
+               <item name="wcf.clipboard.item.com.woltlab.wcf.article.trash.confirmMessage"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} {if $count == 1}einen{else}{#$count}{/if} Artikel wirklich in den Papierkorb verschieben?]]></item>
+               <item name="wcf.clipboard.item.com.woltlab.wcf.article.unpublish"><![CDATA[Veröffentlichung zurückziehen ({#$count})]]></item>
+               
                <item name="wcf.clipboard.item.com.woltlab.wcf.media.delete"><![CDATA[Löschen ({#$count})]]></item>
                <item name="wcf.clipboard.item.com.woltlab.wcf.media.delete.confirmMessage"><![CDATA[{if $count == 1}Eine Datei{else}{#$count} Dateien{/if} löschen?]]></item>
                <item name="wcf.clipboard.item.com.woltlab.wcf.media.insert"><![CDATA[Einfügen ({#$count})]]></item>
@@ -2186,6 +2196,7 @@ Fehler sind beispielsweise:
                <item name="wcf.clipboard.item.com.woltlab.wcf.user.sendNewPassword"><![CDATA[Neues Kennwort senden ({#$count})]]></item>
                <item name="wcf.clipboard.item.com.woltlab.wcf.user.sendNewPassword.confirmMessage"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} wirklich {if $count == 1}einem Benutzer{else}{#$count} Benutzern{/if} ein neues Kennwort zusenden?]]></item>
                
+               <item name="wcf.clipboard.label.com.woltlab.wcf.article.marked"><![CDATA[{if $count == 1}Ein{else}{#$count}{/if} Artikel]]></item>
                <item name="wcf.clipboard.label.com.woltlab.wcf.media.marked"><![CDATA[{if $count == 1}Eine Datei{else}{#$count} Dateien{/if}]]></item>
                <item name="wcf.clipboard.label.com.woltlab.wcf.tag.marked"><![CDATA[{if $count == 1}Ein Tag{else}{#$count} Tags{/if}]]></item>
                <item name="wcf.clipboard.label.com.woltlab.wcf.user.marked"><![CDATA[{if $count == 1}Ein{else}{#$count}{/if} Benutzer]]></item>
index 02c39c3b661dfe87b6262fc51bc1a96ca8255ce8..eed1e033f21a3fd159dc58fd1e7e317f6f58dcfc 100644 (file)
@@ -96,6 +96,7 @@
                <item name="wcf.acp.article.publicationStatus.published"><![CDATA[Published]]></item>
                <item name="wcf.acp.article.publicationStatus.delayed"><![CDATA[Delayed publishing]]></item>
                <item name="wcf.acp.article.restore.confirmMessage"><![CDATA[Do you really want to restore {if $isArticleEdit|empty}the article <span class="confirmationObject">{$article->getTitle()}</span>{else}this article{/if}?]]></item>
+               <item name="wcf.acp.article.setCategory"><![CDATA[Set Category]]></item>
                <item name="wcf.acp.article.teaser"><![CDATA[Teaser]]></item>
                <item name="wcf.acp.article.trash.confirmMessage"><![CDATA[Do you really want to move {if $isArticleEdit|empty}the article <span class="confirmationObject">{$article->getTitle()}</span>{else}this article{/if} to the trash bin?]]></item>
                <item name="wcf.acp.article.trash.notice"><![CDATA[This article has been moved to the trash bin and is currently hidden from view.]]></item>
@@ -2118,6 +2119,15 @@ Errors are:
        <category name="wcf.clipboard">
                <item name="wcf.clipboard.item.unmarkAll"><![CDATA[Unmark All]]></item>
                
+               <item name="wcf.clipboard.item.com.woltlab.wcf.article.delete"><![CDATA[Delete ({#$count})]]></item>
+               <item name="wcf.clipboard.item.com.woltlab.wcf.article.delete.confirmMessage"><![CDATA[Do you really want to delete {#$count} article{if $count != 1}s{/if}?]]></item>
+               <item name="wcf.clipboard.item.com.woltlab.wcf.article.publish"><![CDATA[Publish ({#$count})]]></item>
+               <item name="wcf.clipboard.item.com.woltlab.wcf.article.restore"><![CDATA[Restore ({#$count})]]></item>
+               <item name="wcf.clipboard.item.com.woltlab.wcf.article.setCategory"><![CDATA[Set Category ({#$count})]]></item>
+               <item name="wcf.clipboard.item.com.woltlab.wcf.article.trash"><![CDATA[Move to Trash ({#$count})]]></item>
+               <item name="wcf.clipboard.item.com.woltlab.wcf.article.trash.confirmMessage"><![CDATA[Do you really want to move {#$count} article{if $count != 1}s{/if} to the trash bin?]]></item>
+               <item name="wcf.clipboard.item.com.woltlab.wcf.article.unpublish"><![CDATA[Withdraw Publication ({#$count})]]></item>
+               
                <item name="wcf.clipboard.item.com.woltlab.wcf.media.delete"><![CDATA[Delete ({#$count})]]></item>
                <item name="wcf.clipboard.item.com.woltlab.wcf.media.delete.confirmMessage"><![CDATA[Do you really want to delete {#$count} file{if $count != 1}s{/if}?]]></item>
                <item name="wcf.clipboard.item.com.woltlab.wcf.media.insert"><![CDATA[Insert ({#$count})]]></item>
@@ -2138,6 +2148,7 @@ Errors are:
                <item name="wcf.clipboard.item.com.woltlab.wcf.user.sendNewPassword"><![CDATA[Send New Password ({#$count})]]></item>
                <item name="wcf.clipboard.item.com.woltlab.wcf.user.sendNewPassword.confirmMessage"><![CDATA[Do you really want to send a new password to {#$count} user{if $count != 1}s{/if}?]]></item>
                
+               <item name="wcf.clipboard.label.com.woltlab.wcf.article.marked"><![CDATA[{if $count == 1}One Article{else}{#$count} Articles{/if}]]></item>
                <item name="wcf.clipboard.label.com.woltlab.wcf.media.marked"><![CDATA[{if $count == 1}One File{else}{#$count} Files{/if}]]></item>
                <item name="wcf.clipboard.label.com.woltlab.wcf.tag.marked"><![CDATA[{if $count == 1}One Tag{else}{#$count} Tags{/if}]]></item>
                <item name="wcf.clipboard.label.com.woltlab.wcf.user.marked"><![CDATA[{if $count == 1}One User{else}{#$count} Users{/if}]]></item>