From 95eaac81230d07af14962e02e7cc36595b4744f4 Mon Sep 17 00:00:00 2001 From: Bogdanova Olga Date: Thu, 25 Feb 2021 15:49:21 +0300 Subject: [PATCH 1/4] Issues-438: fixed rendring temoplates --- .../applications/vanilla/js/categoryfilter.js | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 vanilla/applications/vanilla/js/categoryfilter.js diff --git a/vanilla/applications/vanilla/js/categoryfilter.js b/vanilla/applications/vanilla/js/categoryfilter.js new file mode 100644 index 0000000..734458d --- /dev/null +++ b/vanilla/applications/vanilla/js/categoryfilter.js @@ -0,0 +1,262 @@ +(function(window, $, document) { + + /** + * Given a template string and an object, replaces any property name contained in curly braces `{}` with the + * corresponding value in the json object. If the property doesn't exist, replaces the property name with an + * empty string. Will only replace the values passed as strings in the replacements array. + * + * @param {Object. } json The object that contains the values we're replacing. + * @param {string} template The template string with the variables to replace. + * @param {string[]} replacements The replacement properties to look for. + * @returns {string} A string with the replacement values replaced. + */ + var renderTemplate = function(json, template, replacements) { + var result = template; + for (var i = 0; i < replacements.length; ++i) { + if (json[replacements[i]] === undefined) { + json[replacements[i]] = ""; + } + if(replacements[i] ==='NameHTML') { + result = result.replace('{escapeHTML(' + replacements[i] + ')}', json[replacements[i]]); + } else { + result = result.replace('{' + replacements[i] + '}', json[replacements[i]]); + } + } + return result; + }; + + /** + * Takes an object of `key: value` attributes and outputs an attributes string to add to an HTML tag. + * + * @param {Object. } attributes A `key: value` object of attributes to transform into a string. + * @returns {string} An string representation of the attributes. + */ + var attrToString = function(attributes) { + if (attributes === undefined) { + return ''; + } + var attrStr = ''; + Object.keys(attributes).forEach(function(key) { + attrStr += key + '="' + attributes[key] + '" '; + }); + + return attrStr; + }; + + /** + * Takes an array of options (retrieved using the CategoriesController's getOptions function) and outputs + * an HTML string representing the options dropdown. + * + * @param {Object. } options The options to render. + * @returns {string} + */ + var categoryOptionsToString = function(options) { + + var itemTemplate = ' \ + \ + {icon}{text} \ + '; + + var headerTemplate = ' \ + '; + + var dropdownTemplate = ' \ + '; + + var menuItems = ''; + var items = options.items; + + for (var key in items) { + if (items.hasOwnProperty(key)) { + var group = items[key]; + if (group.text !== "") { + menuItems += renderTemplate(group, headerTemplate, ['text']); + } + var groupItems = items[key].items; + for (var key in groupItems) { + if (groupItems.hasOwnProperty(key)) { + var item = groupItems[key]; + if (typeof item === 'object') { + item.attributes = attrToString(item.attributes); + var properties = ['cssClass', 'icon', 'text', 'url', 'attributes']; + menuItems += renderTemplate(item, itemTemplate, properties); + } + } + } + + // If this isn't the last item in the list... + if (key !== Object.keys(items)[Object.keys(items).length-1]) { + menuItems += ''; + } + } + } + + options.items = menuItems; + options.text = options.trigger.text; + options.categoryID = options.trigger.attributes['data-id']; + return renderTemplate(options, dropdownTemplate, ['text', 'items', 'categoryID']); + }; + + /** + * Transforms an input box into a category filter box. The input should set the following data attributes: + * + * data-category-id: Which parent category to filter children for. + * data-container: The selector for the container that we display the results in. + * data-hide-container: OPTIONAL If we're toggling showing and hiding categories based on the existance of a + * filter input, set this to be the selector for the container we should hide when the filter exists. + * + * @param {jQuery} $input The form input wrapped in a jQuery object. + */ + var categoryFilter = function($input) { + var categories; + var filteredCategories; + var hideContainerSelector = $input.data('hideContainer'); + var categoryID = $input.data('categoryId'); + var containerSelector = $input.data('container'); + + if (!containerSelector) { + containerSelector = '.category-filter-container'; + } + + var categoryOptions = ' \ +
\ + {Options} \ +
'; + + var categoryTemplate = ' \ +
  • \ +
    \ + {escapeHTML(NameHTML)} \ +
    \ + '+ categoryOptions + '\ +
  • '; + + /** + * Renders the HTML for any category in our filtered list and displays in the container. + */ + var renderCategories = function() { + var replacements = ['NameHTML', 'CategoryID', 'Options']; + $(containerSelector).html(''); + var html = ''; + filteredCategories.forEach(function(category) { + if (category['DisplayAs'] === 'Categories' || category['DisplayAs'] === 'Flat') { + // Wrap in an anchor + category['NameHTML'] = ' \ + \ + ' + category["Name"] + ' \ + '; + } else { + category['NameHTML'] = category['Name']; + } + html += renderTemplate(category, categoryTemplate, replacements); + }); + $(containerSelector).html(html) + }; + + /** + * Filters the categories list for categories whose names contain the filter string. + * Stores these categories in the filteredCategories array. + * + * @param {string} filter The filter to filter category names by. + */ + var filterCategories = function(filter) { + if (filter === undefined) { + filteredCategories = categories; + return; + } + filteredCategories = []; + if (categories !== undefined) { + for (var i = 0; i < categories.length; ++i) { + var name = categories[i]['Name'].toLowerCase(); + filter = filter.toLowerCase(); + if (name.indexOf(filter) !== -1) { + filteredCategories.push(categories[i]); + } + } + } + }; + + /** + * Toggles the display of the container and hide container, if one exists. + */ + var hideContainers = function() { + if (hideContainerSelector !== undefined) { + if ($input.val() === '') { + $(containerSelector).hide(); + $(hideContainerSelector).show(); + } else { + $(containerSelector).show(); + $(hideContainerSelector).hide(); + } + } + }; + + /** + * Updates the category options with new HTML. + * + * @param {Object. } data An object containing the category ID and the new HTML for the options. + */ + var updateCategoryOptionsText = function(data) { + categories.forEach(function(category) { + if (category.CategoryID === data.categoryID) { + category.Options = data.options; + } + }); + }; + + /** + * Do the things we do on page load or when we get a new filter. + * + * @param filter + */ + var go = function(filter) { + filterCategories(filter); + renderCategories(); + }; + + hideContainers(); + + /** + * Fetch the categories. Just once. + */ + (function() { + // fetch our categories + jQuery.get( + gdn.url("categories/getflattenedchildren/" + categoryID), + function(json, textStatus, jqXHR) { + categories = json.Categories; + for (var i = 0; i < categories.length; ++i) { + categories[i].Options = categoryOptionsToString(categories[i].Options); + } + go(); + }, + 'json' + ); + })(); + + $input.on('keyup', function(filterEvent) { + go(filterEvent.target.value); + hideContainers(); + }); + + $(document).on('updateDisplayAs', function(e, data) { + updateCategoryOptionsText(data); + }); + }; + + $(document).on('contentLoad', function(e) { + $(".js-category-filter-input", e.target).each(function() { + categoryFilter($(this)); + }); + }); + +})(window, jQuery, document); From 664bc908f48191c5e4e844ef69730deb20193b51 Mon Sep 17 00:00:00 2001 From: Bogdanova Olga Date: Thu, 25 Feb 2021 16:22:37 +0300 Subject: [PATCH 2/4] Moved filter to CatgeoryModel --- .../class.categoriescontroller.php | 10 +--- .../vanilla/models/class.categorymodel.php | 46 ++++++++++++------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/vanilla/applications/vanilla/controllers/class.categoriescontroller.php b/vanilla/applications/vanilla/controllers/class.categoriescontroller.php index 37782bc..82dab55 100644 --- a/vanilla/applications/vanilla/controllers/class.categoriescontroller.php +++ b/vanilla/applications/vanilla/controllers/class.categoriescontroller.php @@ -140,14 +140,8 @@ private function getCategoryTree($category = null, $displayAs = null, $recent = $perPage = c('Vanilla.Categories.PerPage', 30); $page = Gdn::request()->get('Page', Gdn::request()->get('page', null)); list($offset, $limit) = offsetLimit($page, $perPage); - - $filter = []; - if(Gdn::session()->isValid()) { - $filter['UserID'] = Gdn::session()->UserID; - $filter['isAdmin'] = Gdn::session()->User->Admin; - } - $categoryTree = $this->CategoryModel->getTreeAsFlat($categoryIdentifier, $offset, $limit,$filter, 'c.DateInserted', 'desc'); - $countOfCategoryTree = $this->CategoryModel->countOfCategories($categoryIdentifier,$filter); + $categoryTree = $this->CategoryModel->getTreeAsFlat($categoryIdentifier, $offset, $limit,null, 'c.DateInserted', 'desc'); + $countOfCategoryTree = $this->CategoryModel->countOfCategories($categoryIdentifier, null); $this->setData('_Limit', $perPage); $this->setData('_RecordCount', $countOfCategoryTree); $this->setData('_CurrentRecords', count($categoryTree)); diff --git a/vanilla/applications/vanilla/models/class.categorymodel.php b/vanilla/applications/vanilla/models/class.categorymodel.php index bdd27c8..73c0f8c 100644 --- a/vanilla/applications/vanilla/models/class.categorymodel.php +++ b/vanilla/applications/vanilla/models/class.categorymodel.php @@ -1084,18 +1084,20 @@ public function getTreeAsFlat($id, $offset = null, $limit = null, $filter = null { $query = $this->SQL->from('Category c'); - if(!$filter) { - if (Gdn::session()->isValid()) { - $filter = []; - $filter['UserID'] = Gdn::session()->UserID; - $filter['isAdmin'] = Gdn::session()->User->Admin; - } + $filters = []; + if ($filter && is_string($filter)) { + $filters['Name']= $filter; + } + + if (Gdn::session()->isValid()) { + $filters['UserID'] = Gdn::session()->UserID; + $filters['isAdmin'] = Gdn::session()->User->Admin; } //FIX: https://github.com/topcoder-platform/forums/issues/422 - if (!val('isAdmin', $filter, false)) { - if (val('UserID', $filter, false)) { - $userID = val('UserID', $filter); + if (!val('isAdmin', $filters, false)) { + if (val('UserID', $filters, false)) { + $userID = val('UserID', $filters); $query-> leftJoin('UserGroup ug', 'c.GroupID = ug.GroupID') ->beginWhereGroup() @@ -1113,8 +1115,8 @@ public function getTreeAsFlat($id, $offset = null, $limit = null, $filter = null $query->limit($limit, $offset) ->orderBy($orderFields, $orderDirection); - if ($filter && is_string($filter)) { - $query->like('Name', $filter); + if (!val('Name', $filters, false)) { + $query->like('Name', $filters['Name']); } $categories = $query->get()->resultArray(); @@ -1131,13 +1133,24 @@ public function getTreeAsFlat($id, $offset = null, $limit = null, $filter = null * */ public function countOfCategories($id, $filter = null) { + $filters = []; + if ($filter && is_string($filter)) { + $filters['Name']= $filter; + } + + if (Gdn::session()->isValid()) { + $filters['UserID'] = Gdn::session()->UserID; + $filters['isAdmin'] = Gdn::session()->User->Admin; + } + $query = $this->SQL ->select('c.CategoryID', 'count', 'Count') ->from('Category c'); - if (!val('isAdmin', $filter, false)) { - if (val('UserID', $filter, false)) { - $userID = val('UserID', $filter); + + if (!val('isAdmin', $filters, false)) { + if (val('UserID', $filters, false)) { + $userID = val('UserID', $filters); $query-> leftJoin('UserGroup ug', 'c.GroupID = ug.GroupID') ->beginWhereGroup() @@ -1152,9 +1165,10 @@ public function countOfCategories($id, $filter = null) { $query->where('DisplayAs <>', 'Heading') ->where('ParentCategoryID', $id); - if ($filter && is_string($filter)) { - $query->like('Name', $filter); + if (!val('Name', $filters, false)) { + $query->like('Name', $filters['Name']); } + $count = $query->get() ->firstRow()->Count; return $count; From da80ac1baa1269cfc99db46f163160a83fe8db80 Mon Sep 17 00:00:00 2001 From: Bogdanova Olga Date: Thu, 25 Feb 2021 17:10:16 +0300 Subject: [PATCH 3/4] Issues-439: show New discussion if categoty type=discussions --- .../vanilla/controllers/class.categoriescontroller.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/vanilla/applications/vanilla/controllers/class.categoriescontroller.php b/vanilla/applications/vanilla/controllers/class.categoriescontroller.php index 82dab55..b80a752 100644 --- a/vanilla/applications/vanilla/controllers/class.categoriescontroller.php +++ b/vanilla/applications/vanilla/controllers/class.categoriescontroller.php @@ -410,8 +410,10 @@ public function index($categoryIdentifier = '', $page = '0') { $this->Head->addRss(categoryUrl($category) . '/feed.rss', $this->Head->title()); } - // Add modules - $this->addModule('NewDiscussionModule'); + if($category->DisplayAs == 'Discussions') { + // Add modules + $this->addModule('NewDiscussionModule'); + } $this->addModule('DiscussionFilterModule'); // $this->addModule('CategoriesModule'); $this->addModule('BookmarkedModule'); @@ -642,7 +644,7 @@ public function all($Category = '', $displayAs = '') { $this->setData('CategoryTree', $categoryTree); // Add modules - if($Category) { + if($Category && $displayAs == 'Discussions') { $this->addModule('NewDiscussionModule'); } $this->addModule('DiscussionFilterModule'); From 2b50cd40c62971b8aaac3801ac375de63d05018b Mon Sep 17 00:00:00 2001 From: Bogdanova Olga Date: Thu, 25 Feb 2021 19:29:08 +0300 Subject: [PATCH 4/4] Issues-415: custom breadcrumbs for challenge discussions --- .../class.categoriescontroller.php | 12 ++------- .../class.discussioncontroller.php | 4 +-- .../controllers/class.postcontroller.php | 3 +-- .../controllers/class.vanillacontroller.php | 25 +++++++++++++++++++ 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/vanilla/applications/vanilla/controllers/class.categoriescontroller.php b/vanilla/applications/vanilla/controllers/class.categoriescontroller.php index b80a752..17ace55 100644 --- a/vanilla/applications/vanilla/controllers/class.categoriescontroller.php +++ b/vanilla/applications/vanilla/controllers/class.categoriescontroller.php @@ -34,7 +34,6 @@ class CategoriesController extends VanillaController { const SORT_LAST_POST = 'new'; const SORT_OLDEST_POST = 'old'; - const ROOT_CATEGORY = ['Name' => 'Roundtables', 'Url'=>'/']; /** * @var \Closure $categoriesCompatibilityCallback A backwards-compatible callback to get `$this->data('Categories')`. */ @@ -335,11 +334,7 @@ public function index($categoryIdentifier = '', $page = '0') { } // Load the breadcrumbs. - - $ancestors = CategoryModel::getAncestors(val('CategoryID', $category)); - array_unshift ( $ancestors , self::ROOT_CATEGORY); - $this->setData('Breadcrumbs', $ancestors); - + $this->setData('Breadcrumbs', $this->buildBreadcrumbs(val('CategoryID', $category))); $this->setData('Category', $category, true); // Set CategoryID @@ -553,10 +548,7 @@ public function all($Category = '', $displayAs = '') { $this->description(c('Garden.Description', null)); } - $ancestors = CategoryModel::getAncestors(val('CategoryID', $this->data('Category'))); - array_unshift ( $ancestors , self::ROOT_CATEGORY); - $this->setData('Breadcrumbs', $ancestors); - + $this->setData('Breadcrumbs', $this->buildBreadcrumbs(val('CategoryID', $this->data('Category')))); // Set the category follow toggle before we load category data so that it affects the category query appropriately. $CategoryFollowToggleModule = new CategoryFollowToggleModule($this); diff --git a/vanilla/applications/vanilla/controllers/class.discussioncontroller.php b/vanilla/applications/vanilla/controllers/class.discussioncontroller.php index e5cfc55..c65e48e 100644 --- a/vanilla/applications/vanilla/controllers/class.discussioncontroller.php +++ b/vanilla/applications/vanilla/controllers/class.discussioncontroller.php @@ -125,9 +125,7 @@ public function index($DiscussionID = '', $DiscussionStub = '', $Page = '') { Gdn_Theme::section($CategoryCssClass); } - $ancestors = CategoryModel::getAncestors($this->CategoryID); - array_unshift ( $ancestors , CategoriesController::ROOT_CATEGORY); - $this->setData('Breadcrumbs', $ancestors); + $this->setData('Breadcrumbs', $this->buildBreadcrumbs($this->CategoryID)); // Setup $this->title($this->Discussion->Name); diff --git a/vanilla/applications/vanilla/controllers/class.postcontroller.php b/vanilla/applications/vanilla/controllers/class.postcontroller.php index ed33d7f..0ed9cbe 100644 --- a/vanilla/applications/vanilla/controllers/class.postcontroller.php +++ b/vanilla/applications/vanilla/controllers/class.postcontroller.php @@ -409,7 +409,7 @@ public function discussion($categoryUrlCode = '') { $this->fireEvent('BeforeDiscussionRender'); if ($this->CategoryID) { - $breadcrumbs = CategoryModel::getAncestors($this->CategoryID); + $breadcrumbs = $this->buildBreadcrumbs($this->CategoryID); } else { $breadcrumbs = []; } @@ -419,7 +419,6 @@ public function discussion($categoryUrlCode = '') { 'Url' => val('AddUrl', val($this->data('Type'), DiscussionModel::discussionTypes()), '/post/discussion') ]; - array_unshift ( $breadcrumbs , CategoriesController::ROOT_CATEGORY); $this->setData('Breadcrumbs', $breadcrumbs); // FIX: Hide Announce options diff --git a/vanilla/applications/vanilla/controllers/class.vanillacontroller.php b/vanilla/applications/vanilla/controllers/class.vanillacontroller.php index e54ef97..ea46859 100644 --- a/vanilla/applications/vanilla/controllers/class.vanillacontroller.php +++ b/vanilla/applications/vanilla/controllers/class.vanillacontroller.php @@ -13,6 +13,8 @@ */ class VanillaController extends Gdn_Controller { + const ROOT_CATEGORY = ['Name' => 'Roundtables', 'Url'=>'/']; + /** * Include JS, CSS, and modules used by all methods. * @@ -70,6 +72,29 @@ protected function checkPageRange(int $offset, int $totalCount) { } } + protected function buildBreadcrumbs($CategoryID) { + $Category = CategoryModel::categories($CategoryID); + $ancestors = CategoryModel::getAncestors($CategoryID); + if(val('GroupID', $Category) > 0) { + $temp = []; + foreach ($ancestors as $id => $ancestor) { + if($ancestor['GroupID'] > 0) { + $temp[$ancestor['CategoryID']] = $ancestor; + } else { + if($ancestor['UrlCode'] == 'challenges-forums') { + array_push($temp, ['Name' => 'Challenge Discussions', 'Url'=>'/groups/mine?filter=challenge']); + }else if($ancestor['UrlCode'] == 'groups') { + array_push($temp, ['Name' => 'Group Discussions', 'Url'=>'/groups/mine?filter=regular']); + } + } + } + return $temp; + } else { + array_unshift($ancestors, self::ROOT_CATEGORY); + return $ancestors; + } + } + protected function log($message, $context = [], $level = Logger::DEBUG) { // if(c('Debug')) { Logger::log($level, sprintf('%s : %s',get_class($this), $message), $context);