Skip to content
代码片段 群组 项目
提交 f6f5c6c6 编辑于 作者: Kushal Pandya's avatar Kushal Pandya
浏览文件

Validate Work Items while adding them to parent

Validates selected Work Items while adding them
to parent as non-confidential parent cannot have
confidential children.

EE: true
上级 a343bd3e
No related branches found
No related tags found
无相关合并请求
......@@ -12,7 +12,7 @@ import {
import { debounce } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __, s__ } from '~/locale';
import { __, s__, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
......@@ -191,6 +191,9 @@ export default {
this.parentWorkItemType,
);
},
showConfidentialityTooltip() {
return this.isCreateForm && this.parentConfidential;
},
addOrCreateMethod() {
return this.isCreateForm ? this.createChild : this.addChild;
},
......@@ -207,7 +210,10 @@ export default {
return this.parentMilestone?.id;
},
isSubmitButtonDisabled() {
return this.isCreateForm ? this.search.length === 0 : this.workItemsToAdd.length === 0;
if (this.isCreateForm) {
return this.search.length === 0;
}
return this.workItemsToAdd.length === 0 || !this.areWorkItemsToAddValid;
},
isLoading() {
return this.$apollo.queries.availableWorkItems.loading;
......@@ -215,6 +221,32 @@ export default {
addInputPlaceholder() {
return sprintfWorkItem(I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER, this.childrenTypeName);
},
tokenSelectorContainerClass() {
return !this.areWorkItemsToAddValid ? 'gl-inset-border-1-red-500!' : '';
},
invalidWorkItemsToAdd() {
return this.parentConfidential
? this.workItemsToAdd.filter((workItem) => !workItem.confidential)
: [];
},
areWorkItemsToAddValid() {
return this.invalidWorkItemsToAdd.length === 0;
},
showWorkItemsToAddInvalidMessage() {
return !this.isCreateForm && !this.areWorkItemsToAddValid;
},
workItemsToAddInvalidMessage() {
return sprintf(
s__(
'WorkItem|%{invalidWorkItemsList} cannot be added: Cannot assign a non-confidential %{childWorkItemType} to a confidential parent %{parentWorkItemType}. Make the selected %{childWorkItemType} confidential and try again.',
),
{
invalidWorkItemsList: this.invalidWorkItemsToAdd.map(({ title }) => title).join(', '),
childWorkItemType: this.childrenTypeName,
parentWorkItemType: this.parentWorkItemType,
},
);
},
},
created() {
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
......@@ -334,6 +366,7 @@ export default {
/>
</gl-form-group>
<gl-form-checkbox
v-if="isCreateForm"
ref="confidentialityCheckbox"
v-model="confidential"
name="isConfidential"
......@@ -342,35 +375,44 @@ export default {
>{{ confidentialityCheckboxLabel }}</gl-form-checkbox
>
<gl-tooltip
v-if="parentConfidential"
v-if="showConfidentialityTooltip"
:target="getConfidentialityTooltipTarget"
triggers="hover"
>{{ confidentialityCheckboxTooltip }}</gl-tooltip
>
<gl-token-selector
v-if="!isCreateForm"
v-model="workItemsToAdd"
:dropdown-items="availableWorkItems"
:loading="isLoading"
:placeholder="addInputPlaceholder"
menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
class="gl-mb-4"
data-testid="work-item-token-select-input"
@text-input="debouncedSearchKeyUpdate"
@focus="handleFocus"
@mouseover.native="handleMouseOver"
@mouseout.native="handleMouseOut"
>
<template #token-content="{ token }">
{{ token.title }}
</template>
<template #dropdown-item-content="{ dropdownItem }">
<div class="gl-display-flex">
<div class="gl-text-secondary gl-mr-4">{{ getIdFromGraphQLId(dropdownItem.id) }}</div>
<div class="gl-text-truncate">{{ dropdownItem.title }}</div>
</div>
</template>
</gl-token-selector>
<div class="gl-mb-4">
<gl-token-selector
v-if="!isCreateForm"
v-model="workItemsToAdd"
:dropdown-items="availableWorkItems"
:loading="isLoading"
:placeholder="addInputPlaceholder"
menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
:container-class="tokenSelectorContainerClass"
data-testid="work-item-token-select-input"
@text-input="debouncedSearchKeyUpdate"
@focus="handleFocus"
@mouseover.native="handleMouseOver"
@mouseout.native="handleMouseOut"
>
<template #token-content="{ token }">
{{ token.title }}
</template>
<template #dropdown-item-content="{ dropdownItem }">
<div class="gl-display-flex">
<div class="gl-text-secondary gl-mr-4">{{ getIdFromGraphQLId(dropdownItem.id) }}</div>
<div class="gl-text-truncate">{{ dropdownItem.title }}</div>
</div>
</template>
</gl-token-selector>
<div
v-if="showWorkItemsToAddInvalidMessage"
class="gl-text-red-500"
data-testid="work-items-invalid"
>
{{ workItemsToAddInvalidMessage }}
</div>
</div>
<gl-button
category="primary"
variant="confirm"
......
......@@ -11,6 +11,7 @@ query projectWorkItems(
id
title
state
confidential
}
}
}
......
......@@ -47093,6 +47093,9 @@ msgstr ""
msgid "WorkItem|%{count} more assignees"
msgstr ""
 
msgid "WorkItem|%{invalidWorkItemsList} cannot be added: Cannot assign a non-confidential %{childWorkItemType} to a confidential parent %{parentWorkItemType}. Make the selected %{childWorkItemType} confidential and try again."
msgstr ""
msgid "WorkItem|%{workItemType} deleted"
msgstr ""
 
import Vue from 'vue';
import { GlForm, GlFormInput, GlFormCheckbox, GlTooltip, GlTokenSelector } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { sprintf } from '~/locale';
import { sprintf, s__ } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
......@@ -170,6 +170,16 @@ describe('WorkItemLinksForm', () => {
});
describe('adding an existing work item', () => {
const selectAvailableWorkItemTokens = async () => {
findTokenSelector().vm.$emit(
'input',
availableWorkItemsResponse.data.workspace.workItems.nodes,
);
findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
await waitForPromises();
};
beforeEach(async () => {
await createComponent({ formType: FORM_TYPES.add });
});
......@@ -179,6 +189,7 @@ describe('WorkItemLinksForm', () => {
expect(findTokenSelector().exists()).toBe(true);
expect(findAddChildButton().text()).toBe('Add task');
expect(findInput().exists()).toBe(false);
expect(findConfidentialCheckbox().exists()).toBe(false);
});
it('searches for available work items as prop when typing in input', async () => {
......@@ -190,13 +201,7 @@ describe('WorkItemLinksForm', () => {
});
it('selects and adds children', async () => {
findTokenSelector().vm.$emit(
'input',
availableWorkItemsResponse.data.workspace.workItems.nodes,
);
findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
await waitForPromises();
await selectAvailableWorkItemTokens();
expect(findAddChildButton().text()).toBe('Add tasks');
findForm().vm.$emit('submit', {
......@@ -205,6 +210,31 @@ describe('WorkItemLinksForm', () => {
await waitForPromises();
expect(updateMutationResolver).toHaveBeenCalled();
});
it('shows validation error when non-confidential child items are being added to confidential parent', async () => {
await createComponent({ formType: FORM_TYPES.add, parentConfidential: true });
await selectAvailableWorkItemTokens();
const validationEl = wrapper.findByTestId('work-items-invalid');
expect(validationEl.exists()).toBe(true);
expect(validationEl.text().trim()).toBe(
sprintf(
s__(
'WorkItem|%{invalidWorkItemsList} cannot be added: Cannot assign a non-confidential %{childWorkItemType} to a confidential parent %{parentWorkItemType}. Make the selected %{childWorkItemType} confidential and try again.',
),
{
// Only non-confidential work items are shown in the error message
invalidWorkItemsList: availableWorkItemsResponse.data.workspace.workItems.nodes
.filter((wi) => !wi.confidential)
.map((wi) => wi.title)
.join(', '),
childWorkItemType: 'Task',
parentWorkItemType: 'Issue',
},
),
);
});
});
describe('associate iteration with task', () => {
......
......@@ -1177,6 +1177,7 @@ export const availableWorkItemsResponse = {
title: 'Task 1',
state: 'OPEN',
createdAt: '2022-08-03T12:41:54Z',
confidential: false,
__typename: 'WorkItem',
},
{
......@@ -1184,6 +1185,15 @@ export const availableWorkItemsResponse = {
title: 'Task 2',
state: 'OPEN',
createdAt: '2022-08-03T12:41:54Z',
confidential: false,
__typename: 'WorkItem',
},
{
id: 'gid://gitlab/WorkItem/460',
title: 'Task 3',
state: 'OPEN',
createdAt: '2022-08-03T12:41:54Z',
confidential: true,
__typename: 'WorkItem',
},
],
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册