From 534d27568af50aadba97a129042e480d00fae1d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Ch=C3=A1vez?= Date: Thu, 9 May 2024 18:39:56 -0500 Subject: [PATCH] feat: Tagging UX refinements - refresh tag count on edit (#34059) * style: drawer-cover color updated for all tagging drawers * feat: Update TagList component when a tag is updated on Manage tags drawer * feat: Refactor TagCount to be able to refresh the count * feat: Sync tag count in units --- .../contentstore/tests/test_contentstore.py | 6 +- cms/djangoapps/contentstore/views/block.py | 1 + cms/djangoapps/contentstore/views/preview.py | 1 + .../contentstore/views/tests/test_block.py | 8 +- cms/static/js/factories/tag_count.js | 13 ++++ cms/static/js/models/tag_count.js | 13 ++++ cms/static/js/views/course_outline.js | 27 +++++-- cms/static/js/views/pages/container.js | 1 + .../js/views/pages/container_subviews.js | 77 +++++++++++++++++++ cms/static/js/views/tag_count.js | 54 +++++++++++++ cms/static/sass/elements/_drawer.scss | 4 + cms/templates/container.html | 2 +- cms/templates/course_outline.html | 6 +- cms/templates/js/course-outline.underscore | 13 +--- cms/templates/js/tag-count.underscore | 7 ++ cms/templates/studio_xblock_wrapper.html | 23 ++++-- webpack.common.config.js | 1 + 17 files changed, 219 insertions(+), 38 deletions(-) create mode 100644 cms/static/js/factories/tag_count.js create mode 100644 cms/static/js/models/tag_count.js create mode 100644 cms/static/js/views/tag_count.js create mode 100644 cms/templates/js/tag-count.underscore diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index c871e8146e26..a63cb066c3f9 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -49,7 +49,6 @@ delete_course, reverse_course_url, reverse_url, - get_taxonomy_tags_widget_url, ) from cms.djangoapps.contentstore.views.component import ADVANCED_COMPONENT_TYPES from common.djangoapps.course_action_state.managers import CourseActionStateItemNotFoundError @@ -1381,14 +1380,11 @@ def test_course_overview_view_with_course(self): self.assertEqual(resp.status_code, 404) return - taxonomy_tags_widget_url = get_taxonomy_tags_widget_url(course.id) - self.assertContains( resp, - '
'.format( # lint-amnesty, pylint: disable=line-too-long + '
'.format( # lint-amnesty, pylint: disable=line-too-long locator=str(course.location), course_key=str(course.id), - taxonomy_tags_widget_url=taxonomy_tags_widget_url, ), status_code=200, html=True diff --git a/cms/djangoapps/contentstore/views/block.py b/cms/djangoapps/contentstore/views/block.py index 896e740fab0b..6f7b879c994d 100644 --- a/cms/djangoapps/contentstore/views/block.py +++ b/cms/djangoapps/contentstore/views/block.py @@ -1375,6 +1375,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F xblock_info["tags"] = tags if use_tagging_taxonomy_list_page(): xblock_info["taxonomy_tags_widget_url"] = get_taxonomy_tags_widget_url() + xblock_info["course_authoring_url"] = settings.COURSE_AUTHORING_MICROFRONTEND_URL if course_outline: if xblock_info['has_explicit_staff_lock']: diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index acd530631d66..55d81bb5d462 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -315,6 +315,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): 'can_edit': can_edit, 'enable_copy_paste': enable_copy_paste, 'can_edit_visibility': context.get('can_edit_visibility', xblock.scope_ids.usage_id.context_key.is_course), + 'course_authoring_url': settings.COURSE_AUTHORING_MICROFRONTEND_URL, 'selected_groups_label': selected_groups_label, 'can_add': context.get('can_add', True), 'can_move': context.get('can_move', xblock.scope_ids.usage_id.context_key.is_course), diff --git a/cms/djangoapps/contentstore/views/tests/test_block.py b/cms/djangoapps/contentstore/views/tests/test_block.py index 737a8b1e8ffb..dd51e8f7a001 100644 --- a/cms/djangoapps/contentstore/views/tests/test_block.py +++ b/cms/djangoapps/contentstore/views/tests/test_block.py @@ -258,15 +258,9 @@ def test_tag_count_in_container_fragment(self, mock_get_object_tag_counts): self.assertEqual(resp.status_code, 200) usage_key = self.response_usage_key(resp) - # Get the preview HTML without tags - mock_get_object_tag_counts.return_value = {} - html, __ = self._get_container_preview(root_usage_key) - self.assertIn("wrapper-xblock", html) - self.assertNotIn('data-testid="tag-count-button"', html) - # Get the preview HTML with tags mock_get_object_tag_counts.return_value = { - str(usage_key): 13 + str(usage_key): 13, } html, __ = self._get_container_preview(root_usage_key) self.assertIn("wrapper-xblock", html) diff --git a/cms/static/js/factories/tag_count.js b/cms/static/js/factories/tag_count.js new file mode 100644 index 000000000000..cadcfa220f1a --- /dev/null +++ b/cms/static/js/factories/tag_count.js @@ -0,0 +1,13 @@ +import * as TagCountView from 'js/views/tag_count'; +import * as TagCountModel from 'js/models/tag_count'; + +// eslint-disable-next-line no-unused-expressions +'use strict'; +export default function TagCountFactory(TagCountJson, el) { + var model = new TagCountModel(TagCountJson, {parse: true}); + var tagCountView = new TagCountView({el, model}); + tagCountView.setupMessageListener(); + tagCountView.render(); +} + +export {TagCountFactory}; diff --git a/cms/static/js/models/tag_count.js b/cms/static/js/models/tag_count.js new file mode 100644 index 000000000000..7007dfc9dc30 --- /dev/null +++ b/cms/static/js/models/tag_count.js @@ -0,0 +1,13 @@ +define(['backbone', 'underscore'], function(Backbone, _) { + /** + * Model for Tag count view + */ + var TagCountModel = Backbone.Model.extend({ + defaults: { + content_id: null, + tags_count: 0, + course_authoring_url: null, + }, + }); + return TagCountModel; +}); diff --git a/cms/static/js/views/course_outline.js b/cms/static/js/views/course_outline.js index 61ce21423aa1..322a421ae7a5 100644 --- a/cms/static/js/views/course_outline.js +++ b/cms/static/js/views/course_outline.js @@ -10,10 +10,10 @@ */ define(['jquery', 'underscore', 'js/views/xblock_outline', 'common/js/components/utils/view_utils', 'js/views/utils/xblock_utils', 'js/models/xblock_outline_info', 'js/views/modals/course_outline_modals', 'js/utils/drag_and_drop', - 'js/views/utils/tagging_drawer_utils',], + 'js/views/utils/tagging_drawer_utils', 'js/views/tag_count', 'js/models/tag_count'], function( $, _, XBlockOutlineView, ViewUtils, XBlockViewUtils, - XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger, TaggingDrawerUtils + XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger, TaggingDrawerUtils, TagCountView, TagCountModel ) { var CourseOutlineView = XBlockOutlineView.extend({ // takes XBlockOutlineInfo as a model @@ -23,9 +23,28 @@ function( render: function() { var renderResult = XBlockOutlineView.prototype.render.call(this); this.makeContentDraggable(this.el); + this.renderTagCount(); return renderResult; }, + renderTagCount: function() { + const contentId = this.model.get('id'); + const tagCountsByUnit = this.model.get('tag_counts_by_unit') + const tagsCount = tagCountsByUnit !== undefined ? tagCountsByUnit[contentId] : 0 + var countModel = new TagCountModel({ + content_id: contentId, + tags_count: tagsCount, + course_authoring_url: this.model.get('course_authoring_url'), + }, {parse: true}); + var tagCountView = new TagCountView({el: this.$('.tag-count'), model: countModel}); + tagCountView.setupMessageListener(); + tagCountView.render(); + this.$('.tag-count').click((event) => { + event.preventDefault(); + this.openManageTagsDrawer(); + }); + }, + shouldExpandChildren: function() { return this.expandedLocators.contains(this.model.get('id')); }, @@ -217,10 +236,8 @@ function( }, openManageTagsDrawer() { - const article = document.querySelector('[data-taxonomy-tags-widget-url]'); - const taxonomyTagsWidgetUrl = $(article).attr('data-taxonomy-tags-widget-url'); + const taxonomyTagsWidgetUrl = this.model.get('taxonomy_tags_widget_url'); const contentId = this.model.get('id'); - TaggingDrawerUtils.openDrawer(taxonomyTagsWidgetUrl, contentId); }, diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js index fbea10e00349..2deb934efceb 100644 --- a/cms/static/js/views/pages/container.js +++ b/cms/static/js/views/pages/container.js @@ -98,6 +98,7 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView el: this.$('.unit-tags'), model: this.model }); + this.tagListView.setupMessageListener(); this.tagListView.render(); this.unitOutlineView = new UnitOutlineView({ diff --git a/cms/static/js/views/pages/container_subviews.js b/cms/static/js/views/pages/container_subviews.js index 74a012732a76..a9f32030d25f 100644 --- a/cms/static/js/views/pages/container_subviews.js +++ b/cms/static/js/views/pages/container_subviews.js @@ -322,6 +322,83 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, H } }, + setupMessageListener: function () { + window.addEventListener( + "message", (event) => { + // Listen any message from Manage tags drawer. + var data = event.data; + var courseAuthoringUrl = this.model.get("course_authoring_url") + if (event.origin == courseAuthoringUrl + && data.includes('[Manage tags drawer] Tags updated:')) { + // This message arrives when there is a change in the tag list. + // The message contains the new list of tags. + let jsonData = data.replace(/\[Manage tags drawer\] Tags updated: /g, ""); + jsonData = JSON.parse(jsonData); + if (jsonData.contentId == this.model.id) { + this.model.set('tags', this.buildTaxonomyTree(jsonData)); + this.render(); + } + } + }, + ); + }, + + buildTaxonomyTree: function(data) { + // TODO We can use this function for the initial request of tags + // and avoid to use two functions (see get_unit_tags on contentstore/views/component.py) + + var taxonomyList = []; + var totalCount = 0; + var actualId = 0; + data.taxonomies.forEach((taxonomy) => { + // Build a tag tree for each taxonomy + var rootTagsValues = []; + var tags = {}; + taxonomy.tags.forEach((tag) => { + // Creates the tags for all the lineage of this tag + for (let i = tag.lineage.length - 1; i >= 0; i--){ + var tagValue = tag.lineage[i] + var tagProcessedBefore = tags.hasOwnProperty(tagValue); + if (!tagProcessedBefore) { + tags[tagValue] = { + id: actualId, + value: tagValue, + children: [], + } + actualId++; + if (i == 0) { + rootTagsValues.push(tagValue); + } + } + if (i !== tag.lineage.length - 1) { + // Add a child into the children list + tags[tagValue].children.push(tags[tag.lineage[i + 1]]) + } + if (tagProcessedBefore) { + // Break this loop if the tag has been processed before, + // we don't need to process lineage again to avoid duplicates. + break; + } + } + }) + + var tagCount = Object.keys(tags).length; + // Add the tree to the taxonomy list + taxonomyList.push({ + id: taxonomy.taxonomyId, + value: taxonomy.name, + tags: rootTagsValues.map(rootValue => tags[rootValue]), + count: tagCount, + }); + totalCount += tagCount; + }); + + return { + count: totalCount, + taxonomies: taxonomyList, + }; + }, + handleKeyDownOnHeader: function(event) { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); diff --git a/cms/static/js/views/tag_count.js b/cms/static/js/views/tag_count.js new file mode 100644 index 000000000000..c7ba4d79e5ed --- /dev/null +++ b/cms/static/js/views/tag_count.js @@ -0,0 +1,54 @@ +define(['jquery', 'underscore', 'js/views/baseview', 'edx-ui-toolkit/js/utils/html-utils'], +function($, _, BaseView, HtmlUtils) { + 'use strict'; + + /** + * TagCountView displays the tag count of a unit/component + * + * This component is being rendered in this way to allow receiving + * messages from the Manage tags drawer and being able to update the count. + */ + var TagCountView = BaseView.extend({ + // takes TagCountModel as a model + + initialize: function() { + BaseView.prototype.initialize.call(this); + this.template = this.loadTemplate('tag-count'); + }, + + setupMessageListener: function () { + window.addEventListener( + 'message', (event) => { + // Listen any message from Manage tags drawer. + var data = event.data; + var courseAuthoringUrl = this.model.get("course_authoring_url") + if (event.origin == courseAuthoringUrl + && data.includes('[Manage tags drawer] Count updated:')) { + // This message arrives when there is a change in the tag list. + // The message contains the new count of tags. + let jsonData = data.replace(/\[Manage tags drawer\] Count updated: /g, ""); + jsonData = JSON.parse(jsonData); + if (jsonData.contentId == this.model.get("content_id")) { + this.model.set('tags_count', jsonData.count); + this.render(); + } + } + } + ); + }, + + render: function() { + HtmlUtils.setHtml( + this.$el, + HtmlUtils.HTML( + this.template({ + tags_count: this.model.get("tags_count"), + }) + ) + ); + return this; + } + }); + + return TagCountView; +}); diff --git a/cms/static/sass/elements/_drawer.scss b/cms/static/sass/elements/_drawer.scss index c18073be9864..96edfe1983f1 100644 --- a/cms/static/sass/elements/_drawer.scss +++ b/cms/static/sass/elements/_drawer.scss @@ -13,6 +13,10 @@ background: rgba(0, 0, 0, 0.8); } +.drawer-cover.gray-cover { + background: rgba(112, 112, 112, 0.8); +} + .drawer { @extend %ui-depth4; diff --git a/cms/templates/container.html b/cms/templates/container.html index e5a99b11a0c1..5098ba64b9fe 100644 --- a/cms/templates/container.html +++ b/cms/templates/container.html @@ -236,5 +236,5 @@
${_("Location ID")}
-
+
diff --git a/cms/templates/course_outline.html b/cms/templates/course_outline.html index 9dd30f8ea8a8..5b1f0e9fbb3d 100644 --- a/cms/templates/course_outline.html +++ b/cms/templates/course_outline.html @@ -29,7 +29,7 @@ <%block name="header_extras"> -% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'self-paced-due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'unit-access-editor', 'discussion-editor', 'content-visibility-editor', 'verification-access-editor', 'timed-examination-preference-editor', 'access-editor', 'settings-modal-tabs', 'show-correctness-editor', 'highlights-editor', 'highlights-enable-editor', 'course-highlights-enable']: +% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'self-paced-due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor', 'unit-access-editor', 'discussion-editor', 'content-visibility-editor', 'verification-access-editor', 'timed-examination-preference-editor', 'access-editor', 'settings-modal-tabs', 'show-correctness-editor', 'highlights-editor', 'highlights-enable-editor', 'course-highlights-enable', 'tag-count']: @@ -279,7 +279,7 @@

${_("Page Actions")}

course_locator = context_course.location %>

${_("Course Outline")}

-
+
@@ -321,5 +321,5 @@

${_("Changing the content learners see")}

-
+
diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore index 7d53e392ff16..979649e4a2e5 100644 --- a/cms/templates/js/course-outline.underscore +++ b/cms/templates/js/course-outline.underscore @@ -7,7 +7,8 @@ var hasPartitionGroups = xblockInfo.get('has_partition_group_components'); var userPartitionInfo = xblockInfo.get('user_partition_info'); var selectedGroupsLabel = userPartitionInfo['selected_groups_label']; var selectedPartitionIndex = userPartitionInfo['selected_partition_index']; -var tagsCount = (xblockInfo.get('tag_counts_by_unit') || {})[xblockInfo.get('id')] || 0; +var xblockId = xblockInfo.get('id') +var tagsCount = (xblockInfo.get('tag_counts_by_unit') || {})[xblockId] || 0; var statusMessages = []; var messageType; @@ -189,14 +190,8 @@ if (is_proctored_exam) { <% } %> - <% if (xblockInfo.isVertical() && typeof useTaggingTaxonomyListPage !== "undefined" && useTaggingTaxonomyListPage && tagsCount > 0) { %> -
  • - - - <%- tagsCount %> - <%- gettext('Manage Tags') %> - -
  • + <% if (xblockInfo.isVertical() && typeof useTaggingTaxonomyListPage !== "undefined" && useTaggingTaxonomyListPage) { %> +
  • <% } %> <% if (xblockInfo.isDraggable()) { %> diff --git a/cms/templates/js/tag-count.underscore b/cms/templates/js/tag-count.underscore new file mode 100644 index 000000000000..253323109f3c --- /dev/null +++ b/cms/templates/js/tag-count.underscore @@ -0,0 +1,7 @@ +<% if (tags_count && tags_count > 0) { %> + +<% } %> diff --git a/cms/templates/studio_xblock_wrapper.html b/cms/templates/studio_xblock_wrapper.html index fea24c17be4f..6c26ec55fa6a 100644 --- a/cms/templates/studio_xblock_wrapper.html +++ b/cms/templates/studio_xblock_wrapper.html @@ -29,6 +29,9 @@ + +<%static:webpack entry="js/factories/tag_count"> + TagCountFactory({ + tags_count: "${tags_count | n, js_escaped_string}", + content_id: "${xblock.location | n, js_escaped_string}", + course_authoring_url: "${course_authoring_url | n, js_escaped_string}", + }, + $('li.tag-count[data-locator="${xblock.location | n, js_escaped_string}"]') + ); + + % if not is_root: % if is_reorderable:
  • @@ -86,14 +99,8 @@