Commit c4c9abeb authored by Dennis Tang's avatar Dennis Tang Committed by Mike Greiling

Nonnegative integer weights in issuable sidebar

parent 8e323420
const CustomNumber = {
keydown(e) {
if (this.destroyed) return;
const { list } = e.detail.hook;
const { value } = e.detail.hook.trigger;
const parsedValue = Number(value);
const config = e.detail.hook.config.CustomNumber;
const { defaultOptions } = config;
const isValidNumber = !Number.isNaN(parsedValue) && value !== '';
const customOption = [{ id: parsedValue, title: parsedValue }];
const defaultDropdownOptions = defaultOptions.map(o => ({ id: o, title: o }));
list.setData(isValidNumber ? customOption : defaultDropdownOptions);
list.currentIndex = 0;
},
debounceKeydown(e) {
if (
[
13, // enter
16, // shift
17, // ctrl
18, // alt
20, // caps lock
37, // left arrow
38, // up arrow
39, // right arrow
40, // down arrow
91, // left window
92, // right window
93, // select
].indexOf(e.detail.which || e.detail.keyCode) > -1
)
return;
if (this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(this.keydown.bind(this, e), 200);
},
init(hook) {
this.hook = hook;
this.destroyed = false;
this.eventWrapper = {};
this.eventWrapper.debounceKeydown = this.debounceKeydown.bind(this);
this.hook.trigger.addEventListener('keydown.dl', this.eventWrapper.debounceKeydown);
this.hook.trigger.addEventListener('mousedown.dl', this.eventWrapper.debounceKeydown);
},
destroy() {
if (this.timeout) clearTimeout(this.timeout);
this.destroyed = true;
this.hook.trigger.removeEventListener('keydown.dl', this.eventWrapper.debounceKeydown);
this.hook.trigger.removeEventListener('mousedown.dl', this.eventWrapper.debounceKeydown);
},
};
export default CustomNumber;
import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils';
import CustomNumber from '../droplab/plugins/custom_number';
export default class DropdownWeight extends FilteredSearchDropdown {
constructor(options = {}) {
super(options);
this.defaultOptions = Array.from(Array(21).keys());
this.config = {
CustomNumber: {
defaultOptions: this.defaultOptions,
},
};
}
itemClicked(e) {
super.itemClicked(e, selected => {
const title = selected.querySelector('.js-data-value').innerText.trim();
return `${DropdownUtils.getEscapedText(title)}`;
});
}
renderContent(forceShowList = false) {
this.droplab.changeHookList(this.hookId, this.dropdown, [CustomNumber], this.config);
const defaultDropdownOptions = this.defaultOptions.map(o => ({ id: o, title: o }));
this.droplab.setData(defaultDropdownOptions);
super.renderContent(forceShowList);
}
init() {
this.droplab.addHook(this.input, this.dropdown, [CustomNumber], this.config).init();
}
}
...@@ -7,6 +7,7 @@ import DropdownHint from './dropdown_hint'; ...@@ -7,6 +7,7 @@ import DropdownHint from './dropdown_hint';
import DropdownEmoji from './dropdown_emoji'; import DropdownEmoji from './dropdown_emoji';
import DropdownNonUser from './dropdown_non_user'; import DropdownNonUser from './dropdown_non_user';
import DropdownUser from './dropdown_user'; import DropdownUser from './dropdown_user';
import DropdownWeight from './dropdown_weight';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
export default class FilteredSearchDropdownManager { export default class FilteredSearchDropdownManager {
...@@ -92,12 +93,12 @@ export default class FilteredSearchDropdownManager { ...@@ -92,12 +93,12 @@ export default class FilteredSearchDropdownManager {
}, },
weight: { weight: {
reference: null, reference: null,
gl: DropdownNonUser, gl: DropdownWeight,
element: this.container.querySelector('#js-dropdown-weight'), element: this.container.querySelector('#js-dropdown-weight'),
}, },
}; };
supportedTokens.forEach((type) => { supportedTokens.forEach(type => {
if (availableMappings[type]) { if (availableMappings[type]) {
allowedMappings[type] = availableMappings[type]; allowedMappings[type] = availableMappings[type];
} }
...@@ -152,13 +153,16 @@ export default class FilteredSearchDropdownManager { ...@@ -152,13 +153,16 @@ export default class FilteredSearchDropdownManager {
updateDropdownOffset(key) { updateDropdownOffset(key) {
// Always align dropdown with the input field // Always align dropdown with the input field
let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left; let offset =
this.filteredSearchInput.getBoundingClientRect().left -
this.container.querySelector('.scroll-container').getBoundingClientRect().left;
const maxInputWidth = 240; const maxInputWidth = 240;
const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth; const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
// Make sure offset never exceeds the input container // Make sure offset never exceeds the input container
const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth; const offsetMaxWidth =
this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
if (offsetMaxWidth < offset) { if (offsetMaxWidth < offset) {
offset = offsetMaxWidth; offset = offsetMaxWidth;
} }
...@@ -184,8 +188,7 @@ export default class FilteredSearchDropdownManager { ...@@ -184,8 +188,7 @@ export default class FilteredSearchDropdownManager {
const glArguments = Object.assign({}, defaultArguments, extraArguments); const glArguments = Object.assign({}, defaultArguments, extraArguments);
// Passing glArguments to `new glClass(<arguments>)` // Passing glArguments to `new glClass(<arguments>)`
mappingKey.reference = mappingKey.reference = new (Function.prototype.bind.apply(glClass, [null, glArguments]))();
new (Function.prototype.bind.apply(glClass, [null, glArguments]))();
} }
if (firstLoad) { if (firstLoad) {
...@@ -212,8 +215,8 @@ export default class FilteredSearchDropdownManager { ...@@ -212,8 +215,8 @@ export default class FilteredSearchDropdownManager {
} }
const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key const shouldOpenFilterDropdown =
&& this.mapping[match.key]; match && this.currentDropdown !== match.key && this.mapping[match.key];
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
...@@ -224,8 +227,10 @@ export default class FilteredSearchDropdownManager { ...@@ -224,8 +227,10 @@ export default class FilteredSearchDropdownManager {
setDropdown() { setDropdown() {
const query = DropdownUtils.getSearchQuery(true); const query = DropdownUtils.getSearchQuery(true);
const { lastToken, searchToken } = const { lastToken, searchToken } = this.tokenizer.processTokens(
this.tokenizer.processTokens(query, this.filteredSearchTokenKeys.getKeys()); query,
this.filteredSearchTokenKeys.getKeys(),
);
if (this.currentDropdown) { if (this.currentDropdown) {
this.updateCurrentDropdownOffset(); this.updateCurrentDropdownOffset();
......
...@@ -215,6 +215,15 @@ ...@@ -215,6 +215,15 @@
} }
} }
} }
&.weight {
.gl-field-error {
margin-top: $gl-padding-8;
margin-left: -6px;
display: flex;
align-items: center;
}
}
} }
.block-first { .block-first {
......
...@@ -107,17 +107,16 @@ ...@@ -107,17 +107,16 @@
- if type == :issues || type == :boards || type == :boards_modal - if type == :issues || type == :boards || type == :boards_modal
#js-dropdown-weight.filtered-search-input-dropdown-menu.dropdown-menu #js-dropdown-weight.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ 'data-dropdown' => true } %ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => 'none' } %li.filter-dropdown-item{ 'data-value' => 'None' }
%button.btn.btn-link %button.btn.btn-link
No Weight None
%li.filter-dropdown-item{ 'data-value' => 'any' } %li.filter-dropdown-item{ 'data-value' => 'Any' }
%button.btn.btn-link %button.btn.btn-link
Any Weight Any
%li.divider.droplab-item-ignore %li.divider.droplab-item-ignore
%ul.filter-dropdown{ 'data-dropdown' => true } %ul.filter-dropdown{ data: { dropdown: true, dynamic: true } }
- Issue.weight_filter_options.each do |weight| %li.filter-dropdown-item{ data: { value: '{{id}}' } }
%li.filter-dropdown-item{ 'data-value' => "#{weight}" } %button.btn.btn-link {{title}}
%button.btn.btn-link= weight
%button.clear-search.hidden{ type: 'button' } %button.clear-search.hidden{ type: 'button' }
= icon('times') = icon('times')
......
# Issue Weight **[STARTER]** # Issue Weight **[STARTER]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/76) > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/76)
in [GitLab Starter](https://about.gitlab.com/products/) 8.3. > in [GitLab Starter](https://about.gitlab.com/products/) 8.3.
When you have a lot of issues, it can be hard to get an overview. When you have a lot of issues, it can be hard to get an overview.
By adding a weight to each issue, you can get a better idea of how much time, By adding a weight to each issue, you can get a better idea of how much time,
value or complexity a given issue has or will cost. value or complexity a given issue has or will cost.
You can set the weight of an issue during its creation, by simply changing the You can set the weight of an issue during its creation, by simply changing the
value in the dropdown menu. You can set it to a numeric value from 1 to 9. value in the dropdown menu. You can set it to a non-negative integer
value from 0, 1, 2, and so on. You can remove weight from an issue
as well.
This value will appear on the right sidebar of an individual issue, as well as This value will appear on the right sidebar of an individual issue, as well as
in the issues page next to a distinctive balance scale icon. in the issues page next to a distinctive balance scale icon.
......
doc/workflow/issue_weight/issue.png

158 KB | W: | H:

doc/workflow/issue_weight/issue.png

238 KB | W: | H:

doc/workflow/issue_weight/issue.png
doc/workflow/issue_weight/issue.png
doc/workflow/issue_weight/issue.png
doc/workflow/issue_weight/issue.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -6,17 +6,17 @@ const weightTokenKey = { ...@@ -6,17 +6,17 @@ const weightTokenKey = {
param: '', param: '',
symbol: '', symbol: '',
icon: 'balance-scale', icon: 'balance-scale',
tag: 'weight', tag: 'number',
}; };
const weightConditions = [{ const weightConditions = [{
url: 'weight=No+Weight', url: 'weight=None',
tokenKey: 'weight', tokenKey: 'weight',
value: 'none', value: 'None',
}, { }, {
url: 'weight=Any+Weight', url: 'weight=Any',
tokenKey: 'weight', tokenKey: 'weight',
value: 'any', value: 'Any',
}]; }];
const alternativeTokenKeys = [{ const alternativeTokenKeys = [{
......
<script> <script>
import Flash from '~/flash'; import Flash from '~/flash';
import eventHub from '~/sidebar/event_hub'; import eventHub from '~/sidebar/event_hub';
import weightComponent from './weight.vue'; import weightComponent from './weight.vue';
export default { export default {
components: { components: {
weight: weightComponent, weight: weightComponent,
}, },
...@@ -27,13 +27,12 @@ ...@@ -27,13 +27,12 @@
methods: { methods: {
onUpdateWeight(newWeight) { onUpdateWeight(newWeight) {
this.mediator.updateWeight(newWeight) this.mediator.updateWeight(newWeight).catch(() => {
.catch(() => {
Flash('Error occurred while updating the issue weight'); Flash('Error occurred while updating the issue weight');
}); });
}, },
}, },
}; };
</script> </script>
<template> <template>
...@@ -41,7 +40,6 @@ ...@@ -41,7 +40,6 @@
:fetching="mediator.store.isFetching.weight" :fetching="mediator.store.isFetching.weight"
:loading="mediator.store.isLoading.weight" :loading="mediator.store.isLoading.weight"
:weight="mediator.store.weight" :weight="mediator.store.weight"
:weight-options="mediator.store.weightOptions"
:weight-none-value="mediator.store.weightNoneValue" :weight-none-value="mediator.store.weightNoneValue"
:editable="mediator.store.editable" :editable="mediator.store.editable"
/> />
......
<script> <script>
/* eslint-disable vue/require-default-prop */ import $ from 'jquery';
import { s__ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import $ from 'jquery'; export default {
import { s__ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
export default {
components: { components: {
icon, icon,
loadingIcon, loadingIcon,
...@@ -28,16 +26,14 @@ ...@@ -28,16 +26,14 @@
default: false, default: false,
}, },
weight: { weight: {
type: Number, type: [String, Number],
required: false, required: false,
}, default: '',
weightOptions: {
type: Array,
required: true,
}, },
weightNoneValue: { weightNoneValue: {
type: String, type: String,
required: true, required: true,
default: 'None',
}, },
editable: { editable: {
type: Boolean, type: Boolean,
...@@ -45,14 +41,16 @@ ...@@ -45,14 +41,16 @@
default: false, default: false,
}, },
id: { id: {
type: Number, type: [String, Number],
required: false, required: false,
default: '',
}, },
}, },
data() { data() {
return { return {
shouldShowDropdown: false, hasValidInput: true,
collapseAfterDropdownCloses: false, shouldShowEditField: false,
collapsedAfterUpdate: false,
}; };
}, },
computed: { computed: {
...@@ -62,7 +60,12 @@ ...@@ -62,7 +60,12 @@
collapsedWeightLabel() { collapsedWeightLabel() {
let label = this.weight; let label = this.weight;
if (this.checkIfNoValue(this.weight)) { if (this.checkIfNoValue(this.weight)) {
label = s__('Sidebar|No'); label = this.noValueLabel;
}
// Truncate with ellipsis after five digits
if (this.weight > 99999) {
label = `${this.weight.toString().substr(0, 5)}&hellip;`;
} }
return label; return label;
...@@ -82,7 +85,7 @@ ...@@ -82,7 +85,7 @@
return label; return label;
}, },
shouldShowWeight() { shouldShowWeight() {
return !this.fetching && !this.shouldShowDropdown; return !this.fetching && !this.shouldShowEditField;
}, },
tooltipTitle() { tooltipTitle() {
let tooltipTitle = s__('Sidebar|Weight'); let tooltipTitle = s__('Sidebar|Weight');
...@@ -94,66 +97,51 @@ ...@@ -94,66 +97,51 @@
return tooltipTitle; return tooltipTitle;
}, },
}, },
mounted() {
$(this.$refs.weightDropdown).glDropdown({
showMenuAbove: false,
selectable: true,
filterable: false,
multiSelect: false,
data: (searchTerm, callback) => {
callback(this.weightOptions);
},
renderRow: (weight) => {
const isActive = weight === this.weight ||
(this.checkIfNoValue(weight) && this.checkIfNoValue(this.weight));
return `
<li>
<a href="#" class="${isActive ? 'is-active' : ''}">
${weight}
</a>
</li>
`;
},
hidden: () => {
this.shouldShowDropdown = false;
this.collapseAfterDropdownCloses = false;
},
clicked: (options) => {
const selectedValue = this.checkIfNoValue(options.selectedObj) ?
null :
options.selectedObj;
const resultantValue = options.isMarking ? selectedValue : null;
eventHub.$emit('updateWeight', resultantValue, this.id);
},
});
},
methods: { methods: {
checkIfNoValue(weight) { checkIfNoValue(weight) {
return weight === undefined || return weight === undefined || weight === null || weight === this.weightNoneValue;
weight === null ||
weight === 0 ||
weight === this.weightNoneValue;
}, },
showDropdown() { showEditField(bool = true) {
this.shouldShowDropdown = true; this.shouldShowEditField = bool;
// Trigger the bootstrap dropdown
setTimeout(() => { if (this.shouldShowEditField) {
$(this.$refs.dropdownToggle).dropdown('toggle'); this.$nextTick(() => {
this.$refs.editableField.focus();
}); });
}
}, },
onCollapsedClick() { onCollapsedClick() {
this.collapseAfterDropdownCloses = true; this.showEditField(true);
this.showDropdown(); this.collapsedAfterUpdate = true;
}, },
onSubmit(e) {
const { value } = e.target;
const validatedValue = Number(value);
const isNewValue = validatedValue !== this.weight;
this.hasValidInput = validatedValue >= 0 || value === '';
if (!this.loading && this.hasValidInput) {
$(this.$el).trigger('hidden.gl.dropdown');
if (isNewValue) {
eventHub.$emit('updateWeight', value, this.id);
}
this.showEditField(false);
}
}, },
}; removeWeight() {
eventHub.$emit('updateWeight', '', this.id);
},
},
};
</script> </script>
<template> <template>
<div <div
class="block weight" class="block weight"
:class="{ 'collapse-after-update': collapseAfterDropdownCloses }" :class="{ 'collapse-after-update': collapsedAfterUpdate }"
> >
<div <div
class="sidebar-collapsed-icon js-weight-collapsed-block" class="sidebar-collapsed-icon js-weight-collapsed-block"
...@@ -174,10 +162,9 @@ ...@@ -174,10 +162,9 @@
/> />
<span <span
v-else v-else
v-html="collapsedWeightLabel"
class="js-weight-collapsed-weight-label" class="js-weight-collapsed-weight-label"
> ></span>
{{ collapsedWeightLabel }}
</span>
</div> </div>
<div class="title hide-collapsed"> <div class="title hide-collapsed">
{{ s__('Sidebar|Weight') }} {{ s__('Sidebar|Weight') }}
...@@ -190,76 +177,55 @@ ...@@ -190,76 +177,55 @@
v-if="editable" v-if="editable"
class="float-right js-weight-edit-link" class="float-right js-weight-edit-link"
href="#" href="#"
@click="showDropdown" @click="showEditField(!shouldShowEditField)"
> >
{{ __('Edit') }} {{ __('Edit') }}
</a> </a>
</div> </div>
<div <div
v-if="shouldShowWeight" class="hide-collapsed"
class="value hide-collapsed js-weight-weight-label" v-if="shouldShowEditField"
> >
<strong v-if="!isNoValue"> <input
{{ weight }} class="form-control"
</strong> type="text"
ref="editableField"
:value="weight"
@blur="onSubmit"
@keydown.enter="onSubmit"
/>
<span <span
v-else class="gl-field-error"
class="no-value"> v-if="!hasValidInput"
{{ noValueLabel }} >
<icon
name="merge-request-close-m"
:size="24"
/>
{{ s__('Sidebar|Only numeral characters allowed') }}
</span> </span>
</div> </div>
<div <div
class="selectbox hide-collapsed" v-if="shouldShowWeight"
:class="{ show: shouldShowDropdown }" class="value hide-collapsed js-weight-weight-label"
>
<div
ref="weightDropdown"
class="dropdown"
>
<button
ref="dropdownToggle"
class="dropdown-menu-toggle js-gl-dropdown-refresh-on-open"
type="button"
data-toggle="dropdown"
> >
<span <span v-if="!isNoValue">
class="dropdown-toggle-text js-weight-dropdown-toggle-text" <strong class="js-weight-weight-label-value">{{ weight }}</strong>
:class="{ 'is-default': isNoValue }" &nbsp;-&nbsp;
<a
v-if="editable"
class="btn-default-hover-link js-weight-remove-link"
href="#"
@click="removeWeight"
> >
{{ dropdownToggleLabel }} {{ __('remove weight') }}
</a>
</span> </span>
<i <span
aria-hidden="true" v-else
data-hidden="true" class="no-value">
class="fa fa-chevron-down" {{ noValueLabel }}
>
</i>
</button>
<div
v-once
class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-weight"
>
<div class="dropdown-title">
<span>
{{ changeWeightLabel }}
</span> </span>
<button
class="dropdown-title-button dropdown-menu-close"
aria-label="Close"
type="button"
>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-times dropdown-menu-close-icon"
>
</i>
</button>
</div>
<div class="dropdown-content js-weight-dropdown-content"></div>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
...@@ -4,13 +4,15 @@ module EE ...@@ -4,13 +4,15 @@ module EE
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
prepended do prepended do
WEIGHT_RANGE = 1..9 WEIGHT_RANGE = 0..20
WEIGHT_ALL = 'Everything'.freeze WEIGHT_ALL = 'Everything'.freeze
WEIGHT_ANY = 'Any Weight'.freeze WEIGHT_ANY = 'Any'.freeze
WEIGHT_NONE = 'No Weight'.freeze WEIGHT_NONE = 'None'.freeze
scope :order_weight_desc, -> { reorder ::Gitlab::Database.nulls_last_order('weight', 'DESC') } scope :order_weight_desc, -> { reorder ::Gitlab::Database.nulls_last_order('weight', 'DESC') }
scope :order_weight_asc, -> { reorder ::Gitlab::Database.nulls_last_order('weight') } scope :order_weight_asc, -> { reorder ::Gitlab::Database.nulls_last_order('weight') }
validates :weight, allow_nil: true, numericality: { greater_than_or_equal_to: 0 }
end end
# override # override
......
...@@ -21,13 +21,13 @@ module EE ...@@ -21,13 +21,13 @@ module EE
explanation do |weight| explanation do |weight|
"Sets weight to #{weight}." if weight "Sets weight to #{weight}." if weight
end end
params ::Issue::WEIGHT_RANGE.to_s.squeeze('.').tr('.', '-') params "0, 1, 2, …"
condition do condition do
issuable.supports_weight? && issuable.supports_weight? &&
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
end end
parse_params do |weight| parse_params do |weight|
weight.to_i if ::Issue.weight_filter_options.include?(weight.to_i) weight.to_i if weight.to_i > 0
end end
command :weight do |weight| command :weight do |weight|
@updates[:weight] = weight if weight @updates[:weight] = weight if weight
......
---
title: Add support for non-negative integer weight values in issuable sidebar
merge_request:
author:
type: changed
...@@ -170,8 +170,8 @@ describe 'Scoped issue boards', :js do ...@@ -170,8 +170,8 @@ describe 'Scoped issue boards', :js do
end end
end end
it 'creates board filtering by "Any weight"' do it 'creates board filtering by "Any" weight' do
create_board_weight('Any Weight') create_board_weight('Any')
expect(page).to have_selector('.board-card', count: 4) expect(page).to have_selector('.board-card', count: 4)
end end
...@@ -356,7 +356,7 @@ describe 'Scoped issue boards', :js do ...@@ -356,7 +356,7 @@ describe 'Scoped issue boards', :js do
end end
it 'sets board to Any weight' do it 'sets board to Any weight' do
update_board_weight('Any Weight') update_board_weight('Any')
expect(page).to have_selector('.board-card', count: 4) expect(page).to have_selector('.board-card', count: 4)
end end
......
...@@ -186,7 +186,7 @@ describe 'Issue Boards', :js do ...@@ -186,7 +186,7 @@ describe 'Issue Boards', :js do
page.within '.weight' do page.within '.weight' do
click_link 'Edit' click_link 'Edit'
click_link '1' find('.block.weight input').send_keys 1, :enter
page.within '.value' do page.within '.value' do
expect(page).to have_content '1' expect(page).to have_content '1'
...@@ -212,8 +212,7 @@ describe 'Issue Boards', :js do ...@@ -212,8 +212,7 @@ describe 'Issue Boards', :js do
wait_for_requests wait_for_requests
page.within '.weight' do page.within '.weight' do
click_link 'Edit' click_link 'remove weight'
click_link 'No Weight'
page.within '.value' do page.within '.value' do
expect(page).to have_content 'None' expect(page).to have_content 'None'
......
...@@ -17,7 +17,7 @@ describe 'Dropdown weight', :js do ...@@ -17,7 +17,7 @@ describe 'Dropdown weight', :js do
end end
def click_weight(text) def click_weight(text)
find('#js-dropdown-weight .filter-dropdown .filter-dropdown-item', text: text).click find('#js-dropdown-weight .filter-dropdown .filter-dropdown-item', text: text, exact_text: true).click
end end
def click_static_weight(text) def click_static_weight(text)
...@@ -49,7 +49,7 @@ describe 'Dropdown weight', :js do ...@@ -49,7 +49,7 @@ describe 'Dropdown weight', :js do
it 'should load all the weights when opened' do it 'should load all the weights when opened' do
send_keys_to_filtered_search('weight:') send_keys_to_filtered_search('weight:')
expect(page.all('#js-dropdown-weight .filter-dropdown .filter-dropdown-item').size).to eq(9) expect(page.all('#js-dropdown-weight .filter-dropdown .filter-dropdown-item').size).to eq(21)
end end
end end
...@@ -83,10 +83,10 @@ describe 'Dropdown weight', :js do ...@@ -83,10 +83,10 @@ describe 'Dropdown weight', :js do
end end
it 'fills in `no weight`' do it 'fills in `no weight`' do
click_static_weight('No Weight') click_static_weight('None')
expect(page).to have_css(js_dropdown_weight, visible: false) expect(page).to have_css(js_dropdown_weight, visible: false)
expect_tokens([{ name: 'Weight', value: 'none' }]) expect_tokens([{ name: 'Weight', value: 'None' }])
expect_filtered_search_input_empty expect_filtered_search_input_empty
end end
end end
......
require 'rails_helper'
feature 'Issue Sidebar' do
include MobileHelpers
let(:group) { create(:group, :nested) }
let(:project) { create(:project, :public, namespace: group) }
let!(:user) { create(:user)}
let!(:label) { create(:label, project: project, title: 'bug') }
let(:issue) { create(:labeled_issue, project: project, labels: [label]) }
before do
sign_in(user)
end
context 'updating weight', :js do
before do
project.add_master(user)
visit_issue(project, issue)
end
it 'updates weight in sidebar to 1' do
page.within '.weight' do
click_link 'Edit'
find('input').send_keys 1, :enter
page.within '.value' do
expect(page).to have_content '1'
end
end
end
it 'updates weight in sidebar to no weight' do
page.within '.weight' do
click_link 'Edit'
find('input').send_keys 1, :enter
page.within '.value' do
expect(page).to have_content '1'
end
click_link 'remove weight'
page.within '.value' do
expect(page).to have_content 'None'
end
end
end
end
def visit_issue(project, issue)
visit project_issue_path(project, issue)
end
end
...@@ -4,6 +4,29 @@ describe Issue do ...@@ -4,6 +4,29 @@ describe Issue do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
include ExternalAuthorizationServiceHelpers include ExternalAuthorizationServiceHelpers
describe 'validations' do
subject { build(:issue) }
describe 'weight' do
it 'is not valid when negative number' do
subject.weight = -1
expect(subject).not_to be_valid
expect(subject.errors[:weight]).not_to be_empty
end
it 'is valid when non-negative' do
subject.weight = 0
expect(subject).to be_valid
subject.weight = 1
expect(subject).to be_valid
end
end
end
describe '#allows_multiple_assignees?' do describe '#allows_multiple_assignees?' do
it 'does not allow multiple assignees without license' do it 'does not allow multiple assignees without license' do
stub_licensed_features(multiple_issue_assignees: false) stub_licensed_features(multiple_issue_assignees: false)
......
...@@ -225,35 +225,6 @@ feature 'Issue Sidebar' do ...@@ -225,35 +225,6 @@ feature 'Issue Sidebar' do
end end
end end
context 'updating weight', :js do
before do
project.add_master(user)
visit_issue(project, issue)
end
it 'updates weight in sidebar to 1' do
page.within '.weight' do
click_link 'Edit'
click_link '1'
page.within '.value' do
expect(page).to have_content '1'
end
end
end
it 'updates weight in sidebar to no weight' do
page.within '.weight' do
click_link 'Edit'
click_link 'No Weight'
page.within '.value' do
expect(page).to have_content 'None'
end
end
end
end
def visit_issue(project, issue) def visit_issue(project, issue)
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
end end
......
...@@ -546,7 +546,7 @@ describe 'Issues' do ...@@ -546,7 +546,7 @@ describe 'Issues' do
expect(page).to have_content "None" expect(page).to have_content "None"
click_link 'Edit' click_link 'Edit'
find('.dropdown-content a', text: '1').click find('.block.weight input').send_keys 1, :enter
page.within('.value') do page.within('.value') do
expect(page).to have_content "1" expect(page).to have_content "1"
......
...@@ -7,7 +7,7 @@ describe('Filtered Search Token Keys (Issues EE)', () => { ...@@ -7,7 +7,7 @@ describe('Filtered Search Token Keys (Issues EE)', () => {
param: '', param: '',
symbol: '', symbol: '',
icon: 'balance-scale', icon: 'balance-scale',
tag: 'weight', tag: 'number',
}; };
describe('get', () => { describe('get', () => {
......
...@@ -9,7 +9,8 @@ RESPONSE_MAP.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar'] = ...@@ -9,7 +9,8 @@ RESPONSE_MAP.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar'] =
username: 'user0', username: 'user0',
id: 22, id: 22,
state: 'active', state: 'active',
avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', avatar_url:
'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/user0', web_url: 'http: //localhost:3001/user0',
}, },
{ {
...@@ -17,7 +18,8 @@ RESPONSE_MAP.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar'] = ...@@ -17,7 +18,8 @@ RESPONSE_MAP.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar'] =
username: 'tajuana', username: 'tajuana',
id: 18, id: 18,
state: 'active', state: 'active',
avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', avatar_url:
'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/tajuana', web_url: 'http: //localhost:3001/tajuana',
}, },
{ {
...@@ -25,7 +27,8 @@ RESPONSE_MAP.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar'] = ...@@ -25,7 +27,8 @@ RESPONSE_MAP.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar'] =
username: 'michaele.will', username: 'michaele.will',
id: 16, id: 16,
state: 'active', state: 'active',
avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', avatar_url:
'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/michaele.will', web_url: 'http: //localhost:3001/michaele.will',
}, },
], ],
...@@ -37,7 +40,8 @@ RESPONSE_MAP.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar'] = ...@@ -37,7 +40,8 @@ RESPONSE_MAP.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar'] =
username: 'user0', username: 'user0',
id: 22, id: 22,
state: 'active', state: 'active',
avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', avatar_url:
'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/user0', web_url: 'http: //localhost:3001/user0',
}, },
{ {
...@@ -45,7 +49,8 @@ RESPONSE_MAP.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar'] = ...@@ -45,7 +49,8 @@ RESPONSE_MAP.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar'] =
username: 'tajuana', username: 'tajuana',
id: 18, id: 18,
state: 'active', state: 'active',
avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', avatar_url:
'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/tajuana', web_url: 'http: //localhost:3001/tajuana',
}, },
{ {
...@@ -53,7 +58,8 @@ RESPONSE_MAP.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar'] = ...@@ -53,7 +58,8 @@ RESPONSE_MAP.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar'] =
username: 'michaele.will', username: 'michaele.will',
id: 16, id: 16,
state: 'active', state: 'active',
avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', avatar_url:
'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/michaele.will', web_url: 'http: //localhost:3001/michaele.will',
}, },
], ],
...@@ -67,8 +73,8 @@ export default { ...@@ -67,8 +73,8 @@ export default {
...CEMockData, ...CEMockData,
mediator: { mediator: {
...CEMockData.mediator, ...CEMockData.mediator,
weightOptions: ['No Weight', 0, 1, 3], weightOptions: ['None', 0, 1, 3],
weightNoneValue: 'No Weight', weightNoneValue: 'None',
}, },
responseMap: RESPONSE_MAP, responseMap: RESPONSE_MAP,
}; };
...@@ -6,8 +6,8 @@ describe('EE Sidebar store', () => { ...@@ -6,8 +6,8 @@ describe('EE Sidebar store', () => {
beforeEach(() => { beforeEach(() => {
store = new SidebarStore({ store = new SidebarStore({
weight: null, weight: null,
weightOptions: ['No Weight', 0, 1, 3], weightOptions: ['None', 0, 1, 3],
weightNoneValue: 'No Weight', weightNoneValue: 'None',
}); });
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import weight from 'ee/sidebar/components/weight/weight.vue'; import weight from 'ee/sidebar/components/weight/weight.vue';
import eventHub from '~/sidebar/event_hub'; import eventHub from '~/sidebar/event_hub';
import { ENTER_KEY_CODE } from '~/lib/utils/keycodes';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
const DEFAULT_PROPS = { const DEFAULT_PROPS = {
weightOptions: ['No Weight', 1, 2, 3], weightNoneValue: 'None',
weightNoneValue: 'No Weight',
}; };
describe('Weight', function () { describe('Weight', function() {
let vm; let vm;
let Weight; let Weight;
...@@ -51,9 +50,12 @@ describe('Weight', function () { ...@@ -51,9 +50,12 @@ describe('Weight', function () {
weight: WEIGHT, weight: WEIGHT,
}); });
expect(vm.$el.querySelector('.js-weight-collapsed-weight-label').textContent.trim()).toEqual(`${WEIGHT}`); expect(vm.$el.querySelector('.js-weight-collapsed-weight-label').textContent.trim()).toEqual(
expect(vm.$el.querySelector('.js-weight-weight-label').textContent.trim()).toEqual(`${WEIGHT}`); `${WEIGHT}`,
expect(vm.$el.querySelector('.js-weight-dropdown-toggle-text').textContent.trim()).toEqual(`${WEIGHT}`); );
expect(vm.$el.querySelector('.js-weight-weight-label-value').textContent.trim()).toEqual(
`${WEIGHT}`,
);
}); });
it('shows weight no-value', () => { it('shows weight no-value', () => {
...@@ -64,20 +66,23 @@ describe('Weight', function () { ...@@ -64,20 +66,23 @@ describe('Weight', function () {
weight: WEIGHT, weight: WEIGHT,
}); });
expect(vm.$el.querySelector('.js-weight-collapsed-weight-label').textContent.trim()).toEqual('No'); expect(vm.$el.querySelector('.js-weight-collapsed-weight-label').textContent.trim()).toEqual(
expect(vm.$el.querySelector('.js-weight-weight-label').textContent.trim()).toEqual('None'); 'None',
// Put a placeholder in the dropdown toggle );
expect(vm.$el.querySelector('.js-weight-dropdown-toggle-text').textContent.trim()).toEqual('Weight'); expect(vm.$el.querySelector('.js-weight-weight-label .no-value').textContent.trim()).toEqual(
'None',
);
}); });
it('adds `collapse-after-update` class when clicking the collapsed block', (done) => { it('adds `collapse-after-update` class when clicking the collapsed block', done => {
vm = mountComponent(Weight, { vm = mountComponent(Weight, {
...DEFAULT_PROPS, ...DEFAULT_PROPS,
}); });
vm.$el.querySelector('.js-weight-collapsed-block').click(); vm.$el.querySelector('.js-weight-collapsed-block').click();
vm.$nextTick() vm
.$nextTick()
.then(() => { .then(() => {
expect(vm.$el.classList.contains('collapse-after-update')).toEqual(true); expect(vm.$el.classList.contains('collapse-after-update')).toEqual(true);
}) })
...@@ -85,26 +90,28 @@ describe('Weight', function () { ...@@ -85,26 +90,28 @@ describe('Weight', function () {
.catch(done.fail); .catch(done.fail);
}); });
it('shows dropdown on "Edit" link click', (done) => { it('shows dropdown on "Edit" link click', done => {
vm = mountComponent(Weight, { vm = mountComponent(Weight, {
...DEFAULT_PROPS, ...DEFAULT_PROPS,
editable: true, editable: true,
}); });
expect(vm.shouldShowDropdown).toEqual(false); expect(vm.shouldShowEditField).toEqual(false);
vm.$el.querySelector('.js-weight-edit-link').click(); vm.$el.querySelector('.js-weight-edit-link').click();
vm.$nextTick() vm
.$nextTick()
.then(() => { .then(() => {
expect(vm.shouldShowDropdown).toEqual(true); expect(vm.shouldShowEditField).toEqual(true);
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
}); });
it('emits event on dropdown item click', (done) => { it('emits event on input submission', done => {
const ID = 123; const ID = 123;
const expectedWeightValue = '3';
spyOn(eventHub, '$emit'); spyOn(eventHub, '$emit');
vm = mountComponent(Weight, { vm = mountComponent(Weight, {
...DEFAULT_PROPS, ...DEFAULT_PROPS,
...@@ -114,15 +121,78 @@ describe('Weight', function () { ...@@ -114,15 +121,78 @@ describe('Weight', function () {
vm.$el.querySelector('.js-weight-edit-link').click(); vm.$el.querySelector('.js-weight-edit-link').click();
vm.$nextTick() vm.$nextTick(() => {
.then(() => getSetTimeoutPromise()) const event = new CustomEvent('keydown');
.then(() => { event.keyCode = ENTER_KEY_CODE;
vm.$el.querySelector('.js-weight-dropdown-content li:nth-child(2) a').click();
}) vm.$refs.editableField.click();
.then(() => { vm.$refs.editableField.value = expectedWeightValue;
expect(eventHub.$emit).toHaveBeenCalledWith('updateWeight', DEFAULT_PROPS.weightOptions[1], ID); vm.$refs.editableField.dispatchEvent(event);
})
.then(done) expect(vm.hasValidInput).toBe(true);
.catch(done.fail); expect(eventHub.$emit).toHaveBeenCalledWith('updateWeight', expectedWeightValue, ID);
done();
});
});
it('emits event on remove weight link click', done => {
const ID = 123;
spyOn(eventHub, '$emit');
vm = mountComponent(Weight, {
...DEFAULT_PROPS,
editable: true,
weight: 3,
id: ID,
});
vm.$el.querySelector('.js-weight-remove-link').click();
vm.$nextTick(() => {
expect(vm.hasValidInput).toBe(true);
expect(eventHub.$emit).toHaveBeenCalledWith('updateWeight', '', ID);
done();
});
});
it('triggers error on invalid string value', done => {
vm = mountComponent(Weight, {
...DEFAULT_PROPS,
editable: true,
});
vm.$el.querySelector('.js-weight-edit-link').click();
vm.$nextTick(() => {
const event = new CustomEvent('keydown');
event.keyCode = ENTER_KEY_CODE;
vm.$refs.editableField.click();
vm.$refs.editableField.value = 'potato';
vm.$refs.editableField.dispatchEvent(event);
expect(vm.hasValidInput).toBe(false);
done();
});
});
it('triggers error on invalid negative integer value', done => {
vm = mountComponent(Weight, {
...DEFAULT_PROPS,
editable: true,
});
vm.$el.querySelector('.js-weight-edit-link').click();
vm.$nextTick(() => {
const event = new CustomEvent('keydown');
event.keyCode = ENTER_KEY_CODE;
vm.$refs.editableField.click();
vm.$refs.editableField.value = -9001;
vm.$refs.editableField.dispatchEvent(event);
expect(vm.hasValidInput).toBe(false);
done();
});
}); });
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment