diff options
| author | Tiffany Chang (tic) <tic@odoo.com> | 2020-01-30 09:46:31 +0000 |
|---|---|---|
| committer | Thibault Delavallée <tde@odoo.com> | 2020-03-05 10:53:45 +0000 |
| commit | 27005ebdbbab7e0b173ffe0b0b9707963579a0cc (patch) | |
| tree | 71735343617f7d4870eb4b6822307d995a4c96d6 /addons/website_slides/static/src | |
| parent | ff828a3e0c5386dc54e6a46fd71de9272ef3b691 (diff) | |
[IMP] website_slides: Add course tags creation to frontend
This commit makes it possible for course tags to be added via the
frontend. To support this, course tags were also made viewable in a
specific course's website since previously these tags were only visible
in the courses frontpage (i.e. within the course cards). "Add Tag" is
placed after course page's tags. Note that due to the ability to make new
tags within this "Add Tag" modal restricts addition to only 1 tag at a
time since new tags require a "Tap Group" to be assigned and handling a
mix of new/existing tags would get confusing and complicated.
Supports Task: 2128166
PR #44365
Diffstat (limited to 'addons/website_slides/static/src')
4 files changed, 402 insertions, 1 deletions
diff --git a/addons/website_slides/static/src/js/slides_course_tag_add.js b/addons/website_slides/static/src/js/slides_course_tag_add.js new file mode 100644 index 00000000000..dcab7176a93 --- /dev/null +++ b/addons/website_slides/static/src/js/slides_course_tag_add.js @@ -0,0 +1,330 @@ +odoo.define('website_slides.channel_tag.add', function (require) { +'use strict'; + +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var publicWidget = require('web.public.widget'); + +var _t = core._t; + +var TagCourseDialog = Dialog.extend({ + template: 'website.slides.tag.add', + events: _.extend({}, Dialog.prototype.events, { + 'change input#tag_id' : '_onChangeTag', + }), + + /** + * @override + * @param {Object} parent + * @param {Object} options holding channelId + * + */ + init: function (parent, options) { + options = _.defaults(options || {}, { + title: _t("Add a tag"), + size: 'medium', + buttons: [{ + text: _t("Add"), + classes: 'btn-primary', + click: this._onClickFormSubmit.bind(this) + }, { + text: _t("Discard"), + close: true + }] + }); + + this.channelID = parseInt(options.channelId, 10); + this.tagIds = options.channelTagIds || []; + this._super(parent, options); + }, + start: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + self._bindSelect2Dropdown(); + self._hideTagGroup(); + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * 'Tag' and 'Tag Group' management for select2 + * + * @private + */ + _bindSelect2Dropdown: function () { + var self = this; + this.$('#tag_id').select2(this._select2Wrapper(_t('Tag'), + function () { + return self._rpc({ + route: '/slides/channel/tag/search_read', + params: { + fields: ['name'], + domain: [['id','not in',self.tagIds]], + } + }); + }) + ); + this.$('#tag_group_id').select2(this._select2Wrapper(_t('Tag Group (required for new tags)'), + function () { + return self._rpc({ + route: '/slides/channel/tag/group/search_read', + params: { + fields: ['name'], + domain: [], + } + }); + }) + ); + }, + + /** + * Wrapper for select2 load data from server at once and store it. + * + * @private + * @param {String} Placeholder for element. + * @param {Function} Function to fetch data from remote location should return a Promise + * resolved data should be array of object with id and name. eg. [{'id': id, 'name': 'text'}, ...] + * @param {String} [nameKey='name'] (optional) the name key of the returned record + * ('name' if not provided) + * @returns {Object} select2 wrapper object + */ + _select2Wrapper: function (tag, fetchFNC, nameKey) { + nameKey = nameKey || 'name'; + + var values = { + width: '100%', + placeholder: tag, + allowClear: true, + formatNoMatches: false, + selection_data: false, + fetch_rpc_fnc: fetchFNC, + formatSelection: function (data) { + if (data.tag) { + data.text = data.tag; + } + return data.text; + }, + createSearchChoice: function (term, data) { + var addedTags = $(this.opts.element).select2('data'); + if (_.filter(_.union(addedTags, data), function (tag) { + return tag.text.toLowerCase().localeCompare(term.toLowerCase()) === 0; + }).length === 0) { + if (this.opts.can_create) { + return { + id: _.uniqueId('tag_'), + create: true, + tag: term, + text: _.str.sprintf(_t("Create new %s '%s'"), tag, term), + }; + } else { + return undefined; + } + } + }, + fill_data: function (query, data) { + var that = this, + tags = {results: []}; + _.each(data, function (obj) { + if (that.matcher(query.term, obj[nameKey])) { + tags.results.push({id: obj.id, text: obj[nameKey]}); + } + }); + query.callback(tags); + }, + query: function (query) { + var that = this; + // fetch data only once and store it + if (!this.selection_data) { + this.fetch_rpc_fnc().then(function (data) { + that.can_create = data.can_create; + that.fill_data(query, data.read_results); + that.selection_data = data.read_results; + }); + } else { + this.fill_data(query, this.selection_data); + } + } + }; + return values; + }, + + /** + * Get value for tag_id and [when appropriate] tag_group_id to send to server + * + * @private + */ + _getSelect2DropdownValues: function () { + var result = {}; + var tag = this.$('#tag_id').select2('data'); + if (tag) { + if (tag.create) { + // new tag + var group = this.$('#tag_group_id').select2('data'); + if(group) { + result['tag_id'] = [0, {'name': tag.text}] + if (group.create) { + // new tag group + result['group_id'] = [0, {'name': group.text}]; + } else { + result['group_id'] = [group.id]; + } + } + } else { + result['tag_id'] = [tag.id]; + } + } + return result; + }, + + /** + * Select2 fields makes the "required" input hidden on the interface. + * Therefore we need to make a method to visually provide this requirement + * feedback to users. "tag group" field should only need this when a new tag + * is created. + * + * @private + */ + _formValidate: function ($form) { + $form.addClass('was-validated'); + var result = $form[0].checkValidity(); + + var $tagInput = this.$('#tag_id'); + if ($tagInput.length !== 0){ + var $tagSelect2Container = $tagInput + .closest('.form-group') + .find('.select2-container'); + $tagSelect2Container.removeClass('is-invalid is-valid'); + if ($tagInput.is(':invalid')) { + $tagSelect2Container.addClass('is-invalid'); + } else if ($tagInput.is(':valid')) { + $tagSelect2Container.addClass('is-valid'); + var $tagGroupInput = this.$('#tag_group_id'); + if ($tagGroupInput.length !== 0){ + var $tagGroupSelect2Container = $tagGroupInput + .closest('.form-group') + .find('.select2-container'); + if ($tagGroupInput.is(':invalid')) { + $tagGroupSelect2Container.addClass('is-invalid'); + } else if ($tagGroupInput.is(':valid')) { + $tagGroupSelect2Container.addClass('is-valid'); + } + } + } + } + return result; + }, + + _alertDisplay: function (message) { + this._alertRemove(); + $('<div/>', { + "class": 'alert alert-warning', + role: 'alert' + }).text(message).insertBefore(this.$('form')); + }, + _alertRemove: function () { + this.$('.alert-warning').remove(); + }, + + /** + * When the user IS NOT creating a new tag, this function hides the group tag field + * and makes it not required. Since the select2 field makes an extra container, this + * needs to be hidden along with the group tag input field and its label. + * + * @private + */ + _hideTagGroup: function () { + var $tag_group_id = this.$('#tag_group_id'); + var $tagGroupSelect2Container = $tag_group_id.closest('.form-group'); + $tagGroupSelect2Container.hide(); + $tag_group_id.removeAttr("required"); + $tag_group_id.select2("val", ""); + }, + + /** + * When the user IS creating a new tag, this function shows the field and + * makes it required. Since the select2 field makes an extra container, this + * needs to be shown along with the group input field and its label. + * + * @private + */ + _showTagGroup: function () { + var $tag_group_id = this.$('#tag_group_id'); + var $tagGroupSelect2Container = $tag_group_id.closest('.form-group'); + $tagGroupSelect2Container.show(); + $tag_group_id.attr("required", "required"); + }, + + //-------------------------------------------------------------------------- + // Handler + //-------------------------------------------------------------------------- + _onClickFormSubmit: function () { + var self = this; + var $form = this.$('#slides_channel_tag_add_form'); + if (this._formValidate($form)) { + var values = this._getSelect2DropdownValues(); + return this._rpc({ + route: '/slides/channel/tag/add', + params: {'channel_id': this.channelID, + 'tag_id': values.tag_id, + 'group_id': values.group_id}, + }).then(function (data) { + if (data.error) { + self._alertDisplay(data.error); + } else { + window.location = data.url; + } + }); + } + }, + + _onChangeTag: function (ev) { + var self = this; + var tag = $(ev.currentTarget).select2('data'); + if (tag && tag.create) { + self._showTagGroup(); + } else { + self._hideTagGroup(); + } + }, +}); + +publicWidget.registry.websiteSlidesTag = publicWidget.Widget.extend({ + selector: '.o_wslides_js_channel_tag_add', + xmlDependencies: ['/website_slides/static/src/xml/website_slides_channel_tag.xml'], + events: { + 'click': '_onAddTagClick', + }, + + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _openDialog: function ($element) { + var data = $element.data(); + return new TagCourseDialog(this, data).open(); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onAddTagClick: function (ev) { + ev.preventDefault(); + this._openDialog($(ev.currentTarget)); + }, +}); + +return { + TagCourseDialog: TagCourseDialog, + websiteSlidesTag: publicWidget.registry.websiteSlidesTag +}; + +}); diff --git a/addons/website_slides/static/src/tests/tours/slides_course_publisher.js b/addons/website_slides/static/src/tests/tours/slides_course_publisher.js index 2b9aad17f92..f357a2296c8 100644 --- a/addons/website_slides/static/src/tests/tours/slides_course_publisher.js +++ b/addons/website_slides/static/src/tests/tours/slides_course_publisher.js @@ -75,6 +75,8 @@ tour.register('course_publisher', { run: function () {} // check membership } ].concat( + slidesTourTools.addExistingCourseTag(), + slidesTourTools.addNewCourseTag('The Most Awesome Course'), slidesTourTools.addSection('Introduction'), slidesTourTools.addVideoToSection('Introduction'), [{ diff --git a/addons/website_slides/static/src/tests/tours/slides_tour_tools.js b/addons/website_slides/static/src/tests/tours/slides_tour_tools.js index 2290cd4e982..5b5f525cfeb 100644 --- a/addons/website_slides/static/src/tests/tours/slides_tour_tools.js +++ b/addons/website_slides/static/src/tests/tours/slides_tour_tools.js @@ -73,7 +73,7 @@ var addWebPageToSection = function (sectionName, pageName) { content: 'eLearning: click on tags', trigger: 'ul.select2-choices:first', }, { - content: 'eLearning: select gardener tag', + content: 'eLearning: select Theory tag', trigger: 'div.select2-result-label:contains("Theory")', in_modal: false, }, { @@ -83,10 +83,59 @@ var addWebPageToSection = function (sectionName, pageName) { }]; }; +var addExistingCourseTag = function () { + return [ +{ + content: 'eLearning: click on Add Tag', + trigger: 'a.o_wslides_js_channel_tag_add', +}, { + content: 'eLearning: click on tag dropdown', + trigger: 'a.select2-choice:first', +}, { + content: 'eLearning: select advanced tag', + trigger: 'div.select2-result-label:contains("Advanced")', + in_modal: false, +}, { + content: 'eLearning: add existing course tag', + trigger: 'footer.modal-footer button:contains("Add")' +}]; +}; + +var addNewCourseTag = function (courseTagName) { + return [ +{ + content: 'eLearning: click on Add Tag', + trigger: 'a.o_wslides_js_channel_tag_add', +}, { + content: 'eLearning: click on tag dropdown', + trigger: 'a.select2-choice:first', +}, { + content: 'eLearning: add a new course tag', + trigger: 'a.select2-choice:first', + run: function () { + // directly add new tag since we can assume select2 works correctly + $('#tag_id').select2('data', {id:'123', text: courseTagName, create: true}); + $('#tag_id').trigger('change'); + } +}, { + content: 'eLearning: click on tag group dropdown', + trigger: 'a.select2-choice:last', +}, { + content: 'eLearning: select Tags tag group', + trigger: 'div.select2-result-label:contains("Tags")', + in_modal: false, +}, { + content: 'eLearning: add new course tag', + trigger: 'footer.modal-footer button:contains("Add")' +}]; +}; + return { addSection: addSection, addVideoToSection: addVideoToSection, addWebPageToSection: addWebPageToSection, + addExistingCourseTag: addExistingCourseTag, + addNewCourseTag: addNewCourseTag, }; }); diff --git a/addons/website_slides/static/src/xml/website_slides_channel_tag.xml b/addons/website_slides/static/src/xml/website_slides_channel_tag.xml new file mode 100644 index 00000000000..081032e0a4e --- /dev/null +++ b/addons/website_slides/static/src/xml/website_slides_channel_tag.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<templates xml:space="preserve"> + + <t t-name="website.slides.tag.add"> + <div class="form-group"> + <form action="/slides/channel/tag/add" method="POST" id="slides_channel_tag_add_form"> + <div class="form-group"> + <label for="tag_id" class="col-form-label">Tag</label> + <input class="form-control" id="tag_id" required="required"/> + </div> + <div class="form-group"> + <label id="tag_group_label" for="tag_group_id" class="col-form-label">Tag Group</label> + <input class="form-control" id="tag_group_id"/> + </div> + </form> + </div> + </t> + +</templates> |
