diff --git a/plugins/baser-core/src/Service/UtilitiesService.php b/plugins/baser-core/src/Service/UtilitiesService.php index da1ac33d6e..5679500367 100644 --- a/plugins/baser-core/src/Service/UtilitiesService.php +++ b/plugins/baser-core/src/Service/UtilitiesService.php @@ -57,7 +57,23 @@ class UtilitiesService implements UtilitiesServiceInterface public function verityContentsTree(): bool { $contentsTable = TableRegistry::getTableLocator()->get('BaserCore.Contents'); - $result = $this->_verify($contentsTable); + return $this->verityTree($contentsTable); + } + + /** + * ツリー構造をチェックする + * + * 問題がある場合にはログを出力する + * + * @param Table $table TreeBehavior を利用しているテーブル + * @return bool + * @checked + * @noTodo + * @unitTest + */ + public function verityTree(Table $table): bool + { + $result = $this->_verify($table); if ($result !== true) { foreach($result as $value) { $this->log(implode(', ', $value)); @@ -97,7 +113,6 @@ protected function _verify(Table $table) $left = 'lft'; $scope = '1 = 1'; $parent = 'parent_id'; - $plugin = 'BaserCore'; if (!$table->find()->applyOptions(['withDeleted'])->where([$scope])->count()) { return true; } @@ -125,7 +140,7 @@ protected function _verify(Table $table) } $table->belongsTo('VerifyParent', [ - 'className' => $plugin . '.' . $table->getAlias(), + 'className' => $table->getRegistryAlias(), 'propertyName' => 'VerifyParent', 'foreignKey' => $parent ]); diff --git a/plugins/baser-core/src/Service/UtilitiesServiceInterface.php b/plugins/baser-core/src/Service/UtilitiesServiceInterface.php index a00ecf04f9..4cf4434ca9 100644 --- a/plugins/baser-core/src/Service/UtilitiesServiceInterface.php +++ b/plugins/baser-core/src/Service/UtilitiesServiceInterface.php @@ -14,6 +14,7 @@ use BaserCore\Annotation\UnitTest; use BaserCore\Annotation\NoTodo; use BaserCore\Annotation\Checked; +use Cake\ORM\Table; /** * UtilitiesServiceInterface @@ -31,6 +32,19 @@ interface UtilitiesServiceInterface */ public function verityContentsTree(): bool; + /** + * ツリー構造をチェックする + * + * 問題がある場合にはログを出力する + * + * @param Table $table TreeBehavior を利用しているテーブル + * @return bool + * @checked + * @noTodo + * @unitTest + */ + public function verityTree(Table $table): bool; + /** * コンテンツツリーをリセットし全て同階層にする * diff --git a/plugins/bc-admin-third/src/bc_blog/js/admin/blog_categories/index.js b/plugins/bc-admin-third/src/bc_blog/js/admin/blog_categories/index.js index de41051265..33ff322b38 100755 --- a/plugins/bc-admin-third/src/bc_blog/js/admin/blog_categories/index.js +++ b/plugins/bc-admin-third/src/bc_blog/js/admin/blog_categories/index.js @@ -10,11 +10,231 @@ $(function () { + var scriptData = $("#AdminBlogCategoriesIndexScript"); + var blogContentId = scriptData.attr('data-blogContentId'); + var listType = scriptData.attr('data-listType'); + var addUrl = scriptData.attr('data-addUrl'); + + if (listType === '1') { + // ツリー形式 + initTree(); + $("#GrpChangeTreeOpenClose").show(); + } else { + // 表形式:一括処理 + $.bcBatch.init({ + batchUrl: $.bcUtil.apiAdminBaseUrl + 'bc-blog/blog_categories/batch.json' + }); + } + + // 表示形式の切り替え(ツリー形式 / 表形式) + $("input[name='ViewSetting[list_type]']").change(function () { + $.bcUtil.showLoader(); + var selected = $("input[name='ViewSetting[list_type]']:checked").val(); + location.href = $.bcUtil.adminBaseUrl + 'bc-blog/blog_categories/index/' + blogContentId + '?list_type=' + selected; + }); + /** - * 一括処理実装 + * ノードに紐づくブログカテゴリのデータ(data-jstree)を返す + * @param node + * @returns {object} */ - $.bcBatch.init({ - batchUrl: $.bcUtil.apiAdminBaseUrl + 'bc-blog/blog_categories/batch.json' - }); + function nodeData(node) { + return (node && node.data) ? node.data.jstree : {}; + } + + /** + * ツリー(jstree)の初期化 + */ + function initTree() { + var treeDom = $('#BlogCategoryTreeList'); + if (!treeDom.length) { + return; + } + + treeDom.jstree({ + 'core': { + 'themes': {'name': 'proton', 'stripes': true, 'variant': 'large'}, + 'multiple': false, + 'force_text': true, + // 自分自身の子孫への移動は jstree 側が禁止する。それ以外は任意の親へ移動可。 + 'check_callback': true + }, + 'plugins': ['dnd', 'state', 'wholerow', 'contextmenu', 'types'], + 'types': {'default': {}}, + 'state': {'key': 'blog-category-tree-' + blogContentId}, + 'dnd': { + 'large_drop_target': true, + 'is_draggable': function () { + return true; + } + }, + 'contextmenu': { + 'show_at_node': false, + 'items': buildContextMenu + } + }); + + var jstreeApi = treeDom.jstree(true); + + // 展開・折りたたみ + $("#BtnOpenTree").click(function () { + jstreeApi.open_all(); + }); + $("#BtnCloseTree").click(function () { + jstreeApi.close_all(); + }); + + // ドラッグ&ドロップによる移動(並び替え・再親付け) + treeDom.on('move_node.jstree', function (e, data) { + orderCategory(jstreeApi, data); + }); + } + + /** + * ドラッグ&ドロップ完了時に、移動内容をサーバへ保存する + * @param jstreeApi + * @param data move_node.jstree のイベントデータ + */ + function orderCategory(jstreeApi, data) { + var node = data.node; + var origin = nodeData(node); + + // 移動先の親カテゴリ(ルートは空) + var targetParentId = ''; + if (data.parent !== '#') { + targetParentId = nodeData(jstreeApi.get_node(data.parent)).categoryId; + } + // 移動先で、自分の直後にくる兄弟(=この上に配置する対象)。無ければ末尾。 + var parentChildren = jstreeApi.get_node(data.parent).children; + var nextNodeId = parentChildren[data.position + 1]; + var targetId = nextNodeId ? nodeData(jstreeApi.get_node(nextNodeId)).categoryId : ''; + + $.bcToken.check(function () { + return $.ajax({ + url: $.bcUtil.apiAdminBaseUrl + 'bc-blog/blog_categories/move.json', + type: 'PATCH', + dataType: 'json', + data: { + origin: {id: origin.categoryId, parentId: origin.parentId}, + target: {id: targetId, parentId: targetParentId}, + _csrfToken: $.bcToken.key + }, + beforeSend: function () { + $.bcUtil.hideMessage(); + $.bcUtil.showLoader(); + }, + success: function () { + // 移動先の親を記憶し、続けての移動に備える + origin.parentId = targetParentId; + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + // サーバ状態は変わっていないため、再読込で元の並びへ戻す + var errorMessage = ''; + if (XMLHttpRequest.status === 404) { + errorMessage = '
' + bcI18n.commonNotFoundProgramMessage; + } else if (XMLHttpRequest.responseText) { + errorMessage = '
' + JSON.parse(XMLHttpRequest.responseText).message; + } else { + errorMessage = '
' + errorThrown; + } + $.bcUtil.showAlertMessage(bcI18n.commonBatchExecFailedMessage + '(' + XMLHttpRequest.status + ')' + errorMessage); + location.reload(); + }, + complete: function () { + $.bcUtil.hideLoader(); + } + }); + }, {hideLoader: false}); + } + + /** + * 右クリックメニューを構築する + * @param node + * @returns {object} + */ + function buildContextMenu(node) { + var data = nodeData(node); + var menu = {}; + + // 確認(フロントのカテゴリ一覧を別タブで開く) + if (data.previewUrl) { + menu.view = { + label: bcI18n.bcTreeCheck, + icon: 'bca-icon--preview', + action: function () { + window.open(data.previewUrl, '_blank'); + } + }; + } + + // 編集 + menu.edit = { + label: bcI18n.bcTreeEdit, + icon: 'bca-icon--edit', + action: function () { + location.href = data.editUrl; + } + }; + + // 子カテゴリを追加 + menu.add = { + label: bcI18n.bcTreeAddChild, + icon: 'bca-icon--add', + action: function () { + location.href = addUrl + '?parent_id=' + data.categoryId; + } + }; + + // 削除 + menu.delete = { + label: bcI18n.bcTreeDelete, + icon: 'bca-icon--delete', + action: function () { + deleteCategory(node, data); + } + }; + + return menu; + } + + /** + * カテゴリを削除する + * @param node + * @param data + */ + function deleteCategory(node, data) { + var title = $("#BlogCategoryTreeList").jstree(true).get_text(node); + if (!confirm(bcI18n.blogCategoryConfirmDelete.replace('%s', title))) { + return; + } + $.bcToken.check(function () { + return $.ajax({ + url: $.bcUtil.apiAdminBaseUrl + 'bc-blog/blog_categories/delete/' + data.categoryId + '.json', + type: 'POST', + dataType: 'json', + data: { + _csrfToken: $.bcToken.key + }, + beforeSend: function () { + $.bcUtil.hideMessage(); + $.bcUtil.showLoader(); + }, + success: function () { + // 削除後はサーバの状態に合わせて再読込する + location.reload(); + }, + error: function (XMLHttpRequest) { + var errorMessage = ''; + if (XMLHttpRequest.responseText) { + errorMessage = '
' + JSON.parse(XMLHttpRequest.responseText).message; + } + $.bcUtil.showAlertMessage(bcI18n.blogCategoryDeleteFailed + errorMessage); + }, + complete: function () { + $.bcUtil.hideLoader(); + } + }); + }, {hideLoader: false}); + } }); diff --git a/plugins/bc-admin-third/templates/Admin/Utilities/index.php b/plugins/bc-admin-third/templates/Admin/Utilities/index.php index 2a90d272e5..bbe315caee 100644 --- a/plugins/bc-admin-third/templates/Admin/Utilities/index.php +++ b/plugins/bc-admin-third/templates/Admin/Utilities/index.php @@ -67,6 +67,16 @@ ]) ?> +dispatchLayerEvent('afterUtilitiesIndex', [], ['layer' => 'View', 'class' => '', 'plugin' => '']); + if ($event !== false) { + echo ($event->getResult() === null || $event->getResult() === true)? '' : $event->getResult(); + } +} +?> + BcAdminForm->secure() ?>

diff --git a/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/BlogCategories/index.php b/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/BlogCategories/index.php index a0ae892f68..185d841f3c 100755 --- a/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/BlogCategories/index.php +++ b/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/BlogCategories/index.php @@ -13,13 +13,14 @@ * [ADMIN] ブログカテゴリ 一覧 * @var BcBlog\View\BlogAdminAppView $this * @var BcBlog\Model\Entity\BlogContent $blogContent + * @var string $template 表示テンプレート(index_list: 表形式 / index_tree: ツリー形式) * @checked * @noTodo * @unitTest */ $this->BcAdmin->setTitle(__d('baser_core', '{0}|カテゴリ一覧', $blogContent->content->title)); $this->BcAdmin->setHelp('blog_categories_index'); -$this->BcBaser->js('BcBlog.admin/blog_categories/index.bundle', false); +$this->BcBaser->element('BlogCategories/index_setup', ['blogContent' => $blogContent, 'template' => $template]); $this->BcAdmin->addAdminMainBodyHeaderLinks([ 'url' => ['action' => 'add', $blogContent->id], 'title' => __d('baser_core', '新規追加'), @@ -27,6 +28,10 @@ ?> + + +BcBaser->element('BlogCategories/index_view_setting') ?> +
- BcBaser->element('BlogCategories/index_list') ?> + BcBaser->element('BlogCategories/' . $template) ?>
diff --git a/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/BlogCategories/index_list.php b/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/BlogCategories/index_list.php index 7f95c21a50..c3fcb04b90 100755 --- a/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/BlogCategories/index_list.php +++ b/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/BlogCategories/index_list.php @@ -79,7 +79,7 @@ foreach($rowIdTmps as $rowIdTmp) { $rowGroupId[] = 'row-group-' . $rowIdTmp; } - $rowGroupClass = ' class="depth-' . $blogCategory->depth . ' ' . implode(' ', $rowGroupId) . '"'; + $rowGroupClass = ' class="sortable depth-' . $blogCategory->depth . ' ' . implode(' ', $rowGroupId) . '"'; ?> depth ?> BcBaser->element('BlogCategories/index_row', ['blogCategory' => $blogCategory, 'rowGroupClass' => $rowGroupClass]) ?> diff --git a/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/BlogCategories/index_list_tree.php b/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/BlogCategories/index_list_tree.php new file mode 100644 index 0000000000..38f6e2a9c3 --- /dev/null +++ b/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/BlogCategories/index_list_tree.php @@ -0,0 +1,42 @@ + + * Copyright (c) NPO baser foundation + * + * @copyright Copyright (c) NPO baser foundation + * @link https://basercms.net baserCMS Project + * @since 5.0.0 + * @license https://basercms.net/license/index.html MIT License + */ + +/** + * [ADMIN] ブログカテゴリ 一覧 ツリー(再帰) + * @var BcBlog\View\BlogAdminAppView $this + * @var iterable $blogCategories 親子ネスト(children)付きのカテゴリ + * @var BcBlog\Model\Entity\BlogContent $blogContent + * @checked + * @noTodo + * @unitTest + */ +?> + diff --git a/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/BlogCategories/index_setup.php b/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/BlogCategories/index_setup.php new file mode 100644 index 0000000000..56f9b057c6 --- /dev/null +++ b/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/BlogCategories/index_setup.php @@ -0,0 +1,39 @@ + + * Copyright (c) NPO baser foundation + * + * @copyright Copyright (c) NPO baser foundation + * @link https://basercms.net baserCMS Project + * @since 5.0.0 + * @license https://basercms.net/license/index.html MIT License + */ + +/** + * [ADMIN] ブログカテゴリ 一覧 セットアップ(JS/CSS 読込) + * @var BcBlog\View\BlogAdminAppView $this + * @var BcBlog\Model\Entity\BlogContent $blogContent + * @var string $template + * @checked + * @noTodo + * @unitTest + */ +$this->BcBaser->i18nScript([ + 'blogCategoryConfirmDelete' => __d('baser_core', "カテゴリ「%s」を削除してもよろしいですか?\n※ このカテゴリに関連する記事は、どのカテゴリにも関連しない状態として残ります。"), + 'blogCategoryDeleteSuccess' => __d('baser_core', 'カテゴリを削除しました。'), + 'blogCategoryDeleteFailed' => __d('baser_core', '削除に失敗しました。'), + 'bcTreeCheck' => __d('baser_core', '確認'), + 'bcTreeEdit' => __d('baser_core', '編集'), + 'bcTreeAddChild' => __d('baser_core', '子カテゴリを追加'), + 'bcTreeDelete' => __d('baser_core', '削除'), +]); +$this->BcBaser->js('BcBlog.admin/blog_categories/index.bundle', false, [ + 'id' => 'AdminBlogCategoriesIndexScript', + 'data-blogContentId' => $blogContent->id, + 'data-listType' => ($template === 'index_tree') ? 1 : 2, + 'data-addUrl' => $this->BcBaser->getUrl(['action' => 'add', $blogContent->id]), +]); +if ($template === 'index_tree') { + $this->BcBaser->js(['vendor/jquery.jstree-3.3.8/jstree.min'], false); + $this->BcBaser->css('../js/vendor/jquery.jstree-3.3.8/themes/proton/style.min', false); +} diff --git a/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/BlogCategories/index_tree.php b/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/BlogCategories/index_tree.php new file mode 100644 index 0000000000..90f9a666fe --- /dev/null +++ b/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/BlogCategories/index_tree.php @@ -0,0 +1,30 @@ + + * Copyright (c) NPO baser foundation + * + * @copyright Copyright (c) NPO baser foundation + * @link https://basercms.net baserCMS Project + * @since 5.0.0 + * @license https://basercms.net/license/index.html MIT License + */ + +/** + * [ADMIN] ブログカテゴリ 一覧 ツリー表示 + * @var BcBlog\View\BlogAdminAppView $this + * @var \Cake\Datasource\ResultSetInterface $blogCategories + * @var BcBlog\Model\Entity\BlogContent $blogContent + * @checked + * @noTodo + * @unitTest + */ +?> + + +count()): ?> +
+ BcBaser->element('BlogCategories/index_list_tree', ['blogCategories' => $blogCategories, 'blogContent' => $blogContent]) ?> +
+ +
+ diff --git a/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/BlogCategories/index_view_setting.php b/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/BlogCategories/index_view_setting.php new file mode 100644 index 0000000000..9c8b86d31a --- /dev/null +++ b/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/BlogCategories/index_view_setting.php @@ -0,0 +1,43 @@ + + * Copyright (c) NPO baser foundation + * + * @copyright Copyright (c) NPO baser foundation + * @link https://basercms.net baserCMS Project + * @since 5.0.0 + * @license https://basercms.net/license/index.html MIT License + */ + +/** + * [ADMIN] ブログカテゴリ 一覧 表示切替 + * @var BcBlog\View\BlogAdminAppView $this + * @checked + * @noTodo + * @unitTest + */ +$listType = $this->request->getQuery('list_type') ?: 2; +?> + + +
+
+
+ + BcAdminForm->control('ViewSetting.list_type', [ + 'type' => 'radio', + 'options' => [ + 1 => __d('baser_core', 'ツリー形式'), + 2 => __d('baser_core', '表形式') + ], + 'value' => $listType, + 'hiddenField' => false + ]) ?> +
+
+ +
+
diff --git a/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/Utilities/blog_categories_tree.php b/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/Utilities/blog_categories_tree.php new file mode 100644 index 0000000000..a60fed62ad --- /dev/null +++ b/plugins/bc-admin-third/templates/plugin/BcBlog/Admin/element/Utilities/blog_categories_tree.php @@ -0,0 +1,37 @@ + + * Copyright (c) NPO baser foundation + * + * @copyright Copyright (c) NPO baser foundation + * @link https://basercms.net baserCMS Project + * @since 5.2.0 + * @license https://basercms.net/license/index.html MIT License + */ + +/** + * [ADMIN] ユーティリティ ブログカテゴリのツリー構造チェック・リセット + * @var \BaserCore\View\BcAdminAppView $this + * @checked + * @noTodo + * @unitTest + */ +?> + + +
+

+

+ +

+ BcAdminForm->postLink( + __d('baser_core', 'ツリー構造をチェックする'), + ['plugin' => 'BcBlog', 'controller' => 'BlogCategories', 'action' => 'verity_tree'], + ['class' => 'bca-btn'] + ) ?>   + BcAdminForm->postLink( + __d('baser_core', 'ツリー構造リセット'), + ['plugin' => 'BcBlog', 'controller' => 'BlogCategories', 'action' => 'reset_tree'], + ['class' => 'bca-btn', 'confirm' => __d('baser_core', 'ブログカテゴリのツリー構造をリセットします。本当によろしいですか?')] + ) ?> +
diff --git a/plugins/bc-admin-third/webroot/bc_blog/js/admin/blog_categories/index.bundle.js b/plugins/bc-admin-third/webroot/bc_blog/js/admin/blog_categories/index.bundle.js index 64fe184f96..b8a3f22887 100755 --- a/plugins/bc-admin-third/webroot/bc_blog/js/admin/blog_categories/index.bundle.js +++ b/plugins/bc-admin-third/webroot/bc_blog/js/admin/blog_categories/index.bundle.js @@ -7,5 +7,5 @@ * @since 5.0.0 * @license https://basercms.net/license/index.html MIT License */ -$(function(){$.bcBatch.init({batchUrl:$.bcUtil.apiAdminBaseUrl+"bc-blog/blog_categories/batch.json"})}); +$(function(){var e=$("#AdminBlogCategoriesIndexScript"),t=e.attr("data-blogContentId"),n=e.attr("data-listType"),o=e.attr("data-addUrl");function a(e){return e&&e.data?e.data.jstree:{}}function c(e){var t=a(e),n={};return t.previewUrl&&(n.view={label:bcI18n.bcTreeCheck,icon:"bca-icon--preview",action:function(){window.open(t.previewUrl,"_blank")}}),n.edit={label:bcI18n.bcTreeEdit,icon:"bca-icon--edit",action:function(){location.href=t.editUrl}},n.add={label:bcI18n.bcTreeAddChild,icon:"bca-icon--add",action:function(){location.href=o+"?parent_id="+t.categoryId}},n.delete={label:bcI18n.bcTreeDelete,icon:"bca-icon--delete",action:function(){!function(e,t){var n=$("#BlogCategoryTreeList").jstree(!0).get_text(e);confirm(bcI18n.blogCategoryConfirmDelete.replace("%s",n))&&$.bcToken.check(function(){return $.ajax({url:$.bcUtil.apiAdminBaseUrl+"bc-blog/blog_categories/delete/"+t.categoryId+".json",type:"POST",dataType:"json",data:{_csrfToken:$.bcToken.key},beforeSend:function(){$.bcUtil.hideMessage(),$.bcUtil.showLoader()},success:function(){location.reload()},error:function(e){var t="";e.responseText&&(t="
"+JSON.parse(e.responseText).message),$.bcUtil.showAlertMessage(bcI18n.blogCategoryDeleteFailed+t)},complete:function(){$.bcUtil.hideLoader()}})},{hideLoader:!1})}(e,t)}},n}"1"===n?(function(){var e=$("#BlogCategoryTreeList");if(e.length){e.jstree({core:{themes:{name:"proton",stripes:!0,variant:"large"},multiple:!1,force_text:!0,check_callback:!0},plugins:["dnd","state","wholerow","contextmenu","types"],types:{default:{}},state:{key:"blog-category-tree-"+t},dnd:{large_drop_target:!0,is_draggable:function(){return!0}},contextmenu:{show_at_node:!1,items:c}});var n=e.jstree(!0);$("#BtnOpenTree").click(function(){n.open_all()}),$("#BtnCloseTree").click(function(){n.close_all()}),e.on("move_node.jstree",function(e,t){!function(e,t){var n=t.node,o=a(n),c="";"#"!==t.parent&&(c=a(e.get_node(t.parent)).categoryId);var r=e.get_node(t.parent).children,i=r[t.position+1],l=i?a(e.get_node(i)).categoryId:"";$.bcToken.check(function(){return $.ajax({url:$.bcUtil.apiAdminBaseUrl+"bc-blog/blog_categories/move.json",type:"PATCH",dataType:"json",data:{origin:{id:o.categoryId,parentId:o.parentId},target:{id:l,parentId:c},_csrfToken:$.bcToken.key},beforeSend:function(){$.bcUtil.hideMessage(),$.bcUtil.showLoader()},success:function(){o.parentId=c},error:function(e,t,n){var o="";o=404===e.status?"
"+bcI18n.commonNotFoundProgramMessage:e.responseText?"
"+JSON.parse(e.responseText).message:"
"+n,$.bcUtil.showAlertMessage(bcI18n.commonBatchExecFailedMessage+"("+e.status+")"+o),location.reload()},complete:function(){$.bcUtil.hideLoader()}})},{hideLoader:!1})}(n,t)})}}(),$("#GrpChangeTreeOpenClose").show()):$.bcBatch.init({batchUrl:$.bcUtil.apiAdminBaseUrl+"bc-blog/blog_categories/batch.json"}),$("input[name='ViewSetting[list_type]']").change(function(){$.bcUtil.showLoader();var e=$("input[name='ViewSetting[list_type]']:checked").val();location.href=$.bcUtil.adminBaseUrl+"bc-blog/blog_categories/index/"+t+"?list_type="+e})}); //# sourceMappingURL=index.bundle.js.map \ No newline at end of file diff --git a/plugins/bc-admin-third/webroot/bc_blog/js/admin/blog_categories/index.bundle.js.map b/plugins/bc-admin-third/webroot/bc_blog/js/admin/blog_categories/index.bundle.js.map index 57f64c84f5..10d3836ae1 100755 --- a/plugins/bc-admin-third/webroot/bc_blog/js/admin/blog_categories/index.bundle.js.map +++ b/plugins/bc-admin-third/webroot/bc_blog/js/admin/blog_categories/index.bundle.js.map @@ -1 +1 @@ -{"version":3,"file":"bc_blog/js/admin/blog_categories/index.bundle.js","mappings":";;;;;;;;;AAUAA,EAAE,WAKEA,EAAEC,QAAQC,KAAK,CACXC,SAAUH,EAAEI,OAAOC,gBAAkB,sCAG7C","sources":["webpack://bc-admin-third/./src/bc_blog/js/admin/blog_categories/index.js"],"sourcesContent":["/**\n * baserCMS : Based Website Development Project \n * Copyright (c) NPO baser foundation \n *\n * @copyright Copyright (c) NPO baser foundation\n * @link https://basercms.net baserCMS Project\n * @since 5.0.0\n * @license https://basercms.net/license/index.html MIT License\n */\n\n$(function () {\n\n /**\n * 一括処理実装\n */\n $.bcBatch.init({\n batchUrl: $.bcUtil.apiAdminBaseUrl + 'bc-blog/blog_categories/batch.json'\n });\n\n});\n"],"names":["$","bcBatch","init","batchUrl","bcUtil","apiAdminBaseUrl"],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"bc_blog/js/admin/blog_categories/index.bundle.js","mappings":";;;;;;;;;AAUAA,EAAE,WAEE,IAAIC,EAAaD,EAAE,mCACfE,EAAgBD,EAAWE,KAAK,sBAChCC,EAAWH,EAAWE,KAAK,iBAC3BE,EAASJ,EAAWE,KAAK,eAyB7B,SAASG,EAASC,GACd,OAAQA,GAAQA,EAAKC,KAAQD,EAAKC,KAAKC,OAAS,CAAC,CACrD,CAgHA,SAASC,EAAiBH,GACtB,IAAIC,EAAOF,EAASC,GAChBI,EAAO,CAAC,EAwCZ,OArCIH,EAAKI,aACLD,EAAKE,KAAO,CACRC,MAAOC,OAAOC,YACdC,KAAM,oBACNC,OAAQ,WACJC,OAAOC,KAAKZ,EAAKI,WAAY,SACjC,IAKRD,EAAKU,KAAO,CACRP,MAAOC,OAAOO,WACdL,KAAM,iBACNC,OAAQ,WACJK,SAASC,KAAOhB,EAAKiB,OACzB,GAIJd,EAAKe,IAAM,CACPZ,MAAOC,OAAOY,eACdV,KAAM,gBACNC,OAAQ,WACJK,SAASC,KAAOnB,EAAS,cAAgBG,EAAKoB,UAClD,GAIJjB,EAAI,OAAU,CACVG,MAAOC,OAAOc,aACdZ,KAAM,mBACNC,OAAQ,YAahB,SAAwBX,EAAMC,GAC1B,IAAIsB,EAAQ9B,EAAE,yBAAyBS,QAAO,GAAMsB,SAASxB,GACxDyB,QAAQjB,OAAOkB,0BAA0BC,QAAQ,KAAMJ,KAG5D9B,EAAEmC,QAAQC,MAAM,WACZ,OAAOpC,EAAEqC,KAAK,CACVC,IAAKtC,EAAEuC,OAAOC,gBAAkB,kCAAoChC,EAAKoB,WAAa,QACtFa,KAAM,OACNC,SAAU,OACVlC,KAAM,CACFmC,WAAY3C,EAAEmC,QAAQS,KAE1BC,WAAY,WACR7C,EAAEuC,OAAOO,cACT9C,EAAEuC,OAAOQ,YACb,EACAC,QAAS,WAELzB,SAAS0B,QACb,EACAC,MAAO,SAAUC,GACb,IAAIC,EAAe,GACfD,EAAeE,eACfD,EAAe,OAASE,KAAKC,MAAMJ,EAAeE,cAAcG,SAEpExD,EAAEuC,OAAOkB,iBAAiB1C,OAAO2C,yBAA2BN,EAChE,EACAO,SAAU,WACN3D,EAAEuC,OAAOqB,YACb,GAER,EAAG,CAACA,YAAY,GACpB,CA7CYC,CAAetD,EAAMC,EACzB,GAGGG,CACX,CApLiB,MAAbP,GA8BJ,WACI,IAAI0D,EAAU9D,EAAE,yBAChB,GAAK8D,EAAQC,OAAb,CAIAD,EAAQrD,OAAO,CACX,KAAQ,CACJ,OAAU,CAAC,KAAQ,SAAU,SAAW,EAAM,QAAW,SACzD,UAAY,EACZ,YAAc,EAEd,gBAAkB,GAEtB,QAAW,CAAC,MAAO,QAAS,WAAY,cAAe,SACvD,MAAS,CAAC,QAAW,CAAC,GACtB,MAAS,CAAC,IAAO,sBAAwBP,GACzC,IAAO,CACH,mBAAqB,EACrB,aAAgB,WACZ,OAAO,CACX,GAEJ,YAAe,CACX,cAAgB,EAChB,MAASQ,KAIjB,IAAIsD,EAAYF,EAAQrD,QAAO,GAG/BT,EAAE,gBAAgBiE,MAAM,WACpBD,EAAUE,UACd,GACAlE,EAAE,iBAAiBiE,MAAM,WACrBD,EAAUG,WACd,GAGAL,EAAQM,GAAG,mBAAoB,SAAUC,EAAG7D,IAUhD,SAAuBwD,EAAWxD,GAC9B,IAAID,EAAOC,EAAKD,KACZ+D,EAAShE,EAASC,GAGlBgE,EAAiB,GACD,MAAhB/D,EAAKgE,SACLD,EAAiBjE,EAAS0D,EAAUS,SAASjE,EAAKgE,SAAS5C,YAG/D,IAAI8C,EAAiBV,EAAUS,SAASjE,EAAKgE,QAAQG,SACjDC,EAAaF,EAAelE,EAAKqE,SAAW,GAC5CC,EAAWF,EAAatE,EAAS0D,EAAUS,SAASG,IAAahD,WAAa,GAElF5B,EAAEmC,QAAQC,MAAM,WACZ,OAAOpC,EAAEqC,KAAK,CACVC,IAAKtC,EAAEuC,OAAOC,gBAAkB,oCAChCC,KAAM,QACNC,SAAU,OACVlC,KAAM,CACF8D,OAAQ,CAACS,GAAIT,EAAO1C,WAAYoD,SAAUV,EAAOU,UACjDC,OAAQ,CAACF,GAAID,EAAUE,SAAUT,GACjC5B,WAAY3C,EAAEmC,QAAQS,KAE1BC,WAAY,WACR7C,EAAEuC,OAAOO,cACT9C,EAAEuC,OAAOQ,YACb,EACAC,QAAS,WAELsB,EAAOU,SAAWT,CACtB,EACArB,MAAO,SAAUC,EAAgB+B,EAAYC,GAEzC,IAAI/B,EAAe,GAEfA,EAD0B,MAA1BD,EAAeiC,OACA,OAASrE,OAAOsE,6BACxBlC,EAAeE,aACP,OAASC,KAAKC,MAAMJ,EAAeE,cAAcG,QAEjD,OAAS2B,EAE5BnF,EAAEuC,OAAOkB,iBAAiB1C,OAAOuE,6BAA+B,IAAMnC,EAAeiC,OAAS,IAAMhC,GACpG7B,SAAS0B,QACb,EACAU,SAAU,WACN3D,EAAEuC,OAAOqB,YACb,GAER,EAAG,CAACA,YAAY,GACpB,CA3DQ2B,CAAcvB,EAAWxD,EAC7B,EAtCA,CAuCJ,CAvEIgF,GACAxF,EAAE,2BAA2ByF,QAG7BzF,EAAE0F,QAAQC,KAAK,CACXC,SAAU5F,EAAEuC,OAAOC,gBAAkB,uCAK7CxC,EAAE,wCAAwC6F,OAAO,WAC7C7F,EAAEuC,OAAOQ,aACT,IAAI+C,EAAW9F,EAAE,gDAAgD+F,MACjExE,SAASC,KAAOxB,EAAEuC,OAAOyD,aAAe,iCAAmC9F,EAAgB,cAAgB4F,CAC/G,EA8MJ","sources":["webpack://bc-admin-third/./src/bc_blog/js/admin/blog_categories/index.js"],"sourcesContent":["/**\n * baserCMS : Based Website Development Project \n * Copyright (c) NPO baser foundation \n *\n * @copyright Copyright (c) NPO baser foundation\n * @link https://basercms.net baserCMS Project\n * @since 5.0.0\n * @license https://basercms.net/license/index.html MIT License\n */\n\n$(function () {\n\n var scriptData = $(\"#AdminBlogCategoriesIndexScript\");\n var blogContentId = scriptData.attr('data-blogContentId');\n var listType = scriptData.attr('data-listType');\n var addUrl = scriptData.attr('data-addUrl');\n\n if (listType === '1') {\n // ツリー形式\n initTree();\n $(\"#GrpChangeTreeOpenClose\").show();\n } else {\n // 表形式:一括処理\n $.bcBatch.init({\n batchUrl: $.bcUtil.apiAdminBaseUrl + 'bc-blog/blog_categories/batch.json'\n });\n }\n\n // 表示形式の切り替え(ツリー形式 / 表形式)\n $(\"input[name='ViewSetting[list_type]']\").change(function () {\n $.bcUtil.showLoader();\n var selected = $(\"input[name='ViewSetting[list_type]']:checked\").val();\n location.href = $.bcUtil.adminBaseUrl + 'bc-blog/blog_categories/index/' + blogContentId + '?list_type=' + selected;\n });\n\n /**\n * ノードに紐づくブログカテゴリのデータ(data-jstree)を返す\n * @param node\n * @returns {object}\n */\n function nodeData(node) {\n return (node && node.data) ? node.data.jstree : {};\n }\n\n /**\n * ツリー(jstree)の初期化\n */\n function initTree() {\n var treeDom = $('#BlogCategoryTreeList');\n if (!treeDom.length) {\n return;\n }\n\n treeDom.jstree({\n 'core': {\n 'themes': {'name': 'proton', 'stripes': true, 'variant': 'large'},\n 'multiple': false,\n 'force_text': true,\n // 自分自身の子孫への移動は jstree 側が禁止する。それ以外は任意の親へ移動可。\n 'check_callback': true\n },\n 'plugins': ['dnd', 'state', 'wholerow', 'contextmenu', 'types'],\n 'types': {'default': {}},\n 'state': {'key': 'blog-category-tree-' + blogContentId},\n 'dnd': {\n 'large_drop_target': true,\n 'is_draggable': function () {\n return true;\n }\n },\n 'contextmenu': {\n 'show_at_node': false,\n 'items': buildContextMenu\n }\n });\n\n var jstreeApi = treeDom.jstree(true);\n\n // 展開・折りたたみ\n $(\"#BtnOpenTree\").click(function () {\n jstreeApi.open_all();\n });\n $(\"#BtnCloseTree\").click(function () {\n jstreeApi.close_all();\n });\n\n // ドラッグ&ドロップによる移動(並び替え・再親付け)\n treeDom.on('move_node.jstree', function (e, data) {\n orderCategory(jstreeApi, data);\n });\n }\n\n /**\n * ドラッグ&ドロップ完了時に、移動内容をサーバへ保存する\n * @param jstreeApi\n * @param data move_node.jstree のイベントデータ\n */\n function orderCategory(jstreeApi, data) {\n var node = data.node;\n var origin = nodeData(node);\n\n // 移動先の親カテゴリ(ルートは空)\n var targetParentId = '';\n if (data.parent !== '#') {\n targetParentId = nodeData(jstreeApi.get_node(data.parent)).categoryId;\n }\n // 移動先で、自分の直後にくる兄弟(=この上に配置する対象)。無ければ末尾。\n var parentChildren = jstreeApi.get_node(data.parent).children;\n var nextNodeId = parentChildren[data.position + 1];\n var targetId = nextNodeId ? nodeData(jstreeApi.get_node(nextNodeId)).categoryId : '';\n\n $.bcToken.check(function () {\n return $.ajax({\n url: $.bcUtil.apiAdminBaseUrl + 'bc-blog/blog_categories/move.json',\n type: 'PATCH',\n dataType: 'json',\n data: {\n origin: {id: origin.categoryId, parentId: origin.parentId},\n target: {id: targetId, parentId: targetParentId},\n _csrfToken: $.bcToken.key\n },\n beforeSend: function () {\n $.bcUtil.hideMessage();\n $.bcUtil.showLoader();\n },\n success: function () {\n // 移動先の親を記憶し、続けての移動に備える\n origin.parentId = targetParentId;\n },\n error: function (XMLHttpRequest, textStatus, errorThrown) {\n // サーバ状態は変わっていないため、再読込で元の並びへ戻す\n var errorMessage = '';\n if (XMLHttpRequest.status === 404) {\n errorMessage = '
' + bcI18n.commonNotFoundProgramMessage;\n } else if (XMLHttpRequest.responseText) {\n errorMessage = '
' + JSON.parse(XMLHttpRequest.responseText).message;\n } else {\n errorMessage = '
' + errorThrown;\n }\n $.bcUtil.showAlertMessage(bcI18n.commonBatchExecFailedMessage + '(' + XMLHttpRequest.status + ')' + errorMessage);\n location.reload();\n },\n complete: function () {\n $.bcUtil.hideLoader();\n }\n });\n }, {hideLoader: false});\n }\n\n /**\n * 右クリックメニューを構築する\n * @param node\n * @returns {object}\n */\n function buildContextMenu(node) {\n var data = nodeData(node);\n var menu = {};\n\n // 確認(フロントのカテゴリ一覧を別タブで開く)\n if (data.previewUrl) {\n menu.view = {\n label: bcI18n.bcTreeCheck,\n icon: 'bca-icon--preview',\n action: function () {\n window.open(data.previewUrl, '_blank');\n }\n };\n }\n\n // 編集\n menu.edit = {\n label: bcI18n.bcTreeEdit,\n icon: 'bca-icon--edit',\n action: function () {\n location.href = data.editUrl;\n }\n };\n\n // 子カテゴリを追加\n menu.add = {\n label: bcI18n.bcTreeAddChild,\n icon: 'bca-icon--add',\n action: function () {\n location.href = addUrl + '?parent_id=' + data.categoryId;\n }\n };\n\n // 削除\n menu.delete = {\n label: bcI18n.bcTreeDelete,\n icon: 'bca-icon--delete',\n action: function () {\n deleteCategory(node, data);\n }\n };\n\n return menu;\n }\n\n /**\n * カテゴリを削除する\n * @param node\n * @param data\n */\n function deleteCategory(node, data) {\n var title = $(\"#BlogCategoryTreeList\").jstree(true).get_text(node);\n if (!confirm(bcI18n.blogCategoryConfirmDelete.replace('%s', title))) {\n return;\n }\n $.bcToken.check(function () {\n return $.ajax({\n url: $.bcUtil.apiAdminBaseUrl + 'bc-blog/blog_categories/delete/' + data.categoryId + '.json',\n type: 'POST',\n dataType: 'json',\n data: {\n _csrfToken: $.bcToken.key\n },\n beforeSend: function () {\n $.bcUtil.hideMessage();\n $.bcUtil.showLoader();\n },\n success: function () {\n // 削除後はサーバの状態に合わせて再読込する\n location.reload();\n },\n error: function (XMLHttpRequest) {\n var errorMessage = '';\n if (XMLHttpRequest.responseText) {\n errorMessage = '
' + JSON.parse(XMLHttpRequest.responseText).message;\n }\n $.bcUtil.showAlertMessage(bcI18n.blogCategoryDeleteFailed + errorMessage);\n },\n complete: function () {\n $.bcUtil.hideLoader();\n }\n });\n }, {hideLoader: false});\n }\n\n});\n"],"names":["$","scriptData","blogContentId","attr","listType","addUrl","nodeData","node","data","jstree","buildContextMenu","menu","previewUrl","view","label","bcI18n","bcTreeCheck","icon","action","window","open","edit","bcTreeEdit","location","href","editUrl","add","bcTreeAddChild","categoryId","bcTreeDelete","title","get_text","confirm","blogCategoryConfirmDelete","replace","bcToken","check","ajax","url","bcUtil","apiAdminBaseUrl","type","dataType","_csrfToken","key","beforeSend","hideMessage","showLoader","success","reload","error","XMLHttpRequest","errorMessage","responseText","JSON","parse","message","showAlertMessage","blogCategoryDeleteFailed","complete","hideLoader","deleteCategory","treeDom","length","jstreeApi","click","open_all","close_all","on","e","origin","targetParentId","parent","get_node","parentChildren","children","nextNodeId","position","targetId","id","parentId","target","textStatus","errorThrown","status","commonNotFoundProgramMessage","commonBatchExecFailedMessage","orderCategory","initTree","show","bcBatch","init","batchUrl","change","selected","val","adminBaseUrl"],"sourceRoot":""} \ No newline at end of file diff --git a/plugins/bc-blog/config/permission.php b/plugins/bc-blog/config/permission.php index 6f50120ced..e1f7ac40bf 100755 --- a/plugins/bc-blog/config/permission.php +++ b/plugins/bc-blog/config/permission.php @@ -28,6 +28,8 @@ 'Add' => ['title' => __d('baser_core', '新規追加'), 'url' => '/baser/admin/bc-blog/blog_categories/add/*', 'method' => 'POST', 'auth' => true], 'Edit' => ['title' => __d('baser_core', '編集'), 'url' => '/baser/admin/bc-blog/blog_categories/edit/*', 'method' => 'POST', 'auth' => true], 'Delete' => ['title' => __d('baser_core', '削除'), 'url' => '/baser/admin/bc-blog/blog_categories/delete/*', 'method' => 'POST', 'auth' => true], + 'VerityTree' => ['title' => __d('baser_core', 'ツリー構造チェック'), 'url' => '/baser/admin/bc-blog/blog_categories/verity_tree', 'method' => 'POST', 'auth' => false], + 'ResetTree' => ['title' => __d('baser_core', 'ツリー構造リセット'), 'url' => '/baser/admin/bc-blog/blog_categories/reset_tree', 'method' => 'POST', 'auth' => false], ] ], @@ -93,6 +95,7 @@ 'Edit' => ['title' => __d('baser_core', '編集'), 'url' => '/baser/api/admin/bc-blog/blog_categories/edit/*.json', 'method' => 'POST', 'auth' => true], 'Index' => ['title' => __d('baser_core', '一覧取得'), 'url' => '/baser/api/admin/bc-blog/blog_categories/index/*.json', 'method' => 'GET', 'auth' => true], 'List' => ['title' => __d('baser_core', 'リスト取得'), 'url' => '/baser/api/admin/bc-blog/blog_categories/list/*.json', 'method' => 'GET', 'auth' => true], + 'Move' => ['title' => __d('baser_core', '移動'), 'url' => '/baser/api/admin/bc-blog/blog_categories/move.json', 'method' => 'POST', 'auth' => true], 'View' => ['title' => __d('baser_core', '単一取得'), 'url' => '/baser/api/admin/bc-blog/blog_categories/view/*.json', 'method' => 'GET', 'auth' => true], ] ], diff --git a/plugins/bc-blog/src/Controller/Admin/BlogCategoriesController.php b/plugins/bc-blog/src/Controller/Admin/BlogCategoriesController.php index 72f1736e34..29033a0848 100755 --- a/plugins/bc-blog/src/Controller/Admin/BlogCategoriesController.php +++ b/plugins/bc-blog/src/Controller/Admin/BlogCategoriesController.php @@ -41,7 +41,12 @@ public function index( int $blogContentId ) { - $this->set($service->getViewVarsForIndex($blogContentId)); + // 表示形式(list_type)をセッションに保持し、追加・削除後のリダイレクトでも維持する + $this->setViewConditions('BlogCategories', ['default' => ['query' => [ + 'list_type' => 2, + ]]]); + $listType = $this->getRequest()->getQuery('list_type'); + $this->set($service->getViewVarsForIndex($blogContentId, $listType !== null ? (int)$listType : null)); } /** @@ -79,9 +84,10 @@ public function add(BlogCategoriesAdminServiceInterface $service, string $blogCo $this->BcMessage->setError(__d('baser_core', '入力エラーです。内容を修正してください。')); } } + $parentId = $this->getRequest()->getQuery('parent_id'); $this->set($service->getViewVarsForAdd( $blogContentId, - $blogCategory ?? $service->getNew($blogContentId) + $blogCategory ?? $service->getNew((int)$blogContentId, $parentId? (int)$parentId : null) )); } @@ -152,4 +158,44 @@ public function delete(BlogCategoriesServiceInterface $service, int $blogContent $this->redirect(['action' => 'index', $blogContentId]); } + /** + * [ADMIN] ブログカテゴリのツリー構造のチェックを行う + * + * 問題がある場合にはログを出力する + * + * @param BlogCategoriesServiceInterface $service + * @checked + * @noTodo + * @unitTest + */ + public function verity_tree(BlogCategoriesServiceInterface $service) + { + $this->request->allowMethod(['post']); + if (!$service->verityTree()) { + $this->BcMessage->setError(__d('baser_core', 'ブログカテゴリのツリー構造に問題があります。ログを確認してください。')); + } else { + $this->BcMessage->setSuccess(__d('baser_core', 'ブログカテゴリのツリー構造に問題はありません。'), false); + } + $this->redirect(['plugin' => 'BaserCore', 'controller' => 'Utilities', 'action' => 'index']); + } + + /** + * [ADMIN] ブログカテゴリのツリー構造をリセットする + * + * @param BlogCategoriesServiceInterface $service + * @checked + * @noTodo + * @unitTest + */ + public function reset_tree(BlogCategoriesServiceInterface $service) + { + $this->request->allowMethod(['post']); + if ($service->resetTree()) { + $this->BcMessage->setSuccess(__d('baser_core', 'ブログカテゴリのツリー構造をリセットしました。')); + } else { + $this->BcMessage->setError(__d('baser_core', 'ブログカテゴリのツリー構造のリセットに失敗しました。')); + } + $this->redirect(['plugin' => 'BaserCore', 'controller' => 'Utilities', 'action' => 'index']); + } + } diff --git a/plugins/bc-blog/src/Controller/Api/Admin/BlogCategoriesController.php b/plugins/bc-blog/src/Controller/Api/Admin/BlogCategoriesController.php index ec6093a384..88b1d805fa 100755 --- a/plugins/bc-blog/src/Controller/Api/Admin/BlogCategoriesController.php +++ b/plugins/bc-blog/src/Controller/Api/Admin/BlogCategoriesController.php @@ -278,4 +278,43 @@ public function delete(BlogCategoriesServiceInterface $service, $blogCategoryId) ]); $this->viewBuilder()->setOption('serialize', ['blogCategory', 'message']); } + + /** + * [API] カテゴリの配置を移動する(並び替え・再親付け) + * + * @param BlogCategoriesServiceInterface $service + * @checked + * @noTodo + * @unitTest + */ + public function move(BlogCategoriesServiceInterface $service) + { + $this->request->allowMethod(['post', 'put', 'patch']); + $blogCategory = $errors = null; + try { + $blogCategory = $service->move( + $this->request->getData('origin'), + $this->request->getData('target') + ); + $message = __d('baser_core', 'ブログカテゴリー「{0}」の配置を移動しました。', $blogCategory->title); + $this->BcMessage->setSuccess($message, true, false); + } catch (PersistenceFailedException $e) { + $errors = $e->getEntity()->getErrors(); + $message = __d('baser_core', "入力エラーです。内容を修正してください。"); + $this->setResponse($this->response->withStatus(400)); + } catch (RecordNotFoundException $e) { + $this->setResponse($this->response->withStatus(404)); + $message = __d('baser_core', 'データが見つかりません。'); + } catch (\Throwable $e) { + $message = __d('baser_core', 'データベース処理中にエラーが発生しました。' . $e->getMessage()); + $this->setResponse($this->response->withStatus(500)); + } + + $this->set([ + 'message' => $message, + 'blogCategory' => $blogCategory, + 'errors' => $errors + ]); + $this->viewBuilder()->setOption('serialize', ['message', 'blogCategory', 'errors']); + } } diff --git a/plugins/bc-blog/src/Event/BcBlogViewEventListener.php b/plugins/bc-blog/src/Event/BcBlogViewEventListener.php index 23c8dc1d73..59fda6ce17 100755 --- a/plugins/bc-blog/src/Event/BcBlogViewEventListener.php +++ b/plugins/bc-blog/src/Event/BcBlogViewEventListener.php @@ -31,9 +31,27 @@ class BcBlogViewEventListener extends \BaserCore\Event\BcViewEventListener */ public $events = [ 'beforeRender', - 'leftOfToolbar' + 'leftOfToolbar', + 'afterUtilitiesIndex' ]; + /** + * afterUtilitiesIndex + * + * ユーティリティ画面にブログカテゴリのツリー構造チェック・リセットのセクションを追加する + * + * @param Event $event + * @checked + * @noTodo + * @unitTest + */ + public function afterUtilitiesIndex(Event $event) + { + if (!BcUtil::isAdminSystem()) return; + $view = $event->getSubject(); + echo $view->element('BcBlog.Utilities/blog_categories_tree'); + } + /** * leftOfToolbar * @param Event $event diff --git a/plugins/bc-blog/src/Model/Table/BlogCategoriesTable.php b/plugins/bc-blog/src/Model/Table/BlogCategoriesTable.php index 481b537d27..d75ea9d86b 100755 --- a/plugins/bc-blog/src/Model/Table/BlogCategoriesTable.php +++ b/plugins/bc-blog/src/Model/Table/BlogCategoriesTable.php @@ -486,4 +486,111 @@ public function getParent($parent_id) { return $this->find()->where(['id' => $parent_id])->first(); } + + /** + * 同一階層(同じ親)における並び順を取得する + * + * 指定した親配下(親なしはルート)のカテゴリを lft 昇順に並べ、対象 id の 1 始まりの順位を返す。 + * id が空の場合は件数(=末尾の位置)を返す。該当データが無い場合は false、 + * id に一致するカテゴリが無い場合は null。 + * + * TreeBehavior はスコープを持たず lft/rght は全ブログ横断の単一フォレストで管理されるため、 + * ルート階層(親なし)では全ブログのルートカテゴリを対象とした順位を返す。 + * moveUp()/moveDown() が実際に移動する兄弟の並びと一致させるための仕様。 + * + * @param int|null $id 対象カテゴリID(null で末尾の位置=件数) + * @param int|null $parentId 親カテゴリID(null でルート) + * @return int|false|null + * @checked + * @noTodo + * @unitTest + */ + public function getOrderSameParent($id, $parentId) + { + if ($parentId) { + $conditions = ['BlogCategories.parent_id' => $parentId]; + } else { + $conditions = ['BlogCategories.parent_id IS' => null]; + } + $categories = $this->find() + ->select(['BlogCategories.id', 'BlogCategories.parent_id', 'BlogCategories.title']) + ->where($conditions) + ->orderBy('BlogCategories.lft'); + if ($categories->all()->isEmpty()) { + return false; + } + if (!$id) { + return $categories->all()->count(); + } + $order = null; + foreach ($categories as $key => $category) { + if ($id == $category->id) { + $order = $key + 1; + break; + } + } + return $order; + } + + /** + * オフセットを元にカテゴリを移動する + * + * TreeBehavior により、同一階層(兄弟)内で指定位置数だけ上下に移動する。 + * オフセットが正の場合は下へ、負の場合は上へ移動し、0 の場合は何もしない。 + * 子カテゴリを持つ場合は、その子孫も含めて移動する。 + * + * @param int $id 移動対象のカテゴリID + * @param int $offset 移動する位置数(正: 下、負: 上) + * @return EntityInterface|bool 成功時は対象エンティティ、失敗時は false + * @checked + * @noTodo + * @unitTest + */ + public function moveOffset(int $id, int $offset) + { + $category = $this->get($id); + if ($offset > 0) { + $result = $this->moveDown($category, abs($offset)); + } elseif ($offset < 0) { + $result = $this->moveUp($category, abs($offset)); + } else { + $result = true; + } + return $result ? $category : false; + } + + /** + * ブログカテゴリのツリー構造をリセットする + * + * コンテンツ管理のリセットと同様に、全てのカテゴリをルート直下にフラット化し、 + * lft / rght を現在の並び順(lft 昇順)で振り直す。 + * + * @return bool + * @checked + * @noTodo + * @unitTest + */ + public function resetTree(): bool + { + $this->removeBehavior('Tree'); + $this->getConnection()->begin(); + $result = true; + $categories = $this->find()->orderBy(['BlogCategories.lft' => 'ASC'])->all(); + $count = 0; + foreach($categories as $category) { + $count++; + $category->parent_id = null; + $category->lft = $count; + $count++; + $category->rght = $count; + if (!$this->save($category)) $result = false; + } + $this->addBehavior('Tree'); + if (!$result) { + $this->getConnection()->rollback(); + return false; + } + $this->getConnection()->commit(); + return true; + } } diff --git a/plugins/bc-blog/src/Service/Admin/BlogCategoriesAdminService.php b/plugins/bc-blog/src/Service/Admin/BlogCategoriesAdminService.php index 43417a8486..6cc27680f1 100755 --- a/plugins/bc-blog/src/Service/Admin/BlogCategoriesAdminService.php +++ b/plugins/bc-blog/src/Service/Admin/BlogCategoriesAdminService.php @@ -36,18 +36,31 @@ class BlogCategoriesAdminService extends BlogCategoriesService implements BlogCa * ブログカテゴリ一覧用の view 変数取得 * * @param int $blogContentId + * @param int|null $listType 表示形式(1: ツリー形式 / 2: 表形式。null は表形式) * @return array * @noTodo * @checked * @unitTest */ - public function getViewVarsForIndex(int $blogContentId) + public function getViewVarsForIndex(int $blogContentId, ?int $listType = null) { $blogContentsService = $this->getService(BlogContentsServiceInterface::class); - return [ + $templates = [1 => 'index_tree', 2 => 'index_list']; + $listType = ($listType && isset($templates[$listType])) ? $listType : 2; + $vars = [ 'blogContent' => $blogContentsService->get($blogContentId), - 'blogCategories' => $this->getTreeIndex($blogContentId, []) + 'template' => $templates[$listType], ]; + if ($listType === 1) { + // ツリー形式: 親子ネスト(children)付きで取得 + $vars['blogCategories'] = $this->getIndex($blogContentId, [], 'threaded') + ->orderBy(['BlogCategories.lft']) + ->all(); + } else { + // 表形式: 従来どおり階層をインデント表示したフラット配列 + $vars['blogCategories'] = $this->getTreeIndex($blogContentId, []); + } + return $vars; } /** diff --git a/plugins/bc-blog/src/Service/Admin/BlogCategoriesAdminServiceInterface.php b/plugins/bc-blog/src/Service/Admin/BlogCategoriesAdminServiceInterface.php index 321a1434af..8e0f1a3d2d 100755 --- a/plugins/bc-blog/src/Service/Admin/BlogCategoriesAdminServiceInterface.php +++ b/plugins/bc-blog/src/Service/Admin/BlogCategoriesAdminServiceInterface.php @@ -24,14 +24,15 @@ interface BlogCategoriesAdminServiceInterface /** * ブログカテゴリ一覧用の view 変数取得 - * + * * @param int $blogContentId + * @param int|null $listType 表示形式(1: ツリー形式 / 2: 表形式。null は表形式) * @return array * @noTodo * @checked * @unitTest */ - public function getViewVarsForIndex(int $blogContentId); + public function getViewVarsForIndex(int $blogContentId, ?int $listType = null); /** * ブログカテゴリー登録用の view 変数取得 diff --git a/plugins/bc-blog/src/Service/BlogCategoriesService.php b/plugins/bc-blog/src/Service/BlogCategoriesService.php index eaebcd46e5..3d539d3345 100755 --- a/plugins/bc-blog/src/Service/BlogCategoriesService.php +++ b/plugins/bc-blog/src/Service/BlogCategoriesService.php @@ -12,6 +12,8 @@ namespace BcBlog\Service; use BaserCore\Error\BcException; +use BaserCore\Service\UtilitiesServiceInterface; +use BaserCore\Utility\BcContainerTrait; use BcBlog\Model\Entity\BlogCategory; use BcBlog\Model\Table\BlogCategoriesTable; use Cake\Datasource\EntityInterface; @@ -30,6 +32,11 @@ class BlogCategoriesService implements BlogCategoriesServiceInterface { + /** + * Trait + */ + use BcContainerTrait; + /** * @var BlogCategoriesTable|Table */ @@ -232,16 +239,18 @@ public function getControlSource(string $field, array $options): mixed * 新規エンティティ取得 * * @param int $blogContentId + * @param int|null $parentId 親カテゴリID(子カテゴリとして追加する場合に指定) * @return EntityInterface * @checked * @noTodo * @unitTest */ - public function getNew(int $blogContentId): EntityInterface + public function getNew(int $blogContentId, ?int $parentId = null): EntityInterface { return $this->BlogCategories->newEntity([ 'blog_content_id' => $blogContentId, - 'status' => true + 'status' => true, + 'parent_id' => $parentId ], [ 'validate' => false, ]); @@ -369,4 +378,91 @@ public function getList($blogContentId, array $queryParams = []): array if ($blogContentId) $conditions = ['BlogCategories.blog_content_id' => $blogContentId]; return $query->where($conditions)->toArray(); } + + /** + * カテゴリの配置を移動する(並び替え・再親付け) + * + * ツリー上のドラッグ&ドロップに対応する。基本的に target.id の上に移動する前提で、 + * target.id が空の場合は同じ親の中の一番下に配置する。親が変わる場合は先に parent_id を + * 付け替え(TreeBehavior が末尾へ再配置)、その後オフセットで目標位置まで移動する。 + * 移動元の親IDは DB 上のエンティティから取得するため、origin.parentId は利用しない。 + * + * @param array $origin 移動元 ['id' => int](parentId は後方互換のため受け付けるが利用しない) + * @param array $target 移動先 ['id' => int|null, 'parentId' => int|null] + * @return EntityInterface|bool + * @checked + * @noTodo + * @unitTest + */ + public function move(array $origin, array $target) + { + if (!$this->BlogCategories->exists(['BlogCategories.id' => $origin['id']])) { + throw new BcException(__d('baser_core', 'データが存在しません。')); + } + + $category = $this->get($origin['id']); + $blogContentId = $category->blog_content_id; + // 親変更の判定はリクエスト値に依存せず、DB 上の現在の親IDを利用する + $originParentId = $category->parent_id? (int)$category->parent_id : null; + $targetParentId = $target['parentId']? (int)$target['parentId'] : null; + $targetId = $target['id']? (int)$target['id'] : null; + + $targetSort = $this->BlogCategories->getOrderSameParent($targetId, $targetParentId); + if ($originParentId !== $targetParentId) { + // 親を変更(再親付け) + $category = $this->update($category, [ + 'id' => $origin['id'], + 'name' => $category->name, + 'title' => $category->title, + 'parent_id' => $targetParentId, + 'blog_content_id' => $blogContentId, + ]); + // 移動先に子が無い、または target 未指定(末尾指定)の場合は末尾配置で終了 + if (!$targetSort || !$targetId) { + return $category; + } + $currentSort = $this->BlogCategories->getOrderSameParent(null, $targetParentId); + } else { + $currentSort = $this->BlogCategories->getOrderSameParent($origin['id'], $targetParentId); + } + + // 親変更後のオフセットを算出 + $offset = $targetSort - $currentSort; + if ($originParentId === $targetParentId && $targetId && $offset > 0) { + $offset--; + } + return $this->BlogCategories->moveOffset($origin['id'], $offset); + } + + /** + * ブログカテゴリのツリー構造をチェックする + * + * 問題がある場合にはログを出力する + * + * @return bool + * @checked + * @noTodo + * @unitTest + */ + public function verityTree(): bool + { + /** @var UtilitiesServiceInterface $utilitiesService */ + $utilitiesService = $this->getService(UtilitiesServiceInterface::class); + return $utilitiesService->verityTree($this->BlogCategories); + } + + /** + * ブログカテゴリのツリー構造をリセットする + * + * 全てのカテゴリをルート直下にフラット化し、lft / rght を振り直す。 + * + * @return bool + * @checked + * @noTodo + * @unitTest + */ + public function resetTree(): bool + { + return $this->BlogCategories->resetTree(); + } } diff --git a/plugins/bc-blog/src/Service/BlogCategoriesServiceInterface.php b/plugins/bc-blog/src/Service/BlogCategoriesServiceInterface.php index 89de1fd3ff..b65c79c66f 100755 --- a/plugins/bc-blog/src/Service/BlogCategoriesServiceInterface.php +++ b/plugins/bc-blog/src/Service/BlogCategoriesServiceInterface.php @@ -76,12 +76,13 @@ public function getControlSource(string $field, array $options): mixed; * 新規エンティティ取得 * * @param int $blogContentId + * @param int|null $parentId 親カテゴリID(子カテゴリとして追加する場合に指定) * @return EntityInterface * @checked * @noTodo * @unitTest */ - public function getNew(int $blogContentId): EntityInterface; + public function getNew(int $blogContentId, ?int $parentId = null): EntityInterface; /** * 新規作成 @@ -151,4 +152,42 @@ public function getNamesById($ids): array; * @unitTest */ public function getList($blogContentId, array $queryParams = []): array; + + /** + * カテゴリの配置を移動する(並び替え・再親付け) + * + * 移動元の親IDは DB 上のエンティティから取得する + * + * @param array $origin 移動元 ['id' => int](parentId は後方互換のため受け付けるが利用しない) + * @param array $target 移動先 ['id' => int|null, 'parentId' => int|null] + * @return EntityInterface|bool + * @checked + * @noTodo + * @unitTest + */ + public function move(array $origin, array $target); + + /** + * ブログカテゴリのツリー構造をチェックする + * + * 問題がある場合にはログを出力する + * + * @return bool + * @checked + * @noTodo + * @unitTest + */ + public function verityTree(): bool; + + /** + * ブログカテゴリのツリー構造をリセットする + * + * 全てのカテゴリをルート直下にフラット化し、lft / rght を振り直す。 + * + * @return bool + * @checked + * @noTodo + * @unitTest + */ + public function resetTree(): bool; } diff --git a/plugins/bc-blog/tests/TestCase/Controller/Admin/BlogCategoriesControllerTest.php b/plugins/bc-blog/tests/TestCase/Controller/Admin/BlogCategoriesControllerTest.php index f8e6e02b7a..50b5384741 100755 --- a/plugins/bc-blog/tests/TestCase/Controller/Admin/BlogCategoriesControllerTest.php +++ b/plugins/bc-blog/tests/TestCase/Controller/Admin/BlogCategoriesControllerTest.php @@ -15,9 +15,11 @@ use BaserCore\Test\Scenario\InitAppScenario; use BaserCore\TestSuite\BcTestCase; use BcBlog\Controller\Admin\BlogCategoriesController; +use BcBlog\Event\BcBlogViewEventListener; use BcBlog\Test\Factory\BlogCategoryFactory; use BcBlog\Test\Factory\BlogContentFactory; use Cake\Event\Event; +use Cake\Event\EventManager; use Cake\ORM\TableRegistry; use Cake\TestSuite\IntegrationTestTrait; use CakephpFixtureFactories\Scenario\ScenarioAwareTrait; @@ -73,10 +75,23 @@ public function testAdmin_index() { BlogContentFactory::make(['id' => 1, 'eye_catch_size' => 'YTo0OntzOjExOiJ0aHVtYl93aWR0aCI7czozOiIzMDAiO3M6MTI6InRodW1iX2hlaWdodCI7czozOiIzMDAiO3M6MTg6Im1vYmlsZV90aHVtYl93aWR0aCI7czozOiIxMDAiO3M6MTk6Im1vYmlsZV90aHVtYl9oZWlnaHQiO3M6MzoiMTAwIjt9'])->persist(); ContentFactory::make(['id' => 1, 'entity_id' => 1, 'type' => 'BlogContent', 'plugin' => 'BcBlog'])->persist(); + BlogCategoryFactory::make(['id' => 1, 'blog_content_id' => 1, 'name' => 'release', 'title' => 'プレスリリース', 'status' => 1, 'lft' => 1, 'rght' => 2])->persist(); $this->get('/baser/admin/bc-blog/blog_categories/index'); $this->assertResponseError(); + // 表形式(デフォルト) $this->get('/baser/admin/bc-blog/blog_categories/index/1'); $this->assertResponseOk(); + // ツリー形式 + $this->get('/baser/admin/bc-blog/blog_categories/index/1?list_type=1'); + $this->assertResponseOk(); + $this->assertResponseContains('id="BlogCategoryTreeList"'); + // 表示形式はセッションに保存される + $this->assertSession('1', 'BcApp.viewConditions.BlogCategoriesIndex.query.list_type'); + // セッションに保持された表示形式は、クエリなしのアクセスでも復元される + $this->session(['BcApp' => ['viewConditions' => ['BlogCategoriesIndex' => ['query' => ['list_type' => 1]]]]]); + $this->get('/baser/admin/bc-blog/blog_categories/index/1'); + $this->assertResponseOk(); + $this->assertResponseContains('id="BlogCategoryTreeList"'); } /** @@ -180,6 +195,73 @@ public function testAdmin_add() $this->assertResponseContains('入力エラーです。内容を修正してください。'); } + /** + * [ADMIN] ブログカテゴリのツリー構造チェック + */ + public function testAdmin_verity_tree() + { + $this->enableSecurityToken(); + $this->enableCsrfToken(); + // 正常なツリー + BlogCategoryFactory::make(['id' => 1, 'blog_content_id' => 1, 'name' => 'release', 'title' => 'プレスリリース', 'parent_id' => null, 'lft' => 1, 'rght' => 2])->persist(); + $this->post('/baser/admin/bc-blog/blog_categories/verity_tree'); + $this->assertResponseCode(302); + $this->assertRedirect(['plugin' => 'BaserCore', 'prefix' => 'Admin', 'controller' => 'Utilities', 'action' => 'index']); + $this->assertFlashMessage('ブログカテゴリのツリー構造に問題はありません。'); + + // 破損したツリー + BlogCategoryFactory::make(['id' => 2, 'blog_content_id' => 1, 'name' => 'broken', 'title' => 'B', 'parent_id' => null, 'lft' => 2, 'rght' => 1])->persist(); + $this->post('/baser/admin/bc-blog/blog_categories/verity_tree'); + $this->assertResponseCode(302); + $this->assertFlashMessage('ブログカテゴリのツリー構造に問題があります。ログを確認してください。'); + } + + /** + * [ADMIN] ブログカテゴリのツリー構造リセット + */ + public function testAdmin_reset_tree() + { + $this->enableSecurityToken(); + $this->enableCsrfToken(); + BlogCategoryFactory::make(['id' => 1, 'blog_content_id' => 1, 'name' => 'parent1', 'title' => 'P1', 'parent_id' => null, 'lft' => 1, 'rght' => 4])->persist(); + BlogCategoryFactory::make(['id' => 2, 'blog_content_id' => 1, 'name' => 'child1', 'title' => 'C1', 'parent_id' => 1, 'lft' => 2, 'rght' => 3])->persist(); + $this->post('/baser/admin/bc-blog/blog_categories/reset_tree'); + $this->assertResponseCode(302); + $this->assertRedirect(['plugin' => 'BaserCore', 'prefix' => 'Admin', 'controller' => 'Utilities', 'action' => 'index']); + $this->assertFlashMessage('ブログカテゴリのツリー構造をリセットしました。'); + // 全てルート直下にフラット化されている + $blogCategories = $this->getTableLocator()->get('BcBlog.BlogCategories'); + $this->assertEquals(2, $blogCategories->find()->where(['parent_id IS' => null])->count()); + } + + /** + * [ADMIN] ユーティリティ画面にブログカテゴリのツリー構造チェック・リセットが表示される + */ + public function testUtilitiesIndexContainsBlogCategoriesTreeSection() + { + // BcTestCase が EventManager をリセットするため、プラグイン読込時に + // 自動登録されるリスナーをテストでは明示的に登録する + EventManager::instance()->on(new BcBlogViewEventListener()); + $this->get('/baser/admin/baser-core/utilities/index'); + $this->assertResponseOk(); + $this->assertResponseContains('blog_categories/verity_tree'); + $this->assertResponseContains('blog_categories/reset_tree'); + } + + /** + * [ADMIN] 登録処理(子カテゴリを追加:parent_id クエリの反映) + */ + public function testAdmin_add_withParentId() + { + BlogContentFactory::make(['id' => 1, 'eye_catch_size' => 'YTo0OntzOjExOiJ0aHVtYl93aWR0aCI7czozOiIzMDAiO3M6MTI6InRodW1iX2hlaWdodCI7czozOiIzMDAiO3M6MTg6Im1vYmlsZV90aHVtYl93aWR0aCI7czozOiIxMDAiO3M6MTk6Im1vYmlsZV90aHVtYl9oZWlnaHQiO3M6MzoiMTAwIjt9'])->persist(); + ContentFactory::make(['id' => 1, 'entity_id' => 1, 'type' => 'BlogContent', 'plugin' => 'BcBlog'])->persist(); + BlogCategoryFactory::make(['id' => 1, 'blog_content_id' => 1, 'name' => 'release', 'title' => 'プレスリリース', 'status' => 1, 'lft' => 1, 'rght' => 2])->persist(); + // クエリで指定した親カテゴリが初期選択される + $this->get('/baser/admin/bc-blog/blog_categories/add/1?parent_id=1'); + $this->assertResponseOk(); + $this->assertResponseRegExp('/