static/js/zamboni/themes_review.js (458 lines of code) (raw):

import $ from 'jquery'; import _ from 'underscore'; import { _pd } from '../lib/prevent-default'; import { keys } from '../zamboni/keys'; import { search_result_row_template, search_results_template, } from './themes_review_templates'; /* jQuery.ScrollTo by Ariel Flesler */ $.fn.scrollTo = function (opts) { if (!this.length) return this; opts = $.extend( { duration: 250, marginTop: 0, complete: undefined, }, opts || {}, ); let top = this.offset().top - opts.marginTop; $('html, body').animate( { scrollTop: top }, opts.duration, undefined, opts.complete, ); return this; }; function fieldFocused(e) { let tags = /input|keygen|meter|option|output|progress|select|textarea/i; return tags.test(e.target.nodeName); } (function () { let win = $(window); let doc = $(document); $.fn.themeQueue = function () { return this.each(function () { let queue = this; let currentTheme = 0; let cacheQueueHeight; let $queueContext = $('.queue-context'); let actionConstants = $queueContext.data('actions'); let themesList = $('div.theme', queue); let themes = themesList .map(function () { return { element: this, top: 0, }; }) .get(); function nthTheme(i) { return themesList[i]; } doc.scroll( _.throttle(function () { updateMetrics(); let i = findCurrentTheme(); if (i >= 0 && i != currentTheme) { switchTheme(findCurrentTheme()); } // Undo sidebar-truncation fix in goToTheme if user goes // into free-scrolling mode. if (i === 0) { $('.sidebar').removeClass('lineup'); } }, 250), ); doc.keyup(function (e) { if (!$(queue).hasClass('shortcuts')) return; // Ignore key-bindings when textarea focused. if (fieldFocused(e) && e.which != keys.ENTER) return; // For using Enter to submit textareas. if (e.which == keys.ENTER && keys.ENTER in keymap) { keymap[keys.ENTER](); } let key = String.fromCharCode(e.which).toLowerCase(); if (!(key in keymap)) { return; } let action = keymap[key]; if (action && !e.ctrlKey && !e.altKey && !e.metaKey) { themeActions[action[0]](currentTheme, action[1]); } }); // Pressing Enter in text field doesn't add carriage return. $('textarea').keypress(function (e) { if (e.keyCode == keys.ENTER) { e.preventDefault(); } }); $('.theme', queue).removeClass('active'); updateMetrics(); switchTheme(findCurrentTheme()); function updateMetrics() { let queueHeight = $(queue).height(); if (queueHeight === cacheQueueHeight) return; cacheQueueHeight = queueHeight; $.each(themes, function (i, obj) { let elem = $(obj.element); obj.top = elem.offset().top + elem.outerHeight() / 2; }); } function getThemeParent(elem) { // Given an element (like an approve button), // return the theme for which it is related to. return $(elem).closest('.theme').data('id'); } function goToTheme(i, delay, duration) { delay = delay || 0; duration = duration || 250; setTimeout(function () { if (i >= 0 && i < themes.length) { $(themes[i].element).scrollTo({ duration: duration, marginTop: 20, }); } }, delay); $('.rq-dropdown').hide(); } function switchTheme(i) { if (!themes[currentTheme]) { return; } $(themes[currentTheme].element).removeClass('active'); $(themes[i].element).addClass('active'); vertAlignSidebar(win, $('.theme.active')); currentTheme = i; $('.rq-dropdown').hide(); } function findCurrentTheme() { // Uses location of the window within the page to determine // which theme we're currently looking at. // $(window).scroll() fires too early. if (!themes[currentTheme]) { return 0; } let pageTop = win.scrollTop(); if (pageTop <= themes[currentTheme].top) { for (let i = currentTheme - 1; i >= 0; i--) { if (themes[i].top < pageTop) { return i + 1; } } } else { for (let i = currentTheme; i < themes.length; i++) { // Scroll down the themes until we find a theme // that is at the top of our page. That is our current // theme. if (pageTop <= themes[i].top) { return i; } } } } let keymap = { j: ['next', null], k: ['prev', null], a: ['approve', null], r: ['reject_reason', null], d: ['duplicate', null], f: ['flag', null], m: ['moreinfo', null], }; // keymap[0] = ['other_reject_reason', 0]; for (let j = 1; j <= 9; j++) { keymap[j] = ['reject', j]; } function setReviewed(i, action) { let ac = actionConstants; let actionMap = {}; actionMap[ac.moreinfo] = [gettext('Requested Info'), 'blue']; actionMap[ac.flagged] = [gettext('Flagged'), 'red']; actionMap[ac.duplicate] = [gettext('Duplicate'), 'red']; actionMap[ac.reject] = [gettext('Rejected'), 'red']; actionMap[ac.approve] = [gettext('Approved'), 'green']; let text = actionMap[action][0]; let color = actionMap[action][1]; $(nthTheme(i)).addClass('reviewed'); $('.status', nthTheme(i)) .removeClass('red blue green') .addClass('reviewed ' + color) .find('.status-text') .text(text); $('#reviewed-count').text($('div.theme.reviewed').length); if ($(queue).hasClass('advance')) { goToTheme(i + 1, 250); } else { delete keymap[keys.ENTER]; $('.rq-dropdown').hide(); } } let isRejecting = false; $(document).on( 'click', 'li.reject_reason', _pd(function (e) { if (isRejecting) { let i = getThemeParent(e.currentTarget); let rejectId = $(this).data('id'); if (rejectId === 0) { themeActions.other_reject_reason(i); } else { themeActions.reject(i, rejectId); } } }), ); let themeActions = { next: function (i) { goToTheme(i + 1); }, prev: function (i) { goToTheme(i - 1); }, approve: function (i) { $('input.action', nthTheme(i)).val(actionConstants.approve); setReviewed(i, actionConstants.approve); }, reject_reason: function (i) { // Open up dropdown of rejection reasons and set up // key and click-bindings for choosing a reason. This // function does not actually do the rejecting as the // rejecting is only done once a reason is supplied. $('.rq-dropdown:not(.reject-reason-dropdown)').hide(); $('.reject-reason-dropdown', nthTheme(i)).toggle(); isRejecting = true; }, other_reject_reason: function (i) { if (!isRejecting) { return; } // Open text area to enter in a custom rejection reason. $('.rq-dropdown:not(.reject-reason-detail-dropdown)').hide(); $('.reject-reason-detail-dropdown', nthTheme(i)).toggle(); let textArea = $( '.reject-reason-detail-dropdown textarea', nthTheme(i), ).focus(); // Submit link/URL of the duplicate. let submit = function () { if (textArea.val()) { $('input.comment', nthTheme(i)).val(textArea.val()); textArea.blur(); themeActions.reject(i, 0); } else { $( '.reject-reason-detail-dropdown .error-required', nthTheme(i), ).show(); } }; keymap[keys.ENTER] = submit; $('.reject-reason-detail-dropdown button').click(_pd(submit)); }, reject: function (i, rejectId) { if (!isRejecting) { return; } // Given the rejection reason, does the actual rejection of // the Theme. $('input.action', nthTheme(i)).val(actionConstants.reject); $('input.reject-reason', nthTheme(i)).val(rejectId); setReviewed(i, actionConstants.reject); isRejecting = false; }, duplicate: function (i) { // Open up dropdown to enter ID/URL of duplicate. $('.rq-dropdown:not(.duplicate-dropdown)').hide(); $('.duplicate-dropdown', nthTheme(i)).toggle(); let textArea = $('.duplicate-dropdown textarea', nthTheme(i)).focus(); // Submit link/URL of the duplicate. let submit = function () { if (textArea.val()) { $('input.action', nthTheme(i)).val(actionConstants.duplicate); $('input.comment', nthTheme(i)).val(textArea.val()); textArea.blur(); setReviewed(i, actionConstants.duplicate); } else { $('.duplicate-dropdown .error-required', nthTheme(i)).show(); } }; keymap[keys.ENTER] = submit; $('.duplicate-dropdown button').click(_pd(submit)); }, flag: function (i) { // Open up dropdown to enter reason for flagging. $('.rq-dropdown:not(.flag-dropdown)').hide(); $('.flag-dropdown', nthTheme(i)).toggle(); let textArea = $('.flag-dropdown textarea', nthTheme(i)).focus(); // Submit link/URL of the flag. let submit = function () { if (textArea.val()) { $('input.action', nthTheme(i)).val(actionConstants.flag); $('input.comment', nthTheme(i)).val(textArea.val()); textArea.blur(); setReviewed(i, actionConstants.flagged); } else { $('.flag-dropdown .error-required', nthTheme(i)).show(); } }; keymap[keys.ENTER] = submit; $('.flag-dropdown button').click(_pd(submit)); }, moreinfo: function (i) { // Open up dropdown to enter ID/URL of moreinfo. $('.rq-dropdown:not(.moreinfo-dropdown)').hide(); $('.moreinfo-dropdown', nthTheme(i)).toggle(); let textArea = $('.moreinfo-dropdown textarea', nthTheme(i)).focus(); // Submit link/URL of the moreinfo. let submit = function () { if (textArea.val()) { $('input.action', nthTheme(i)).val(actionConstants.moreinfo); $('input.comment', nthTheme(i)).val(textArea.val()); textArea.blur(); setReviewed(i, actionConstants.moreinfo); } else { $('.moreinfo-dropdown .error-required', nthTheme(i)).show(); } }; keymap[keys.ENTER] = submit; $('.moreinfo-dropdown button').click(_pd(submit)); }, clearReview: function (i) { $( 'input.action, input.comment, input.reject-reason', nthTheme(i), ).prop('value', ''); $(nthTheme(i)).removeClass('reviewed'); $('.status', nthTheme(i)).removeClass('reviewed'); $('#reviewed-count').text($('div.theme.reviewed').length); }, }; $(document) .on( 'click', 'button.approve', _pd(function (e) { themeActions.approve(getThemeParent(e.currentTarget)); }), ) .on( 'click', 'button.reject', _pd(function (e) { themeActions.reject_reason(getThemeParent(e.currentTarget)); }), ) .on( 'click', 'button.duplicate', _pd(function (e) { themeActions.duplicate(getThemeParent(e.currentTarget)); }), ) .on( 'click', 'button.flag', _pd(function (e) { themeActions.flag(getThemeParent(e.currentTarget)); }), ) .on( 'click', 'button.moreinfo', _pd(function (e) { themeActions.moreinfo(getThemeParent(e.currentTarget)); }), ) .on( 'click', '.clear-review', _pd(function (e) { themeActions.clearReview(getThemeParent(e.currentTarget)); }), ); }); }; $.fn.themeQueueOptions = function (queueSelector) { return this.each(function () { let self = this; $('input', self).click(onChange); $('select', self).change(onChange); onChange(); function onChange(e) { let category = $('#rq-category', self).val(); let advance = $('#rq-advance:checked', self).val(); let shortcuts = $('#rq-shortcuts:checked', self).val(); $(queueSelector) .toggleClass('advance', !!advance) .toggleClass('shortcuts', !!shortcuts); } }); }; })(); function vertAlignSidebar(win) { let activeThemeTop = $('.theme.active').offset().top - win.scrollTop(); $('.sidebar .align.fixed').css('top', activeThemeTop + 'px'); } $(document).ready(function () { let $theme_queue = $('.theme-queue'); if ($theme_queue.length) { $('.zoombox').zoomBox(); $('.zoombox img').previewPersona(); $theme_queue.themeQueue(); $('.sidebar').themeQueueOptions('.theme-queue'); $('#commit').click( _pd(function (e) { $('#theme-queue-form').submit(); }), ); // Align sidebar with active theme. if ($('.theme.active').length) { let win = $(window); win.scroll( _.throttle(function () { vertAlignSidebar(win); }, 100), ); vertAlignSidebar(win); } // If daily message is present, align fixed sidebar. if (['none', undefined].indexOf($('.daily-message').css('display')) < 0) { let $sidebar = $('.sidebar .align.fixed'); let top = parseInt($sidebar.css('top'), 10) + 82; $sidebar.css('top', top + 'px'); } } if ($('.theme-search').length) { initSearch(); } }); function initSearch() { let no_results = '<p class="no-results">' + gettext('No results found') + '</p>'; let $clear = $('.clear-queue-search'), $appQueue = $('.search-toggle'), $search = $('.queue-search'), $searchIsland = $('#search-island'); if ($search.length) { let apiUrl = $search.data('api-url'); let review_url = $search.data('review-url'); let statuses = $searchIsland.data('statuses'); $('form', $search).submit( _pd(function () { let $form = $(this); $.get(apiUrl, $form.serialize()).done(function (data) { // Hide app queue. $appQueue.hide(); $clear.show(); // Show results. if (data.meta.total_count === 0) { $searchIsland.html(no_results).show().removeClass('hidden'); } else { let results = []; $.each(data.objects, function (i, item) { item = buildThemeResultRow(item, review_url, statuses); results.push(search_result_row_template(item)); }); $searchIsland.html( search_results_template({ rows: results.join('') }), ); $searchIsland.removeClass('hidden').show(); } }); }), ); } } function buildThemeResultRow(theme, review_url, statuses) { // Add some extra pretty attrs for the template. theme.name = theme.name[0]; // Rather resolve URLs in backend, infer from slug. theme.review_url = review_url.replace('__slug__', theme.slug); theme.status = statuses[theme.status]; return theme; }