Added trash/restore support for articles
authorAlexander Ebert <ebert@woltlab.com>
Mon, 20 Mar 2017 16:45:12 +0000 (17:45 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Mon, 20 Mar 2017 16:45:21 +0000 (17:45 +0100)
See #2209

wcfsetup/install/files/acp/templates/articleList.tpl
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Article/InlineEditor.js [new file with mode: 0644]
wcfsetup/install/files/lib/data/article/Article.class.php
wcfsetup/install/files/lib/data/article/ArticleAction.class.php
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml
wcfsetup/setup/db/install.sql

index 9fd558451103d174862f40702d5cc450d74a79ed..56ad231587fdf4013148bff4b90a244f391a9c30 100644 (file)
@@ -1,15 +1,13 @@
 {include file='header' pageTitle='wcf.acp.article.list'}
 
 <script data-relocate="true">
-       require(['WoltLabSuite/Core/Ui/User/Search/Input'], function(UiUserSearchInput) {
+       require(['Language', 'WoltLabSuite/Core/Ui/User/Search/Input', 'WoltLabSuite/Core/Acp/Ui/Article/InlineEditor'], function(Language, UiUserSearchInput, AcpUiArticleInlineEditor) {
+               Language.addObject({
+                       'wcf.message.status.deleted': '{lang}wcf.message.status.deleted{/lang}'
+               });
+               
                new UiUserSearchInput(elBySel('input[name="username"]'));
-       });
-</script>
-
-<script data-relocate="true">
-       $(function() {
-               new WCF.Action.Delete('wcf\\data\\article\\ArticleAction', '.jsArticleRow');
-               new WCF.Action.Toggle('wcf\\data\\article\\ArticleAction', '.jsArticleRow');
+               new AcpUiArticleInlineEditor();
        });
 </script>
 
                        
                        <tbody>
                                {foreach from=$objects item=article}
-                                       <tr class="jsArticleRow">
+                                       <tr class="jsArticleRow" data-object-id="{@$article->articleID}">
                                                <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()}
-                                                               <span class="icon icon24 fa-times jsDeleteButton jsTooltip pointer" title="{lang}wcf.global.button.delete{/lang}" data-object-id="{@$article->articleID}" data-confirm-message-html="{lang __encode=true}wcf.acp.article.delete.confirmMessage{/lang}"></span>
+                                                               <a href="#" class="jsButtonRestore jsTooltip" title="{lang}wcf.global.button.restore{/lang}" data-confirm-message-html="{lang __encode=true}wcf.acp.article.restore.confirmMessage{/lang}"{if !$article->isDeleted} style="display: none"{/if}><span class="icon icon24 fa-refresh"></span></a>
+                                                               <a href="#" class="jsButtonDelete jsTooltip" title="{lang}wcf.global.button.delete{/lang}" data-confirm-message-html="{lang __encode=true}wcf.acp.article.delete.confirmMessage{/lang}"{if !$article->isDeleted} style="display: none"{/if}><span class="icon icon24 fa-times"></span></a>
+                                                               <a href="#" class="jsButtonTrash jsTooltip" title="{lang}wcf.global.button.trash{/lang}" data-confirm-message-html="{lang __encode=true}wcf.acp.article.trash.confirmMessage{/lang}"{if $article->isDeleted} style="display: none"{/if}><span class="icon icon24 fa-times"></span></a>
                                                        {else}
                                                                <span class="icon icon24 fa-times disabled" title="{lang}wcf.global.button.delete{/lang}"></span>
                                                        {/if}
                                                                
                                                                <div class="containerHeadline">
                                                                        <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 == 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>
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Article/InlineEditor.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Article/InlineEditor.js
new file mode 100644 (file)
index 0000000..e61d3ad
--- /dev/null
@@ -0,0 +1,136 @@
+/**
+ * Handles article trash, restore and delete.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Acp/Ui/Article/InlineEditor
+ */
+define(['Ajax', 'Dictionary', 'Language', 'Ui/Confirmation', 'Ui/Notification'], function (Ajax, Dictionary, Language, UiConfirmation, UiNotification) {
+       "use strict";
+       
+       var _articles = new Dictionary();
+       
+       /**     
+        * @constructor
+        */
+       function AcpUiArticleInlineEditor() { this.init(); }
+       AcpUiArticleInlineEditor.prototype = {
+               /**
+                * Initializes the ACP inline editor for articles.
+                */
+               init: function () {
+                       elBySelAll('.jsArticleRow', undefined, this._initArticle.bind(this));
+               },
+               
+               /**
+                * Initializes an article row element.
+                * 
+                * @param       {Element}       article         article row element
+                * @protected
+                */
+               _initArticle: function (article) {
+                       var objectId = ~~elData(article, 'object-id');
+                       
+                       var buttonDelete = elBySel('.jsButtonDelete', article);
+                       buttonDelete.addEventListener(WCF_CLICK_EVENT, this._prompt.bind(this, objectId, 'delete'));
+                       
+                       var buttonRestore = elBySel('.jsButtonRestore', article);
+                       buttonRestore.addEventListener(WCF_CLICK_EVENT, this._prompt.bind(this, objectId, 'restore'));
+                       
+                       var buttonTrash = elBySel('.jsButtonTrash', article);
+                       buttonTrash.addEventListener(WCF_CLICK_EVENT, this._prompt.bind(this, objectId, 'trash'));
+                       
+                       _articles.set(objectId, {
+                               buttons: {
+                                       delete: buttonDelete,
+                                       restore: buttonRestore,
+                                       trash: buttonTrash
+                               },
+                               element: article
+                       });
+               },
+               
+               /**
+                * Prompts a user to confirm the clicked action before executing it.
+                * 
+                * @param       {int}           objectId        article id
+                * @param       {string}        actionName      action name
+                * @param       {Event}         event           event object
+                * @protected
+                */
+               _prompt: function (objectId, actionName, event) {
+                       event.preventDefault();
+                       
+                       var article = _articles.get(objectId);
+                       
+                       UiConfirmation.show({
+                               confirm: (function () { this._invoke(objectId, actionName) }).bind(this),
+                               message: elData(article.buttons[actionName], 'confirm-message-html'),
+                               messageIsHtml: true
+                       });
+               },
+               
+               /**
+                * Invokes the selected action.
+                * 
+                * @param       {int}           objectId        article id
+                * @param       {string}        actionName      action name
+                * @protected
+                */
+               _invoke: function (objectId, actionName) {
+                       Ajax.api(this, {
+                               actionName: actionName,
+                               objectIDs: [objectId]
+                       });
+               },
+               
+               _ajaxSuccess: function (data) {
+                       var article = _articles.get(data.objectIDs[0]);
+                       switch (data.actionName) {
+                               case 'delete':
+                                       var tbody = article.element.parentNode;
+                                       elRemove(article.element);
+                                       
+                                       if (elBySel('tr', tbody) === null) {
+                                               window.location.reload();
+                                       }
+                                       break;
+                                       
+                               case 'restore':
+                                       elHide(article.buttons.delete);
+                                       elHide(article.buttons.restore);
+                                       elShow(article.buttons.trash);
+                                       
+                                       elRemove(elBySel('.jsIconDeleted', article.element));
+                                       break;
+                                       
+                               case 'trash':
+                                       elShow(article.buttons.delete);
+                                       elShow(article.buttons.restore);
+                                       elHide(article.buttons.trash);
+                                       
+                                       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);
+                                       
+                                       break;
+                       }
+                       
+                       UiNotification.show();
+               },
+               
+               _ajaxSetup: function () {
+                       return {
+                               data: {
+                                       className: 'wcf\\data\\article\\ArticleAction'
+                               }
+                       }
+               }
+       };
+       
+       return AcpUiArticleInlineEditor;
+});
index e830ed011508545436d185e5ddb34266e30a400d..2bed2cbbe1a2f6512d664ad753bf3e9c0fda0f4e 100644 (file)
@@ -27,6 +27,7 @@ use wcf\system\WCF;
  * @property-read      integer         $comments               number of comments on the article
  * @property-read      integer         $views                  number of times the article has been viewed
  * @property-read      integer         $cumulativeLikes        cumulative result of likes (counting `+1`) and dislikes (counting `-1`) for the article
+ * @property-read      integer         $isDeleted              is 1 if the article is in trash bin, otherwise 0
  */
 class Article extends DatabaseObject implements ILinkableObject {
        /**
index fe2ecefe24fd6fc2f904b8a712ac51dfb94114e5..29c6e256c82471cbb3fdf1061a8d3ee143767de0 100644 (file)
@@ -4,11 +4,13 @@ use wcf\data\article\content\ArticleContent;
 use wcf\data\article\content\ArticleContentEditor;
 use wcf\data\AbstractDatabaseObjectAction;
 use wcf\system\comment\CommentHandler;
+use wcf\system\exception\UserInputException;
 use wcf\system\language\LanguageFactory;
 use wcf\system\like\LikeHandler;
 use wcf\system\message\embedded\object\MessageEmbeddedObjectManager;
 use wcf\system\search\SearchIndexManager;
 use wcf\system\tagging\TagEngine;
+use wcf\system\WCF;
 
 /**
  * Executes article related actions.
@@ -23,6 +25,12 @@ use wcf\system\tagging\TagEngine;
  * @method     ArticleEditor   getSingleObject()
  */
 class ArticleAction extends AbstractDatabaseObjectAction {
+       /**
+        * article editor instance
+        * @var ArticleEditor
+        */
+       public $articleEditor;
+       
        /**
         * @inheritDoc
         */
@@ -46,7 +54,7 @@ class ArticleAction extends AbstractDatabaseObjectAction {
        /**
         * @inheritDoc
         */
-       protected $requireACP = ['create', 'delete', 'update'];
+       protected $requireACP = ['create', 'delete', 'restore', 'trash', 'update'];
        
        /**
         * @inheritDoc
@@ -212,4 +220,46 @@ class ArticleAction extends AbstractDatabaseObjectAction {
                        SearchIndexManager::getInstance()->delete('com.woltlab.wcf.article', $articleContentIDs);
                }
        }
+       
+       /**
+        * Validates parameters to move an article to the trash bin.
+        * 
+        * @throws UserInputException
+        */
+       public function validateTrash() {
+               WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
+               
+               $this->articleEditor = $this->getSingleObject();
+               if ($this->articleEditor->isDeleted) {
+                       throw new UserInputException('objectIDs');
+               }
+       }
+       
+       /**
+        * Moves an article to the trash bin.
+        */
+       public function trash() {
+               $this->articleEditor->update(['isDeleted' => 1]);
+       }
+       
+       /**
+        * Validates parameters o restore an article.
+        * 
+        * @throws UserInputException
+        */
+       public function validateRestore() {
+               WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
+               
+               $this->articleEditor = $this->getSingleObject();
+               if (!$this->articleEditor->isDeleted) {
+                       throw new UserInputException('objectIDs');
+               }
+       }
+       
+       /**
+        * Restores an article.
+        */
+       public function restore() {
+               $this->articleEditor->update(['isDeleted' => 0]);
+       }
 }
index f484631c4e71218498f7bba7669620615e5e63b1..158f2b462fcb2b370f65790225e5de642f47fd48 100644 (file)
@@ -91,7 +91,9 @@
                <item name="wcf.acp.article.publicationStatus.unpublished"><![CDATA[Unveröffentlicht]]></item>
                <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} den Artikel <span class="confirmationObject">{$article->getTitle()}</span> wirklich wiederherstellen?]]></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} den Artikel <span class="confirmationObject">{$article->getTitle()}</span> wirklich in den Papierkorb verschieben?]]></item>
                <item name="wcf.acp.article.views"><![CDATA[Zugriffe]]></item>
        </category>
        
@@ -2517,11 +2519,13 @@ Fehler sind beispielsweise:
                <item name="wcf.global.button.preview"><![CDATA[Vorschau]]></item>
                <item name="wcf.global.button.refresh"><![CDATA[Aktualisieren]]></item>
                <item name="wcf.global.button.reset"><![CDATA[Zurücksetzen]]></item>
+               <item name="wcf.global.button.restore"><![CDATA[Wiederherstellen]]></item>
                <item name="wcf.global.button.rss"><![CDATA[RSS-Feed]]></item>
                <item name="wcf.global.button.save"><![CDATA[Speichern]]></item>
                <item name="wcf.global.button.saveSorting"><![CDATA[Sortierung speichern]]></item>
                <item name="wcf.global.button.search"><![CDATA[Suche]]></item>
                <item name="wcf.global.button.submit"><![CDATA[Absenden]]></item>
+               <item name="wcf.global.button.trash"><![CDATA[Löschen]]></item>
                <item name="wcf.global.button.upload"><![CDATA[Hochladen]]></item>
                <item name="wcf.global.button.readMore"><![CDATA[Weiterlesen]]></item>
                <item name="wcf.global.comments"><![CDATA[Kommentare]]></item>
index 495d8fe81173fdf15155b2930e58e62a6cfd2d37..fcd2aa71fe8169912b9c1d495e7865d7a950eb2f 100644 (file)
@@ -91,7 +91,9 @@
                <item name="wcf.acp.article.publicationStatus.unpublished"><![CDATA[Unpublished]]></item>
                <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 the article <span class="confirmationObject">{$article->getTitle()}</span>?]]></item>
                <item name="wcf.acp.article.teaser"><![CDATA[Teaser]]></item>
+               <item name="wcf.acp.article.trash.confirmMessage"><![CDATA[Do you really want to move the article <span class="confirmationObject">{$article->getTitle()}</span> to the trash bin?]]></item>
                <item name="wcf.acp.article.views"><![CDATA[Views]]></item>
        </category>
        
@@ -2469,11 +2471,13 @@ Errors are:
                <item name="wcf.global.button.preview"><![CDATA[Preview]]></item>
                <item name="wcf.global.button.refresh"><![CDATA[Refresh]]></item>
                <item name="wcf.global.button.reset"><![CDATA[Reset]]></item>
+               <item name="wcf.global.button.restore"><![CDATA[Restore]]></item>
                <item name="wcf.global.button.rss"><![CDATA[RSS Feed]]></item>
                <item name="wcf.global.button.save"><![CDATA[Save]]></item>
                <item name="wcf.global.button.saveSorting"><![CDATA[Save Sorting]]></item>
                <item name="wcf.global.button.search"><![CDATA[Search]]></item>
                <item name="wcf.global.button.submit"><![CDATA[Submit]]></item>
+               <item name="wcf.global.button.trash"><![CDATA[Move to Trash]]></item>
                <item name="wcf.global.button.upload"><![CDATA[Upload]]></item>
                <item name="wcf.global.button.readMore"><![CDATA[Read More]]></item>
                <item name="wcf.global.comments"><![CDATA[Comments]]></item>
index f017bf351f3f829255ce3a7c123cc7bc3346c2c4..f72f9f7c060a83c82e6ff2db179f3b5eb3200352 100644 (file)
@@ -167,6 +167,7 @@ CREATE TABLE wcf1_article (
        comments SMALLINT(5) NOT NULL DEFAULT 0,
        views MEDIUMINT(7) NOT NULL DEFAULT 0,
        cumulativeLikes MEDIUMINT(7) NOT NULL DEFAULT 0,
+       isDeleted TINYINT(1) NOT NULL DEFAULT 0,
        
        KEY (time)
 );