From 104d42c67a829a02c97baf6098c4026e401a0897 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Chris=20Ch=C3=A1vez?=
Date: Fri, 17 Nov 2023 13:03:28 -0500
Subject: [PATCH] feat: Tag List on Unit page (feature-flagged) (#33645)
---
cms/djangoapps/contentstore/utils.py | 7 +-
.../contentstore/views/component.py | 97 +++++++++-
.../xblock_storage_handlers/view_handlers.py | 6 +
cms/static/js/models/xblock_info.js | 4 +
cms/static/js/views/course_outline.js | 39 +---
cms/static/js/views/pages/container.js | 19 +-
.../js/views/pages/container_subviews.js | 171 +++++++++++++++++-
.../js/views/utils/tagging_drawer_utils.js | 55 ++++++
cms/static/sass/elements/_controls.scss | 28 +++
cms/static/sass/views/_container.scss | 71 ++++++++
cms/templates/container.html | 8 +-
cms/templates/js/tag-list.underscore | 32 ++++
cms/templates/studio_xblock_wrapper.html | 8 +-
13 files changed, 501 insertions(+), 44 deletions(-)
create mode 100644 cms/static/js/views/utils/tagging_drawer_utils.js
create mode 100644 cms/templates/js/tag-list.underscore
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index ba227ecbec0e..8e3f97eee98e 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -1,6 +1,7 @@
"""
Common utility functions useful throughout the contentstore
"""
+from __future__ import annotations
import configparser
import logging
from collections import defaultdict
@@ -442,7 +443,7 @@ def get_taxonomy_list_url():
return taxonomy_list_url
-def get_taxonomy_tags_widget_url(course_locator) -> str:
+def get_taxonomy_tags_widget_url(course_locator=None) -> str | None:
"""
Gets course authoring microfrontend URL for taxonomy tags drawer widget view.
@@ -451,7 +452,9 @@ def get_taxonomy_tags_widget_url(course_locator) -> str:
taxonomy_tags_widget_url = None
# Uses the same waffle flag as taxonomy list page
if use_tagging_taxonomy_list_page():
- mfe_base_url = get_course_authoring_url(course_locator)
+ mfe_base_url = settings.COURSE_AUTHORING_MICROFRONTEND_URL
+ if course_locator:
+ mfe_base_url = get_course_authoring_url(course_locator)
if mfe_base_url:
taxonomy_tags_widget_url = f'{mfe_base_url}/tagging/components/widget/'
return taxonomy_tags_widget_url
diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py
index 3c31637d4c78..3cd3ffff90e2 100644
--- a/cms/djangoapps/contentstore/views/component.py
+++ b/cms/djangoapps/contentstore/views/component.py
@@ -25,10 +25,14 @@
from common.djangoapps.student.auth import has_course_author_access
from common.djangoapps.xblock_django.api import authorable_xblocks, disabled_xblocks
from common.djangoapps.xblock_django.models import XBlockStudioConfigurationFlag
-from cms.djangoapps.contentstore.toggles import use_new_problem_editor
+from cms.djangoapps.contentstore.toggles import (
+ use_new_problem_editor,
+ use_tagging_taxonomy_list_page,
+)
from openedx.core.lib.xblock_utils import get_aside_from_xblock, is_xblock_aside
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration
from openedx.core.djangoapps.content_staging import api as content_staging_api
+from openedx.core.djangoapps.content_tagging.api import get_content_tags
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order
from ..toggles import use_new_unit_page
@@ -61,7 +65,7 @@
"editor-mode-button", "upload-dialog",
"add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu",
"add-xblock-component-support-legend", "add-xblock-component-support-level", "add-xblock-component-menu-problem",
- "xblock-string-field-editor", "xblock-access-editor", "publish-xblock", "publish-history",
+ "xblock-string-field-editor", "xblock-access-editor", "publish-xblock", "publish-history", "tag-list",
"unit-outline", "container-message", "container-access", "license-selector", "copy-clipboard-button",
"edit-title-button",
]
@@ -178,9 +182,14 @@ def container_handler(request, usage_key_string):
prev_url = quote_plus(prev_url) if prev_url else None
next_url = quote_plus(next_url) if next_url else None
+ show_unit_tags = use_tagging_taxonomy_list_page()
+ unit_tags = None
+ if show_unit_tags and is_unit_page:
+ unit_tags = get_unit_tags(usage_key)
+
# Fetch the XBlock info for use by the container page. Note that it includes information
# about the block's ancestors and siblings for use by the Unit Outline.
- xblock_info = create_xblock_info(xblock, include_ancestor_info=is_unit_page)
+ xblock_info = create_xblock_info(xblock, include_ancestor_info=is_unit_page, tags=unit_tags)
if is_unit_page:
add_container_page_publishing_info(xblock, xblock_info)
@@ -216,6 +225,7 @@ def container_handler(request, usage_key_string):
'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link,
'templates': CONTAINER_TEMPLATES,
+ 'show_unit_tags': show_unit_tags,
# Status of the user's clipboard, exactly as would be returned from the "GET clipboard" REST API.
'user_clipboard': user_clipboard,
})
@@ -598,3 +608,84 @@ def component_handler(request, usage_key_string, handler, suffix=''):
)
return webob_to_django_response(resp)
+
+
+def get_unit_tags(usage_key):
+ """
+ Get the tags of a Unit and build a json to be read by the UI
+
+ Note: When migrating the `TagList` subview from `container_subview.js` to the course-authoring MFE,
+ this function can be simplified to use the REST API of openedx-learning,
+ which already provides this grouping + sorting logic.
+ """
+ # Get content tags from content tagging API
+ content_tags = get_content_tags(usage_key)
+
+ # Group content tags by taxonomy
+ taxonomy_dict = {}
+ for content_tag in content_tags:
+ taxonomy_id = content_tag.taxonomy_id
+ # When a taxonomy is deleted, the id here is None.
+ # In that case the tag is not shown in the UI.
+ if taxonomy_id:
+ if taxonomy_id not in taxonomy_dict:
+ taxonomy_dict[taxonomy_id] = []
+ taxonomy_dict[taxonomy_id].append(content_tag)
+
+ taxonomy_list = []
+ total_count = 0
+
+ def handle_tag(tags, root_ids, tag, child_tag_id=None):
+ """
+ Group each tag by parent to build a tree.
+ """
+ tag_processed_before = tag.id in tags
+ if not tag_processed_before:
+ tags[tag.id] = {
+ 'id': tag.id,
+ 'value': tag.value,
+ 'children': [],
+ }
+ if child_tag_id:
+ # Add a child into the children list
+ tags[tag.id].get('children').append(tags[child_tag_id])
+ if tag.parent_id is None:
+ if tag.id not in root_ids:
+ root_ids.append(tag.id)
+ elif not tag_processed_before:
+ # Group all the lineage of this tag.
+ #
+ # Skip this if the tag has been processed before,
+ # we don't need to process lineage again to avoid duplicates.
+ handle_tag(tags, root_ids, tag.parent, tag.id)
+
+ # Build a tag tree for each taxonomy
+ for content_tag_list in taxonomy_dict.values():
+ tags = {}
+ root_ids = []
+
+ for content_tag in content_tag_list:
+ # When a tag is deleted from the taxonomy, the `tag` here is None.
+ # In that case the tag is not shown in the UI.
+ if content_tag.tag:
+ handle_tag(tags, root_ids, content_tag.tag)
+
+ taxonomy = content_tag_list[0].taxonomy
+
+ if tags:
+ count = len(tags)
+ # Add the tree to the taxonomy list
+ taxonomy_list.append({
+ 'id': taxonomy.id,
+ 'value': taxonomy.name,
+ 'tags': [tags[tag_id] for tag_id in root_ids],
+ 'count': count,
+ })
+ total_count += count
+
+ unit_tags = {
+ 'count': total_count,
+ 'taxonomies': taxonomy_list,
+ }
+
+ return unit_tags
diff --git a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py
index dca25695a29d..a563c6b61da4 100644
--- a/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py
+++ b/cms/djangoapps/contentstore/xblock_storage_handlers/view_handlers.py
@@ -99,6 +99,7 @@
has_children_visible_to_specific_partition_groups,
is_currently_visible_to_students,
is_self_paced,
+ get_taxonomy_tags_widget_url,
)
from .create_xblock import create_xblock
@@ -1093,6 +1094,7 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements
course=None,
is_concise=False,
summary_configuration=None,
+ tags=None,
):
"""
Creates the information needed for client-side XBlockInfo.
@@ -1385,6 +1387,10 @@ def create_xblock_info( # lint-amnesty, pylint: disable=too-many-statements
)
else:
xblock_info["ancestor_has_staff_lock"] = False
+ if tags is not None:
+ xblock_info["tags"] = tags
+ if use_tagging_taxonomy_list_page():
+ xblock_info["taxonomy_tags_widget_url"] = get_taxonomy_tags_widget_url()
if course_outline:
if xblock_info["has_explicit_staff_lock"]:
diff --git a/cms/static/js/models/xblock_info.js b/cms/static/js/models/xblock_info.js
index d7d9ef278b01..49812a6c9d78 100644
--- a/cms/static/js/models/xblock_info.js
+++ b/cms/static/js/models/xblock_info.js
@@ -173,6 +173,10 @@ define(
* True if summary configuration is enabled.
*/
summary_configuration_enabled: null,
+ /**
+ * List of tags of the unit. This list is managed by the content_tagging module.
+ */
+ tags: null,
},
initialize: function() {
diff --git a/cms/static/js/views/course_outline.js b/cms/static/js/views/course_outline.js
index 2faccde97e36..4b7107cc4ddc 100644
--- a/cms/static/js/views/course_outline.js
+++ b/cms/static/js/views/course_outline.js
@@ -11,10 +11,12 @@
define(['jquery', 'underscore', 'js/views/xblock_outline', 'edx-ui-toolkit/js/utils/string-utils',
'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',],
+ 'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt',
+ 'js/views/utils/tagging_drawer_utils',],
function(
$, _, XBlockOutlineView, StringUtils, ViewUtils, XBlockViewUtils,
- XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger, NotificationView, PromptView
+ XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger, NotificationView, PromptView,
+ TaggingDrawerUtils
) {
var CourseOutlineView = XBlockOutlineView.extend({
// takes XBlockOutlineInfo as a model
@@ -458,41 +460,12 @@ function(
event.stopPropagation();
},
- closeManageTagsDrawer(drawer, drawerCover) {
- $(drawerCover).css('display', 'none');
- $(drawer).empty();
- $(drawer).css('display', 'none');
- $('body').removeClass('drawer-open');
- },
-
- openManageTagsDrawer(event) {
- const drawer = document.querySelector("#manage-tags-drawer");
- const drawerCover = document.querySelector(".drawer-cover")
+ openManageTagsDrawer() {
const article = document.querySelector('[data-taxonomy-tags-widget-url]');
const taxonomyTagsWidgetUrl = $(article).attr('data-taxonomy-tags-widget-url');
const contentId = this.model.get('id');
- // Add handler to close drawer when dark background is clicked
- $(drawerCover).click(function() {
- this.closeManageTagsDrawer(drawer, drawerCover);
- }.bind(this));
-
- // Add event listen to close drawer when close button is clicked from within the Iframe
- window.addEventListener("message", function (event) {
- if (event.data === 'closeManageTagsDrawer') {
- this.closeManageTagsDrawer(drawer, drawerCover)
- }
- }.bind(this));
-
- $(drawerCover).css('display', 'block');
- // xss-lint: disable=javascript-jquery-html
- $(drawer).html(
- ``
- );
- $(drawer).css('display', 'block');
-
- // Prevent background from being scrollable when drawer is open
- $('body').addClass('drawer-open');
+ TaggingDrawerUtils.openDrawer(taxonomyTagsWidgetUrl, contentId);
},
addButtonActions: function(element) {
diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js
index 8f296ebb6b58..71cbb9b26d5f 100644
--- a/cms/static/js/views/pages/container.js
+++ b/cms/static/js/views/pages/container.js
@@ -8,10 +8,11 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page
'js/models/xblock_info', 'js/views/xblock_string_field_editor', 'js/views/xblock_access_editor',
'js/views/pages/container_subviews', 'js/views/unit_outline', 'js/views/utils/xblock_utils',
'common/js/components/views/feedback_notification', 'common/js/components/views/feedback_prompt',
+ 'js/views/utils/tagging_drawer_utils',
],
function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView, AddXBlockComponent,
EditXBlockModal, MoveXBlockModal, XBlockInfo, XBlockStringFieldEditor, XBlockAccessEditor,
- ContainerSubviews, UnitOutlineView, XBlockUtils, NotificationView, PromptView) {
+ ContainerSubviews, UnitOutlineView, XBlockUtils, NotificationView, PromptView, TaggingDrawerUtils) {
'use strict';
var XBlockContainerPage = BasePage.extend({
@@ -27,6 +28,7 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
'click .show-actions-menu-button': 'showXBlockActionsMenu',
'click .new-component-button': 'scrollToNewComponentButtons',
'click .paste-component-button': 'pasteComponent',
+ 'click .tags-button': 'openManageTags',
},
options: {
@@ -97,6 +99,12 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
});
this.viewLiveActions.render();
+ this.tagListView = new ContainerSubviews.TagList({
+ el: this.$('.unit-tags'),
+ model: this.model
+ });
+ this.tagListView.render();
+
this.unitOutlineView = new UnitOutlineView({
el: this.$('.wrapper-unit-overview'),
model: this.model
@@ -125,6 +133,7 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
xblockView = this.xblockView,
loadingElement = this.$('.ui-loading'),
unitLocationTree = this.$('.unit-location'),
+ unitTags = this.$('.unit-tags'),
hiddenCss = 'is-hidden';
loadingElement.removeClass(hiddenCss);
@@ -150,6 +159,7 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
// Refresh the views now that the xblock is visible
self.onXBlockRefresh(xblockView);
unitLocationTree.removeClass(hiddenCss);
+ unitTags.removeClass(hiddenCss);
// Re-enable Backbone events for any updated DOM elements
self.delegateEvents();
@@ -409,6 +419,13 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
this.duplicateComponent(this.findXBlockElement(event.target));
},
+ openManageTags: function(event) {
+ const taxonomyTagsWidgetUrl = this.model.get('taxonomy_tags_widget_url');
+ const contentId = this.findXBlockElement(event.target).data('locator');
+
+ TaggingDrawerUtils.openDrawer(taxonomyTagsWidgetUrl, contentId);
+ },
+
showMoveXBlockModal: function(event) {
var xblockElement = this.findXBlockElement(event.target),
parentXBlockElement = xblockElement.parents('.studio-xblock-wrapper'),
diff --git a/cms/static/js/views/pages/container_subviews.js b/cms/static/js/views/pages/container_subviews.js
index fc7f807257ca..b4ee286ae897 100644
--- a/cms/static/js/views/pages/container_subviews.js
+++ b/cms/static/js/views/pages/container_subviews.js
@@ -2,8 +2,9 @@
* Subviews (usually small side panels) for XBlockContainerPage.
*/
define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/components/utils/view_utils',
- 'js/views/utils/xblock_utils', 'js/views/utils/move_xblock_utils', 'edx-ui-toolkit/js/utils/html-utils'],
-function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, HtmlUtils) {
+ 'js/views/utils/xblock_utils', 'js/views/utils/move_xblock_utils', 'edx-ui-toolkit/js/utils/html-utils',
+ 'js/views/utils/tagging_drawer_utils'],
+function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, HtmlUtils, TaggingDrawerUtils) {
'use strict';
var disabledCss = 'is-disabled';
@@ -295,11 +296,175 @@ function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, MoveXBlockUtils, H
}
});
+ /**
+ * TagList displays the tags of a unit.
+ */
+ var TagList = BaseView.extend({
+ // takes XBlockInfo as a model
+
+ events: {
+ 'click .wrapper-tag-header': 'expandTagContainer',
+ 'click .tagging-label': 'expandContentTag',
+ 'click .manage-tag-button': 'openManageTagDrawer',
+ 'keydown .wrapper-tag-header': 'handleKeyDownOnHeader',
+ 'keydown .tagging-label': 'handleKeyDownOnContentTag',
+ 'keydown .manage-tag-button': 'handleKeyDownOnTagDrawer',
+ },
+
+ initialize: function() {
+ BaseView.prototype.initialize.call(this);
+ this.template = this.loadTemplate('tag-list');
+ this.model.on('sync', this.onSync, this);
+ },
+
+ onSync: function(model) {
+ if (ViewUtils.hasChangedAttributes(model, ['tags'])) {
+ this.render();
+ }
+ },
+
+ handleKeyDownOnHeader: function(event) {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ this.expandTagContainer();
+ }
+ },
+
+ handleKeyDownOnContentTag: function(event) {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ this.expandContentTag(event);
+ }
+ },
+
+ handleKeyDownOnTagDrawer: function(event) {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault();
+ this.openManageTagDrawer();
+ }
+ },
+
+ expandTagContainer: function() {
+ var $content = this.$('.wrapper-tags .wrapper-tag-content'),
+ $header = this.$('.wrapper-tags .wrapper-tag-header'),
+ $icon = this.$('.wrapper-tags .wrapper-tag-header .icon');
+
+ if ($content.hasClass('is-hidden')) {
+ $content.removeClass('is-hidden');
+ $icon.addClass('fa-caret-up');
+ $icon.removeClass('fa-caret-down');
+ $header.attr('aria-expanded', 'true');
+ } else {
+ $content.addClass('is-hidden');
+ $icon.removeClass('fa-caret-up');
+ $icon.addClass('fa-caret-down');
+ $header.attr('aria-expanded', 'false');
+ }
+ },
+
+ expandContentTag: function(event) {
+ var contentId = event.target.id,
+ $content = this.$(`.wrapper-tags .content-tags-${contentId}`),
+ $header = this.$(`.wrapper-tags .tagging-label-${contentId}`),
+ $icon = this.$(`.wrapper-tags .tagging-label-${contentId} .icon`);
+
+ if ($content.hasClass('is-hidden')) {
+ $content.removeClass('is-hidden');
+ $icon.addClass('fa-caret-up');
+ $icon.removeClass('fa-caret-down');
+ $header.attr('aria-expanded', 'true');
+ } else {
+ $content.addClass('is-hidden');
+ $icon.removeClass('fa-caret-up');
+ $icon.addClass('fa-caret-down');
+ $header.attr('aria-expanded', 'false');
+ }
+ },
+
+ renderTagElements: function(tags, depth, parentId) {
+ const tagListElement = this;
+ tags.forEach(function(tag) {
+ const parentElement = document.querySelector(`.content-tags-${parentId}`);
+ var tagContentElement = document.createElement('div'),
+ tagValueElement = document.createElement('span');
+
+ // Element that contains the tag value and the arrow icon
+ tagContentElement.style.marginLeft = `${depth}em`;
+ tagContentElement.className = `tagging-label tagging-label-tag-${tag.id}`;
+ tagContentElement.id = `tag-${tag.id}`;
+
+ // Element that contains the tag value
+ tagValueElement.textContent = tag.value;
+ tagValueElement.id = `tag-${tag.id}`;
+ tagValueElement.className = 'tagging-label-value';
+
+ tagContentElement.appendChild(tagValueElement);
+ parentElement.appendChild(tagContentElement);
+
+ if (tag.children.length > 0) {
+ var tagIconElement = document.createElement('span'),
+ tagChildrenElement = document.createElement('div');
+
+ // Arrow icon
+ tagIconElement.className = 'icon fa fa-caret-down';
+ tagIconElement.ariaHidden = 'true';
+ tagIconElement.id = `tag-${tag.id}`;
+
+ // Element that contains the children of this tag
+ tagChildrenElement.className = `content-tags-tag-${tag.id} is-hidden`;
+
+ tagContentElement.tabIndex = 0;
+ tagContentElement.role = "button";
+ tagContentElement.ariaExpanded = "false";
+ tagContentElement.setAttribute('aria-controls', `content-tags-tag-${tag.id}`);
+ tagContentElement.appendChild(tagIconElement);
+ parentElement.appendChild(tagChildrenElement);
+
+ // Render children
+ tagListElement.renderTagElements(tag.children, depth + 1, `tag-${tag.id}`);
+ }
+ });
+ },
+
+ renderTags: function() {
+ if (this.model.get('tags') !== null) {
+ const taxonomies = this.model.get('tags').taxonomies;
+ const tagListElement = this;
+ taxonomies.forEach(function(taxonomy) {
+ tagListElement.renderTagElements(taxonomy.tags, 1, `tax-${taxonomy.id}`);
+ });
+ }
+ },
+
+ openManageTagDrawer: function() {
+ const taxonomyTagsWidgetUrl = this.model.get('taxonomy_tags_widget_url');
+ const contentId = this.model.get('id');
+
+ TaggingDrawerUtils.openDrawer(taxonomyTagsWidgetUrl, contentId);
+ },
+
+ render: function() {
+ HtmlUtils.setHtml(
+ this.$el,
+ HtmlUtils.HTML(
+ this.template({
+ tags: this.model.get('tags'),
+ })
+ )
+ );
+
+ this.renderTags();
+
+ return this;
+ }
+ });
+
return {
MessageView: MessageView,
ViewLiveButtonController: ViewLiveButtonController,
Publisher: Publisher,
PublishHistory: PublishHistory,
- ContainerAccess: ContainerAccess
+ ContainerAccess: ContainerAccess,
+ TagList: TagList
};
}); // end define();
diff --git a/cms/static/js/views/utils/tagging_drawer_utils.js b/cms/static/js/views/utils/tagging_drawer_utils.js
new file mode 100644
index 000000000000..107f79f04470
--- /dev/null
+++ b/cms/static/js/views/utils/tagging_drawer_utils.js
@@ -0,0 +1,55 @@
+/**
+ * Provides utilities to open and close the tagging drawer to manage tags.
+ *
+ * To use this drawer you need to add the following code into your template:
+ *
+ * ```
+ *
+ *
+ * ```
+ */
+define(['jquery'],
+function($,) {
+ 'use strict';
+
+ var closeDrawer, openDrawer;
+
+ closeDrawer = function(drawer, drawerCover) {
+ $(drawerCover).css('display', 'none');
+ $(drawer).empty();
+ $(drawer).css('display', 'none');
+ $('body').removeClass('drawer-open');
+ };
+
+ openDrawer = function(taxonomyTagsWidgetUrl, contentId) {
+ const drawer = document.querySelector("#manage-tags-drawer");
+ const drawerCover = document.querySelector(".drawer-cover");
+
+ // Add handler to close drawer when dark background is clicked
+ $(drawerCover).click(function() {
+ closeDrawer(drawer, drawerCover);
+ }.bind(this));
+
+ // Add event listen to close drawer when close button is clicked from within the Iframe
+ window.addEventListener("message", function (event) {
+ if (event.data === 'closeManageTagsDrawer') {
+ closeDrawer(drawer, drawerCover)
+ }
+ }.bind(this));
+
+ $(drawerCover).css('display', 'block');
+ // xss-lint: disable=javascript-jquery-html
+ $(drawer).html(
+ ``
+ );
+ $(drawer).css('display', 'block');
+
+ // Prevent background from being scrollable when drawer is open
+ $('body').addClass('drawer-open');
+ };
+
+ return {
+ openDrawer: openDrawer,
+ closeDrawer: closeDrawer
+ };
+});
diff --git a/cms/static/sass/elements/_controls.scss b/cms/static/sass/elements/_controls.scss
index 8420961ab4ab..3a9c3af3db89 100644
--- a/cms/static/sass/elements/_controls.scss
+++ b/cms/static/sass/elements/_controls.scss
@@ -119,6 +119,34 @@
}
}
+// inverse primary button
+%btn-primary-inverse {
+ @extend %ui-btn-primary;
+
+ background: theme-color("inverse");
+ border-color: theme-color("primary");
+ color: theme-color("primary");
+
+ &:hover,
+ &:active {
+ background: theme-color("primary");
+ border-color: $uxpl-blue-hover-active;
+ color: theme-color("inverse");
+ }
+
+ &.current,
+ &.active {
+ background: theme-color("primary");
+ border-color: $uxpl-blue-hover-active;
+ color: theme-color("inverse");
+
+ &:hover,
+ &:active {
+ background: theme-color("primary");
+ }
+ }
+}
+
// +Secondary Button - Extends
// ====================
// gray secondary button
diff --git a/cms/static/sass/views/_container.scss b/cms/static/sass/views/_container.scss
index cf8824ca5110..2b55b00cb6f4 100644
--- a/cms/static/sass/views/_container.scss
+++ b/cms/static/sass/views/_container.scss
@@ -242,6 +242,77 @@
}
}
+ .unit-tags {
+ .wrapper-tags {
+ margin-bottom: $baseline;
+ padding: ($baseline*0.75);
+ background-color: $white;
+
+ .wrapper-tag-header {
+ display: flex;
+ justify-content: space-between;
+
+ .tag-title {
+ font-weight: bold;
+ }
+
+ .count-badge {
+ background-color: $gray-l5;
+ border-radius: 50%;
+ display: inline-block;
+ padding: 0px 8px;
+ }
+ }
+
+ .wrapper-tag-header:focus {
+ border: 1px dotted gray;
+ }
+
+ .action-primary {
+ @extend %btn-primary-inverse;
+
+ width: 100%;
+ margin: 16px 2px 8px 2px;
+ }
+
+ .wrapper-tag-content {
+ background-color: $white;
+
+ .content-taxonomies {
+ display: flex;
+ flex-direction: column;
+ padding-top: 10px;
+
+ .tagging-label {
+ display: flex;
+ padding: 4px 0px;
+
+ .tagging-label-value {
+ display: inline-block;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .tagging-label-count {
+ display: inline-block;
+ margin: 0 0.5em;
+ }
+ }
+
+ .tagging-label:hover,
+ .tagging-label:focus {
+ color: $blue;
+ }
+
+ .icon {
+ margin-left: 5px;
+ }
+ }
+ }
+ }
+ }
+
// versioning widget
.unit-publish-history {
.wrapper-last-publish {
diff --git a/cms/templates/container.html b/cms/templates/container.html
index 2f83c0c6d5e1..ee2b56860187 100644
--- a/cms/templates/container.html
+++ b/cms/templates/container.html
@@ -213,7 +213,10 @@ ${_("Location ID")}
)}
-
+
+ % if show_unit_tags:
+
+ % endif
% endif
@@ -238,4 +241,7 @@ ${_("Location ID")}
+
+
+
%block>
diff --git a/cms/templates/js/tag-list.underscore b/cms/templates/js/tag-list.underscore
new file mode 100644
index 000000000000..a006eb111b5b
--- /dev/null
+++ b/cms/templates/js/tag-list.underscore
@@ -0,0 +1,32 @@
+<% if (tags !== null) { %>
+
+<% } %>
diff --git a/cms/templates/studio_xblock_wrapper.html b/cms/templates/studio_xblock_wrapper.html
index a027cc764bd7..38a61ff49106 100644
--- a/cms/templates/studio_xblock_wrapper.html
+++ b/cms/templates/studio_xblock_wrapper.html
@@ -7,13 +7,14 @@
from openedx.core.djangolib.js_utils import (
dump_js_escaped_json, js_escaped_string
)
-from cms.djangoapps.contentstore.toggles import use_new_text_editor, use_new_problem_editor, use_new_video_editor, use_video_gallery_flow
+from cms.djangoapps.contentstore.toggles import use_new_text_editor, use_new_problem_editor, use_new_video_editor, use_video_gallery_flow, use_tagging_taxonomy_list_page
%>
<%
use_new_editor_text = use_new_text_editor()
use_new_editor_video = use_new_video_editor()
use_new_editor_problem = use_new_problem_editor()
use_new_video_gallery_flow = use_video_gallery_flow()
+use_tagging = use_tagging_taxonomy_list_page()
xblock_url = xblock_studio_url(xblock)
show_inline = xblock.has_children and not xblock_url
section_class = "level-nesting" if show_inline else "level-element"
@@ -118,6 +119,11 @@
${_("Duplicate")}
% endif
+ % if use_tagging:
+
+ ${_("Manage tags")}
+
+ % endif
% if can_move:
${_("Move")}