From f0fe16d824abda98de23d7d7de80afbad429e4df Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 5 Apr 2018 16:45:19 +0200 Subject: [PATCH] FEATURE: implements minimum selection for select-kit --- .../discourse/templates/tag-groups-show.hbs | 2 +- .../components/mini-tag-chooser.js.es6 | 14 ++++-- .../select-kit/components/multi-select.js.es6 | 17 ++++++- .../select-kit/components/select-kit.js.es6 | 47 +++++++++++++------ .../components/single-select.js.es6 | 4 +- .../select-kit/components/tag-chooser.js.es6 | 2 +- .../javascripts/select-kit/mixins/tags.js.es6 | 2 +- .../templates/components/multi-select.hbs | 2 +- .../select-kit/select-kit-collection.hbs | 37 +++++++-------- .../templates/components/single-select.hbs | 2 +- .../common/select-kit/select-kit.scss | 13 +++-- config/locales/client.en.yml | 3 +- .../components/multi-select-test.js.es6 | 41 ++++++++++++++++ .../components/single-select-test.js.es6 | 41 ++++++++++++++++ test/javascripts/helpers/select-kit-helper.js | 11 +++++ 15 files changed, 186 insertions(+), 52 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/tag-groups-show.hbs b/app/assets/javascripts/discourse/templates/tag-groups-show.hbs index 032f0ce06eb..55dea80a7c4 100644 --- a/app/assets/javascripts/discourse/templates/tag-groups-show.hbs +++ b/app/assets/javascripts/discourse/templates/tag-groups-show.hbs @@ -15,7 +15,7 @@ {{tag-chooser tags=model.parent_tag_name everyTag=true - limit=1 + maximum=1 allowCreate=true filterPlaceholder="tagging.groups.parent_tag_placeholder"}} {{i18n 'tagging.groups.parent_tag_description'}} diff --git a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 index bc53b6ba335..740fd9e61be 100644 --- a/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 +++ b/app/assets/javascripts/select-kit/components/mini-tag-chooser.js.es6 @@ -34,12 +34,12 @@ export default ComboBox.extend(Tags, { }); }); - this.set("limit", parseInt(this.get("limit") || this.get("siteSettings.max_tags_per_topic"))); + this.set("maximum", parseInt(this.get("limit") || this.get("maximum") || this.get("siteSettings.max_tags_per_topic"))); }, - @computed("hasReachedLimit") - caretIcon(hasReachedLimit) { - return hasReachedLimit ? null : "plus fa-fw"; + @computed("hasReachedMaximum") + caretIcon(hasReachedMaximum) { + return hasReachedMaximum ? null : "plus fa-fw"; }, @computed("tags") @@ -135,6 +135,12 @@ export default ComboBox.extend(Tags, { content.label = joinedTags; } + if (!this.get("hasReachedMinimum") && isEmpty(this.get("selection"))) { + const key = this.get("minimumLabel") || "select_kit.min_content_not_reached"; + const label = I18n.t(key, { count: this.get("minimum") }); + content.title = content.name = content.label = label; + } + content.title = content.name = content.value = joinedTags; return content; diff --git a/app/assets/javascripts/select-kit/components/multi-select.js.es6 b/app/assets/javascripts/select-kit/components/multi-select.js.es6 index 0635f8b906a..9a9453445b5 100644 --- a/app/assets/javascripts/select-kit/components/multi-select.js.es6 +++ b/app/assets/javascripts/select-kit/components/multi-select.js.es6 @@ -140,10 +140,23 @@ export default SelectKitComponent.extend({ }, computeHeaderContent() { - return { + let content = { title: this.get("title"), selection: this.get("selection") }; + + if (this.get("noneLabel")) { + if (!this.get("hasSelection")) { + content.title = content.name = content.label = I18n.t(this.get("noneLabel")); + } + } else { + if (!this.get("hasReachedMinimum")) { + const key = this.get("minimumLabel") || "select_kit.min_content_not_reached"; + content.title = content.name = content.label = I18n.t(key, { count: this.get("minimum") }); + } + } + + return content; }, @computed("filter") @@ -154,7 +167,7 @@ export default SelectKitComponent.extend({ }, validateSelect() { - return this._super() && !this.get("hasReachedLimit"); + return this._super() && !this.get("hasReachedMaximum"); }, @computed("computedValues.[]", "computedContent.[]") diff --git a/app/assets/javascripts/select-kit/components/select-kit.js.es6 b/app/assets/javascripts/select-kit/components/select-kit.js.es6 index eab8ccb7315..a45da128fdd 100644 --- a/app/assets/javascripts/select-kit/components/select-kit.js.es6 +++ b/app/assets/javascripts/select-kit/components/select-kit.js.es6 @@ -25,7 +25,8 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi "isLeftAligned", "isRightAligned", "hasSelection", - "hasReachedLimit", + "hasReachedMaximum", + "hasReachedMinimum", ], isDisabled: false, isExpanded: false, @@ -71,6 +72,10 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi collectionHeader: null, allowAutoSelectFirst: true, highlightedSelection: null, + maximum: null, + minimum: null, + minimumLabel: null, + maximumLabel: null, init() { this._super(); @@ -188,14 +193,22 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi } }, - validateCreate() { return !this.get("hasReachedLimit"); }, + validateCreate() { return !this.get("hasReachedMaximum"); }, - validateSelect() { return !this.get("hasReachedLimit"); }, + validateSelect() { return !this.get("hasReachedMaximum"); }, - @computed("limit", "selection.[]") - hasReachedLimit(limit, selection) { - if (!limit) return false; - return selection.length >= limit; + @computed("maximum", "selection.[]") + hasReachedMaximum(maximum, selection) { + if (!maximum) return false; + selection = makeArray(selection); + return selection.length >= maximum; + }, + + @computed("minimum", "selection.[]") + hasReachedMinimum(minimum, selection) { + if (!minimum) return true; + selection = makeArray(selection); + return selection.length >= minimum; }, @computed("shouldFilter", "allowAny", "filter") @@ -212,10 +225,16 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi } }, - @computed("hasReachedLimit", "limit") - maxContentRow(hasReachedLimit, limit) { - if (hasReachedLimit) { - return I18n.t("select_kit.max_content_reached", { count: limit }); + @computed("hasReachedMaximum", "hasReachedMinimum", "isExpanded") + validationMessage(hasReachedMaximum, hasReachedMinimum) { + if (hasReachedMaximum && this.get("maximum")) { + const key = this.get("maximumLabel") || "select_kit.max_content_reached"; + return I18n.t(key, { count: this.get("maximum") }); + } + + if (!hasReachedMinimum && this.get("minimum")) { + const key = this.get("minimumLabel") || "select_kit.min_content_not_reached"; + return I18n.t(key, { count: this.get("minimum") }); } }, @@ -227,9 +246,9 @@ export default Ember.Component.extend(UtilsMixin, PluginApiMixin, DomHelpersMixi return false; }, - @computed("computedValue", "filter", "collectionComputedContent.[]", "hasReachedLimit", "isLoading") - shouldDisplayCreateRow(computedValue, filter, collectionComputedContent, hasReachedLimit, isLoading) { - if (isLoading || hasReachedLimit) return false; + @computed("computedValue", "filter", "collectionComputedContent.[]", "hasReachedMaximum", "isLoading") + shouldDisplayCreateRow(computedValue, filter, collectionComputedContent, hasReachedMaximum, isLoading) { + if (isLoading || hasReachedMaximum) return false; if (collectionComputedContent.map(c => c.value).includes(filter)) return false; if (this.get("allowAny") && filter.length > 0 && this.validateCreate(filter)) return true; return false; diff --git a/app/assets/javascripts/select-kit/components/single-select.js.es6 b/app/assets/javascripts/select-kit/components/single-select.js.es6 index 204ff3349c0..65e73840adf 100644 --- a/app/assets/javascripts/select-kit/components/single-select.js.es6 +++ b/app/assets/javascripts/select-kit/components/single-select.js.es6 @@ -91,7 +91,7 @@ export default SelectKitComponent.extend({ name: this.get("selection.name") || this.get("noneRowComputedContent.name") }; - if (!this.get("hasSelection") && this.get("noneLabel")) { + if (this.get("noneLabel") && !this.get("hasSelection")) { content.title = content.name = I18n.t(this.get("noneLabel")); } @@ -134,7 +134,7 @@ export default SelectKitComponent.extend({ return selection !== this.get("noneRowComputedContent") && !isNone(selection); }, - @computed("computedValue", "filter", "collectionComputedContent.[]", "hasReachedLimit") + @computed("computedValue", "filter", "collectionComputedContent.[]", "hasReachedMaximum", "hasReachedMinimum") shouldDisplayCreateRow(computedValue, filter) { return this._super() && computedValue !== filter; }, diff --git a/app/assets/javascripts/select-kit/components/tag-chooser.js.es6 b/app/assets/javascripts/select-kit/components/tag-chooser.js.es6 index 2ddd86b08aa..de5341c32ac 100644 --- a/app/assets/javascripts/select-kit/components/tag-chooser.js.es6 +++ b/app/assets/javascripts/select-kit/components/tag-chooser.js.es6 @@ -37,7 +37,7 @@ export default MultiSelectComponent.extend(Tags, { }); if (!this.get("unlimitedTagCount")) { - this.set("limit", parseInt(this.get("limit") || this.get("siteSettings.max_tags_per_topic"))); + this.set("maximum", parseInt(this.get("limit") || this.get("maximum") || this.get("siteSettings.max_tags_per_topic"))); } }, diff --git a/app/assets/javascripts/select-kit/mixins/tags.js.es6 b/app/assets/javascripts/select-kit/mixins/tags.js.es6 index e90ab473fdf..1c2e10f6f43 100644 --- a/app/assets/javascripts/select-kit/mixins/tags.js.es6 +++ b/app/assets/javascripts/select-kit/mixins/tags.js.es6 @@ -33,7 +33,7 @@ export default Ember.Mixin.create({ }, validateCreate(term) { - if (this.get("hasReachedLimit") || !this.site.get("can_create_tag")) { + if (this.get("hasReachedMaximum") || !this.site.get("can_create_tag")) { return false; } diff --git a/app/assets/javascripts/select-kit/templates/components/multi-select.hbs b/app/assets/javascripts/select-kit/templates/components/multi-select.hbs index b3d4f473645..d541cc2c1c4 100644 --- a/app/assets/javascripts/select-kit/templates/components/multi-select.hbs +++ b/app/assets/javascripts/select-kit/templates/components/multi-select.hbs @@ -41,7 +41,7 @@ computedValue=computedValue rowComponentOptions=rowComponentOptions noContentRow=noContentRow - maxContentRow=maxContentRow + validationMessage=validationMessage }} {{/unless}} {{/if}} diff --git a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs index 6a7089e9ecd..b673bd18d24 100644 --- a/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs +++ b/app/assets/javascripts/select-kit/templates/components/select-kit/select-kit-collection.hbs @@ -30,26 +30,25 @@ }} {{/if}} -{{#if maxContentRow}} -
  • - {{maxContentRow}} +{{#if noContentRow}} +
  • + {{noContentRow}}
  • {{else}} - {{#if noContentRow}} -
  • - {{noContentRow}} -
  • - {{else}} - {{#each collectionComputedContent as |computedContent|}} - {{component rowComponent - computedContent=computedContent - highlighted=highlighted - computedValue=computedValue - templateForRow=templateForRow - onClickRow=onClickRow - onMouseoverRow=onMouseoverRow - options=rowComponentOptions - }} - {{/each}} + {{#if validationMessage}} +
    + {{validationMessage}} +
    {{/if}} + {{#each collectionComputedContent as |computedContent|}} + {{component rowComponent + computedContent=computedContent + highlighted=highlighted + computedValue=computedValue + templateForRow=templateForRow + onClickRow=onClickRow + onMouseoverRow=onMouseoverRow + options=rowComponentOptions + }} + {{/each}} {{/if}} diff --git a/app/assets/javascripts/select-kit/templates/components/single-select.hbs b/app/assets/javascripts/select-kit/templates/components/single-select.hbs index 46128823d3a..e01241d3190 100644 --- a/app/assets/javascripts/select-kit/templates/components/single-select.hbs +++ b/app/assets/javascripts/select-kit/templates/components/single-select.hbs @@ -40,7 +40,7 @@ computedValue=computedValue rowComponentOptions=rowComponentOptions noContentRow=noContentRow - maxContentRow=maxContentRow + validationMessage=validationMessage }} {{/if}} diff --git a/app/assets/stylesheets/common/select-kit/select-kit.scss b/app/assets/stylesheets/common/select-kit/select-kit.scss index 4d43328f846..0ba0af1bf5a 100644 --- a/app/assets/stylesheets/common/select-kit/select-kit.scss +++ b/app/assets/stylesheets/common/select-kit/select-kit.scss @@ -171,11 +171,6 @@ white-space: nowrap; } - &.max-content { - white-space: nowrap; - color: $danger; - } - .name { margin: 0; overflow: hidden; @@ -211,6 +206,14 @@ padding: 0; max-height: 200px; + .validation-message { + white-space: nowrap; + color: $danger; + flex: 1 0 auto; + margin: 5px; + padding: 0 2px; + } + .select-kit-collection { padding: 0; margin: 0; diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index fb144a214bd..49cb1121f8a 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1250,7 +1250,8 @@ en: no_content: No matches found filter_placeholder: Search... create: "Create: '{{content}}'" - max_content_reached: "You can only select {{count}} items." + max_content_reached: "You can only select {{count}} item(s)." + min_content_not_reached: "Select at least {{count}} item(s)." emoji_picker: filter_placeholder: Search for emoji diff --git a/test/javascripts/components/multi-select-test.js.es6 b/test/javascripts/components/multi-select-test.js.es6 index 349b26d7c63..d77583271c8 100644 --- a/test/javascripts/components/multi-select-test.js.es6 +++ b/test/javascripts/components/multi-select-test.js.es6 @@ -160,3 +160,44 @@ componentTest('with limitMatches', { andThen(() => assert.equal(this.get('subject').el().find(".select-kit-row").length, 2)); } }); + +componentTest('with minimum', { + template: '{{multi-select content=content minimum=1}}', + + beforeEach() { + this.set('content', ['sam', 'jeff', 'neil']); + }, + + test(assert) { + this.get('subject').expand(); + + andThen(() => assert.equal(this.get('subject').validationMessage(), 'Select at least 1 item(s).')); + + this.get('subject').selectRowByValue('sam'); + + andThen(() => { + assert.equal(this.get('subject').header().label(), 'sam'); + }); + } +}); + +componentTest('with minimumLabel', { + template: '{{multi-select content=content minimum=1 minimumLabel="test.minimum"}}', + + beforeEach() { + I18n.translations[I18n.locale].js.test = { minimum: 'min %{count}' }; + this.set('content', ['sam', 'jeff', 'neil']); + }, + + test(assert) { + this.get('subject').expand(); + + andThen(() => assert.equal(this.get('subject').validationMessage(), 'min 1')); + + this.get('subject').selectRowByValue('jeff'); + + andThen(() => { + assert.equal(this.get('subject').header().label(), 'jeff'); + }); + } +}); diff --git a/test/javascripts/components/single-select-test.js.es6 b/test/javascripts/components/single-select-test.js.es6 index 3cf74cd51ad..619e56ee68d 100644 --- a/test/javascripts/components/single-select-test.js.es6 +++ b/test/javascripts/components/single-select-test.js.es6 @@ -501,3 +501,44 @@ componentTest('with limitMatches', { andThen(() => assert.equal(this.get('subject').el().find(".select-kit-row").length, 2)); } }); + +componentTest('with minimum', { + template: '{{single-select content=content minimum=1 allowAutoSelectFirst=false}}', + + beforeEach() { + this.set('content', ['sam', 'jeff', 'neil']); + }, + + test(assert) { + this.get('subject').expand(); + + andThen(() => assert.equal(this.get('subject').validationMessage(), 'Select at least 1 item(s).')); + + this.get('subject').selectRowByValue('sam'); + + andThen(() => { + assert.equal(this.get('subject').header().label(), 'sam'); + }); + } +}); + +componentTest('with minimumLabel', { + template: '{{single-select content=content minimum=1 minimumLabel="test.minimum" allowAutoSelectFirst=false}}', + + beforeEach() { + I18n.translations[I18n.locale].js.test = { minimum: 'min %{count}' }; + this.set('content', ['sam', 'jeff', 'neil']); + }, + + test(assert) { + this.get('subject').expand(); + + andThen(() => assert.equal(this.get('subject').validationMessage(), 'min 1')); + + this.get('subject').selectRowByValue('jeff'); + + andThen(() => { + assert.equal(this.get('subject').header().label(), 'jeff'); + }); + } +}); diff --git a/test/javascripts/helpers/select-kit-helper.js b/test/javascripts/helpers/select-kit-helper.js index 17f5c578c77..a215bdd75bb 100644 --- a/test/javascripts/helpers/select-kit-helper.js +++ b/test/javascripts/helpers/select-kit-helper.js @@ -63,6 +63,7 @@ function selectKit(selector) { // eslint-disable-line no-unused-vars return { value: function() { return header.attr('data-value'); }, name: function() { return header.attr('data-name'); }, + label: function() { return header.text().trim(); }, icon: function() { return header.find('.icon'); }, title: function() { return header.attr('title'); }, el: function() { return header; } @@ -183,6 +184,16 @@ function selectKit(selector) { // eslint-disable-line no-unused-vars return rowHelper(find(selector).find('.select-kit-row.none')); }, + validationMessage: function() { + var validationMessage = find(selector).find('.validation-message'); + + if (validationMessage.length) { + return validationMessage.html().trim(); + } else { + return null; + } + }, + selectedRow: function() { return rowHelper(find(selector).find('.select-kit-row.is-selected')); },