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
This commit is contained in:
Chris Chávez
2024-01-25 13:33:47 -05:00
committed by GitHub
parent 7535f9d581
commit 5838d68efc
18 changed files with 221 additions and 38 deletions

View File

@@ -49,7 +49,6 @@ from cms.djangoapps.contentstore.utils import (
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
@@ -1416,15 +1415,12 @@ class ContentStoreTest(ContentStoreTestCase):
course.location.course_key
)
taxonomy_tags_widget_url = get_taxonomy_tags_widget_url(course.id)
self.assertContains(
resp,
'<article class="outline outline-complex outline-course" data-locator="{locator}" data-course-key="{course_key}" data-course-assets="{assets_url}" data-taxonomy-tags-widget-url="{taxonomy_tags_widget_url}" >'.format( # lint-amnesty, pylint: disable=line-too-long
'<article class="outline outline-complex outline-course" data-locator="{locator}" data-course-key="{course_key}" data-course-assets="{assets_url}" >'.format( # lint-amnesty, pylint: disable=line-too-long
locator=str(course.location),
course_key=str(course.id),
assets_url=assets_url,
taxonomy_tags_widget_url=taxonomy_tags_widget_url,
),
status_code=200,
html=True

View File

@@ -315,6 +315,7 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False):
'is_reorderable': is_reorderable,
'can_edit': can_edit,
'can_edit_visibility': context.get('can_edit_visibility', is_course),
'course_authoring_url': settings.COURSE_AUTHORING_MICROFRONTEND_URL,
'is_loading': context.get('is_loading', False),
'is_selected': context.get('is_selected', False),
'selectable': context.get('selectable', False),

View File

@@ -288,15 +288,9 @@ class GetItemTest(ItemTest):
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)

View File

@@ -1206,6 +1206,7 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements
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"]:

View File

@@ -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};

View File

@@ -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;
});

View File

@@ -12,11 +12,11 @@ define(['jquery', 'underscore', 'js/views/xblock_outline', 'edx-ui-toolkit/js/ut
'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',
'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt',
'js/views/utils/tagging_drawer_utils',],
'js/views/utils/tagging_drawer_utils', 'js/views/tag_count', 'js/models/tag_count'],
function(
$, _, XBlockOutlineView, StringUtils, ViewUtils, XBlockViewUtils,
XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger, NotificationView, PromptView,
TaggingDrawerUtils
TaggingDrawerUtils, TagCountView, TagCountModel
) {
var CourseOutlineView = XBlockOutlineView.extend({
// takes XBlockOutlineInfo as a model
@@ -28,9 +28,28 @@ function(
this.makeContentDraggable(this.el);
// Show/hide the paste button
this.initializePasteButton(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'));
},
@@ -461,10 +480,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);
},

View File

@@ -111,6 +111,7 @@ function($, _, Backbone, gettext, BasePage,
el: this.$('.unit-tags'),
model: this.model
});
this.tagListView.setupMessageListener();
this.tagListView.render();
this.unitOutlineView = new UnitOutlineView({

View File

@@ -370,6 +370,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();

View File

@@ -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;
});

View File

@@ -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;

View File

@@ -281,5 +281,5 @@ from openedx.core.djangolib.markup import HTML, Text
</div>
<div id="manage-tags-drawer" class="drawer"></div>
<div class="drawer-cover"></div>
<div class="drawer-cover gray-cover"></div>
</%block>

View File

@@ -29,7 +29,7 @@ from django.urls import reverse
<%block name="header_extras">
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
% 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', 'course-video-sharing-enable', 'summary-configuration-editor']:
% 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', 'course-video-sharing-enable', 'summary-configuration-editor', 'tag-count']:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" />
</script>
@@ -281,7 +281,7 @@ from django.urls import reverse
assets_url = reverse('assets_handler', kwargs={'course_key_string': str(course_locator.course_key)})
%>
<h2 class="sr">${_("Course Outline")}</h2>
<article class="outline outline-complex outline-course" data-locator="${course_locator}" data-course-key="${course_locator.course_key}" data-course-assets="${assets_url}" data-taxonomy-tags-widget-url="${taxonomy_tags_widget_url}">
<article class="outline outline-complex outline-course" data-locator="${course_locator}" data-course-key="${course_locator.course_key}" data-course-assets="${assets_url}">
</article>
</div>
<div class="ui-loading">
@@ -323,5 +323,5 @@ from django.urls import reverse
</div>
<div id="manage-tags-drawer" class="drawer"></div>
<div class="drawer-cover"></div>
<div class="drawer-cover gray-cover"></div>
</%block>

View File

@@ -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;
@@ -171,14 +172,8 @@ if (is_proctored_exam) {
</li>
<% } %>
<% if (xblockInfo.isVertical() && typeof useTaggingTaxonomyListPage !== "undefined" && useTaggingTaxonomyListPage && tagsCount > 0) { %>
<li class="action-item">
<a href="#" data-tooltip="<%- gettext('Manage Tags') %>" class="manage-tags-button action-button">
<span class="icon fa fa-tag" aria-hidden="true"></span>
<span><%- tagsCount %></span>
<span class="sr action-button-text"><%- gettext('Manage Tags') %></span>
</a>
</li>
<% if (xblockInfo.isVertical() && typeof useTaggingTaxonomyListPage !== "undefined" && useTaggingTaxonomyListPage) { %>
<li class="action-item tag-count" data-locator="<%- xblockId %>"></li>
<% } %>
<% if (typeof enableCopyPasteUnits !== "undefined" && enableCopyPasteUnits) { %>

View File

@@ -0,0 +1,7 @@
<% if (tags_count && tags_count > 0) { %>
<button data-tooltip="<%- gettext("Manage Tags") %>" class="btn-default action-button manage-tags-button" data-testid="tag-count-button">
<span class="icon fa fa-tag" aria-hidden="true"></span>
<span><%- tags_count %></span>
<span class="sr action-button-text"><%- gettext("Manage Tags") %></span>
</button>
<% } %>

View File

@@ -29,6 +29,9 @@ block_is_unit = is_unit(xblock)
<script type="text/template" id="xblock-validation-messages-tpl">
<%static:include path="js/xblock-validation-messages.underscore" />
</script>
<script type="text/template" id="tag-count-tpl">
<%static:include path="js/tag-count.underscore" />
</script>
</%block>
<script type="text/javascript">
@@ -41,6 +44,16 @@ block_is_unit = is_unit(xblock)
);
</script>
<%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}"]')
);
</%static:webpack>
% if not is_root:
% if is_reorderable:
<li class="studio-xblock-wrapper is-draggable" id="${xblock.location}" data-locator="${xblock.location}" data-course-key="${xblock.location.course_key}">
@@ -99,14 +112,8 @@ block_is_unit = is_unit(xblock)
<ul class="actions-list nav-dd ui-right">
% if not is_root:
% if can_edit:
% if use_tagging and tags_count:
<li class="action-item">
<button data-tooltip="${_("Manage Tags")}" class="btn-default action-button manage-tags-button" data-testid="tag-count-button">
<span class="icon fa fa-tag" aria-hidden="true"></span>
<span>${tags_count}</span>
<span class="sr action-button-text">${_("Manage Tags")}</span>
</button>
</li>
% if use_tagging:
<li class="action-item tag-count" data-locator="${xblock.location}"></li>
% endif
% if not show_inline:
<li class="action-item action-edit">

View File

@@ -3,6 +3,7 @@ Taxonomies API v1 URLs.
"""
from rest_framework.routers import DefaultRouter
from openedx_tagging.core.tagging.rest_api.v1.views import ObjectTagCountsView
from django.urls.conf import path, include
@@ -16,6 +17,7 @@ from . import views
router = DefaultRouter()
router.register("taxonomies", views.TaxonomyOrgView, basename="taxonomy")
router.register("object_tags", views.ObjectTagOrgView, basename="object_tag")
router.register("object_tag_counts", ObjectTagCountsView, basename="object_tag_counts")
urlpatterns = [
path(

View File

@@ -84,6 +84,7 @@ module.exports = Merge.smart({
'js/factories/xblock_validation': './cms/static/js/factories/xblock_validation.js',
'js/factories/edit_tabs': './cms/static/js/factories/edit_tabs.js',
'js/sock': './cms/static/js/sock.js',
'js/factories/tag_count': './cms/static/js/factories/tag_count.js',
// LMS
SingleSupportForm: './lms/static/support/jsx/single_support_form.jsx',