From 9d0424cd3f57a95b5c46057663934b2064d25535 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Chris=20Ch=C3=A1vez?=
Date: Tue, 21 Nov 2023 15:12:17 -0500
Subject: [PATCH] feat: Display tags on the Unit page in Studio
(feature-flagged) - Take 2 (#33761)
---
cms/djangoapps/contentstore/utils.py | 8 +-
cms/djangoapps/contentstore/views/block.py | 9 +-
.../contentstore/views/component.py | 101 ++++++++++-
cms/static/js/models/xblock_info.js | 6 +-
cms/static/js/views/course_outline.js | 38 +---
cms/static/js/views/pages/container.js | 23 ++-
.../js/views/pages/container_subviews.js | 171 +++++++++++++++++-
.../js/views/utils/tagging_drawer_utils.js | 55 ++++++
cms/static/sass/elements/_controls.scss | 28 +++
.../partials/cms/theme/_variables-v1.scss | 2 +-
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 | 12 +-
14 files changed, 511 insertions(+), 53 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 a4c69e55b7ea..190b7dca3473 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -1,7 +1,7 @@
"""
Common utility functions useful throughout the contentstore
"""
-
+from __future__ import annotations
import logging
from contextlib import contextmanager
from datetime import datetime
@@ -395,7 +395,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.
@@ -404,7 +404,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/block.py b/cms/djangoapps/contentstore/views/block.py
index ecaa6928c0b9..f27b3f955046 100644
--- a/cms/djangoapps/contentstore/views/block.py
+++ b/cms/djangoapps/contentstore/views/block.py
@@ -68,7 +68,8 @@
get_visibility_partition_info,
has_children_visible_to_specific_partition_groups,
is_currently_visible_to_students,
- is_self_paced
+ is_self_paced,
+ get_taxonomy_tags_widget_url,
)
from .helpers import (
create_xblock,
@@ -1141,7 +1142,7 @@ def _get_gating_info(course, xblock):
@pluggable_override('OVERRIDE_CREATE_XBLOCK_INFO')
def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=False, include_child_info=False, # lint-amnesty, pylint: disable=too-many-statements
course_outline=False, include_children_predicate=NEVER, parent_xblock=None, graders=None,
- user=None, course=None, is_concise=False):
+ user=None, course=None, is_concise=False, tags=None):
"""
Creates the information needed for client-side XBlockInfo.
@@ -1362,6 +1363,10 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
xblock_info['ancestor_has_staff_lock'] = ancestor_has_staff_lock(xblock, parent_xblock)
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/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py
index b7dd09d5c7cb..6be62ed2b1f4 100644
--- a/cms/djangoapps/contentstore/views/component.py
+++ b/cms/djangoapps/contentstore/views/component.py
@@ -24,9 +24,13 @@
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_tagging.api import get_object_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
@@ -55,7 +59,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",
]
@@ -103,7 +107,7 @@ def _load_mixed_class(category):
@require_GET
@login_required
-def container_handler(request, usage_key_string):
+def container_handler(request, usage_key_string): # pylint: disable=too-many-statements
"""
The restful handler for container xblock requests.
@@ -170,9 +174,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)
@@ -205,7 +214,8 @@ def container_handler(request, usage_key_string):
'xblock_info': xblock_info,
'draft_preview_link': preview_lms_link,
'published_preview_link': lms_link,
- 'templates': CONTAINER_TEMPLATES
+ 'templates': CONTAINER_TEMPLATES,
+ 'show_unit_tags': show_unit_tags,
})
else:
return HttpResponseBadRequest("Only supports HTML requests")
@@ -583,3 +593,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_object_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/static/js/models/xblock_info.js b/cms/static/js/models/xblock_info.js
index 983edc4e5648..d2227218fad3 100644
--- a/cms/static/js/models/xblock_info.js
+++ b/cms/static/js/models/xblock_info.js
@@ -167,7 +167,11 @@ define(
highlights_enabled: false,
highlights_enabled_for_messaging: false,
highlights_preview_only: true,
- highlights_doc_url: ''
+ highlights_doc_url: '',
+ /**
+ * 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 ed30c1b34ddc..61ce21423aa1 100644
--- a/cms/static/js/views/course_outline.js
+++ b/cms/static/js/views/course_outline.js
@@ -9,10 +9,11 @@
* - adding units will automatically redirect to the unit page rather than showing them inline
*/
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/models/xblock_outline_info', 'js/views/modals/course_outline_modals', 'js/utils/drag_and_drop',
+ 'js/views/utils/tagging_drawer_utils',],
function(
$, _, XBlockOutlineView, ViewUtils, XBlockViewUtils,
- XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger
+ XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger, TaggingDrawerUtils
) {
var CourseOutlineView = XBlockOutlineView.extend({
// takes XBlockOutlineInfo as a model
@@ -215,41 +216,12 @@ function(
}
},
- 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 4ce217ff0d35..5a69482c34c8 100644
--- a/cms/static/js/views/pages/container.js
+++ b/cms/static/js/views/pages/container.js
@@ -6,10 +6,11 @@ define(['jquery', 'underscore', 'backbone', 'gettext', 'js/views/pages/base_page
'common/js/components/utils/view_utils', 'js/views/container', 'js/views/xblock',
'js/views/components/add_xblock', 'js/views/modals/edit_xblock', 'js/views/modals/move_xblock_modal',
'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'],
+ 'js/views/pages/container_subviews', 'js/views/unit_outline', 'js/views/utils/xblock_utils',
+ 'js/views/utils/tagging_drawer_utils'],
function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView, AddXBlockComponent,
EditXBlockModal, MoveXBlockModal, XBlockInfo, XBlockStringFieldEditor, XBlockAccessEditor,
- ContainerSubviews, UnitOutlineView, XBlockUtils) {
+ ContainerSubviews, UnitOutlineView, XBlockUtils, TaggingDrawerUtils) {
'use strict';
var XBlockContainerPage = BasePage.extend({
// takes XBlockInfo as a model
@@ -22,7 +23,8 @@ function($, _, Backbone, gettext, BasePage, ViewUtils, ContainerView, XBlockView
'click .move-button': 'showMoveXBlockModal',
'click .delete-button': 'deleteXBlock',
'click .show-actions-menu-button': 'showXBlockActionsMenu',
- 'click .new-component-button': 'scrollToNewComponentButtons'
+ 'click .new-component-button': 'scrollToNewComponentButtons',
+ 'click .tags-button': 'openManageTags',
},
options: {
@@ -92,6 +94,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
@@ -119,6 +127,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);
@@ -144,6 +153,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();
@@ -247,6 +257,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 cdcbca619f02..74a012732a76 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';
@@ -294,11 +295,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..227c19e9cd2a
--- /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/partials/cms/theme/_variables-v1.scss b/cms/static/sass/partials/cms/theme/_variables-v1.scss
index 96777b34f27a..9dd001d455cc 100644
--- a/cms/static/sass/partials/cms/theme/_variables-v1.scss
+++ b/cms/static/sass/partials/cms/theme/_variables-v1.scss
@@ -28,7 +28,7 @@ $fg-column: $gw-column;
$fg-gutter: $gw-gutter;
$fg-max-columns: 12;
$fg-max-width: 1280px;
-$fg-min-width: 900px;
+$fg-min-width: 995px;
// +Fonts
// ====================
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 ffafa8d96e26..e5a99b11a0c1 100644
--- a/cms/templates/container.html
+++ b/cms/templates/container.html
@@ -206,7 +206,10 @@ ${_("Location ID")}
Tip: ${Text(_('To create a link to this unit from an HTML component in this course, enter "/jump_to_id/" as the URL value.'))}
-
+
+ % if show_unit_tags:
+
+ % endif
% endif
@@ -231,4 +234,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 81a08a4d0105..b85ad79fbe4c 100644
--- a/cms/templates/studio_xblock_wrapper.html
+++ b/cms/templates/studio_xblock_wrapper.html
@@ -7,12 +7,13 @@
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
+from cms.djangoapps.contentstore.toggles import use_new_text_editor, use_new_problem_editor, use_new_video_editor, 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_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"
@@ -119,6 +120,15 @@
% endif
+ % if use_tagging:
+
+
+
+ ?
+ ${_("Manage tags")}
+
+
+ % endif
% endif
% if can_add and not enable_copy_paste: