Commit 853ed8f5 authored by Kushal Pandya's avatar Kushal Pandya

Use `issuable_show` for requirement show & create

Use `issuable_body.vue` component to render
requirement sidebar body.
parent 43e0d5ee
......@@ -2,6 +2,7 @@
// See: https://gitlab.com/gitlab-org/gitlab/-/issues/216102
export const BACKSPACE_KEY_CODE = 8;
export const TAB_KEY_CODE = 9;
export const ENTER_KEY_CODE = 13;
export const ESC_KEY_CODE = 27;
export const UP_KEY_CODE = 38;
......
......@@ -3,8 +3,6 @@ import '~/behaviors/markdown/render_gfm';
import $ from 'jquery';
import {
GlDrawer,
GlFormGroup,
GlFormTextarea,
GlButton,
GlFormCheckbox,
GlTooltipDirective,
......@@ -13,7 +11,8 @@ import {
import { isEmpty } from 'lodash';
import { __, sprintf } from '~/locale';
import ZenMode from '~/zen_mode';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { TAB_KEY_CODE } from '~/lib/utils/keycodes';
import IssuableBody from '~/issuable_show/components/issuable_body.vue';
import RequirementStatusBadge from './requirement_status_badge.vue';
......@@ -21,6 +20,7 @@ import RequirementMeta from '../mixins/requirement_meta';
import { MAX_TITLE_LENGTH, TestReportStatus } from '../constants';
export default {
maxTitleLength: MAX_TITLE_LENGTH,
events: {
drawerClose: 'drawer-close',
disableEdit: 'disable-edit',
......@@ -31,12 +31,10 @@ export default {
}),
components: {
GlDrawer,
GlFormGroup,
GlFormTextarea,
GlFormCheckbox,
GlButton,
MarkdownField,
RequirementStatusBadge,
IssuableBody,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -67,9 +65,7 @@ export default {
data() {
return {
zenModeEnabled: false,
title: this.requirement?.title || '',
satisfied: this.requirement?.satisfied || false,
description: this.requirement?.description || '',
};
},
computed: {
......@@ -82,18 +78,24 @@ export default {
saveButtonLabel() {
return this.isCreate ? __('Create requirement') : __('Save changes');
},
titleInvalid() {
return this.title?.length > MAX_TITLE_LENGTH;
canEditRequirement() {
return this.isCreate || (this.canUpdate && !this.isArchived);
},
disableSaveButton() {
return this.title === '' || this.titleInvalid || this.requirementRequestActive;
requirementObject() {
return this.isCreate
? {
iid: '',
title: '',
titleHtml: '',
description: '',
descriptionHtml: '',
}
: this.requirement;
},
},
watch: {
requirement: {
handler(value) {
this.title = value?.title || '';
this.description = value?.description || '';
this.satisfied = value?.satisfied || false;
},
deep: true,
......@@ -101,13 +103,19 @@ export default {
drawerOpen(value) {
// Clear `title` and `satisfied` value on drawer close.
if (!value) {
this.title = '';
this.description = '';
this.satisfied = false;
} else {
document.addEventListener('keydown', this.handleDocumentKeydown);
}
},
enableRequirementEdit(value) {
this.$nextTick(() => {
this.lastEl = this.getDrawerLastEl(value);
});
},
},
mounted() {
this.handleDocumentKeydown = this.handleDrawerKeydown.bind(this);
this.zenMode = new ZenMode();
$(this.$refs.gfmContainer).renderGFM();
$(document).on('zen_mode:enter', () => {
......@@ -133,6 +141,11 @@ export default {
return '';
},
getDrawerLastEl(isEditMode) {
return this.$refs.drawerEl.$el?.querySelector(
isEditMode ? '.js-requirement-cancel' : '.js-issuable-edit',
);
},
newLastTestReportState() {
// lastTestReportState determines whether a requirement is satisfied or not.
// Only create a new test report when manually marking/unmarking a requirement as satisfied:
......@@ -150,20 +163,50 @@ export default {
return null;
},
handleDrawerKeydown(e) {
const { keyCode, shiftKey } = e;
if (!this.firstEl) {
this.firstEl = this.$refs.drawerEl.$el?.querySelector('.gl-drawer-close-button');
}
if (!this.lastEl) {
this.lastEl = this.getDrawerLastEl(this.enableRequirementEdit || this.isCreate);
}
if (keyCode !== TAB_KEY_CODE) return;
if (!this.$refs.drawerEl.$el.contains(document.activeElement)) this.firstEl.focus();
if (shiftKey) {
if (document.activeElement === this.firstEl) {
this.lastEl.focus();
e.preventDefault();
}
} else if (document.activeElement === this.lastEl) {
this.firstEl.focus();
e.preventDefault();
}
},
handleDrawerClose() {
this.$emit(this.$options.events.drawerClose);
document.removeEventListener('keydown', this.handleDocumentKeydown);
this.firstEl = null;
this.lastEl = null;
},
handleFormInputKeyDown() {
if (this.zenModeEnabled) {
// Exit Zen mode, don't close the drawer.
this.zenModeEnabled = false;
this.zenMode.exit();
} else {
this.$emit(this.$options.events.disableEdit);
this.handleCancel();
}
},
handleSave() {
const { title, description } = this;
handleSave({ issuableTitle, issuableDescription }) {
const eventParams = {
title,
description,
title: issuableTitle,
description: issuableDescription,
};
if (!this.isCreate) {
......@@ -184,11 +227,12 @@ export default {
<template>
<gl-drawer
ref="drawerEl"
:open="drawerOpen"
:header-height="getDrawerHeaderHeight()"
:class="{ 'zen-mode gl-absolute': zenModeEnabled }"
class="requirement-form-drawer"
@close="$emit($options.events.drawerClose)"
@close="handleDrawerClose"
>
<template #header>
<h4 v-if="isCreate" class="gl-m-0">{{ __('New Requirement') }}</h4>
......@@ -203,91 +247,47 @@ export default {
</div>
</template>
<template>
<div v-if="!enableRequirementEdit && !isCreate" class="requirement-details">
<div
class="title-container gl-display-flex gl-border-b-1 gl-border-b-solid gl-border-gray-100"
>
<h3 v-safe-html="titleHtml" class="title qa-title gl-flex-grow-1 gl-m-0 gl-mb-3"></h3>
<gl-button
v-if="canUpdate && !isArchived"
v-gl-tooltip.bottom
data-testid="edit"
:title="__('Edit title and description')"
icon="pencil"
class="btn-edit gl-align-self-start"
@click="$emit($options.events.enableEdit, $event)"
/>
</div>
<div data-testid="descriptionContainer" class="description-container gl-mt-3">
<div ref="gfmContainer" v-safe-html="descriptionHtml" class="md"></div>
</div>
</div>
<div v-else class="requirement-form">
<div class="requirement-form-container" :class="{ 'gl-flex-grow-1 gl-mt-2': !isCreate }">
<div data-testid="form-error-container" class="flash-container"></div>
<gl-form-group
data-testid="title"
:label="__('Title')"
:invalid-feedback="$options.titleInvalidMessage"
:state="!titleInvalid"
class="gl-show-field-errors"
label-for="requirementTitle"
>
<gl-form-textarea
id="requirementTitle"
v-model.trim="title"
autofocus
resize
:disabled="requirementRequestActive"
:placeholder="__('Requirement title')"
max-rows="25"
class="requirement-form-textarea"
:class="{ 'gl-field-error-outline': titleInvalid }"
@keydown.escape.exact.stop="handleFormInputKeyDown"
@keydown.meta.enter="handleSave"
@keydown.ctrl.enter="handleSave"
/>
</gl-form-group>
<gl-form-group data-testid="description" class="common-note-form">
<label for="requirementDescription" class="d-block col-form-label gl-pb-0!">
{{ __('Description') }}
</label>
<markdown-field
:markdown-preview-path="descriptionPreviewPath"
:markdown-docs-path="descriptionHelpPath"
:enable-autocomplete="false"
:textarea-value="description"
>
<template #textarea>
<textarea
id="requirementDescription"
v-model="description"
:data-supports-quick-actions="false"
:aria-label="__('Description')"
:placeholder="__('Describe the requirement here')"
class="note-textarea js-gfm-input js-autosize markdown-area qa-description-textarea"
@keydown.escape.exact.stop="handleFormInputKeyDown"
@keydown.meta.enter="handleSave"
@keydown.ctrl.enter="handleSave"
></textarea>
</template>
</markdown-field>
<gl-form-checkbox v-if="!isCreate" v-model="satisfied" class="gl-mt-6">{{
__('Satisfied')
}}</gl-form-checkbox>
</gl-form-group>
<issuable-body
:issuable="requirementObject"
:enable-edit="canEditRequirement"
:enable-autocomplete="false"
:enable-autosave="false"
:edit-form-visible="enableRequirementEdit || isCreate"
:show-field-title="true"
:description-preview-path="descriptionPreviewPath"
:description-help-path="descriptionHelpPath"
status-badge-class="status-box-open"
status-icon="issue-open-m"
@edit-issuable="$emit($options.events.enableEdit, $event)"
@keydown-title.escape.exact.stop="handleFormInputKeyDown"
@keydown-description.escape.exact.stop="handleFormInputKeyDown"
@keydown-title.meta.enter="handleSave(arguments[1])"
@keydown-title.ctrl.enter="handleSave(arguments[1])"
@keydown-description.meta.enter="handleSave(arguments[1])"
@keydown-description.ctrl.enter="handleSave(arguments[1])"
>
<template #edit-form-actions="issuableMeta">
<gl-form-checkbox v-if="!isCreate" v-model="satisfied" class="gl-mt-6">{{
__('Satisfied')
}}</gl-form-checkbox>
<div class="gl-display-flex requirement-form-actions gl-mt-6">
<gl-button
:disabled="disableSaveButton"
:disabled="
requirementRequestActive ||
issuableMeta.issuableTitle.length > $options.maxTitleLength ||
!issuableMeta.issuableTitle.length
"
:loading="requirementRequestActive"
data-testid="requirement-save"
variant="success"
category="primary"
class="gl-mr-auto js-requirement-save"
@click="handleSave"
@click="handleSave(issuableMeta)"
>
{{ saveButtonLabel }}
</gl-button>
<gl-button
data-testid="requirement-cancel"
variant="default"
category="primary"
class="js-requirement-cancel"
......@@ -296,8 +296,8 @@ export default {
{{ __('Cancel') }}
</gl-button>
</div>
</div>
</div>
</template>
</issuable-body>
</template>
</gl-drawer>
</template>
......@@ -21,10 +21,42 @@
}
}
.requirement-form-drawer.zen-mode {
// We need to override `z-index` provided to GlDrawer
// in Zen mode to enable full-screen editing.
z-index: auto !important;
.requirement-form-drawer {
&.zen-mode {
// We need to override `z-index` provided to GlDrawer
// in Zen mode to enable full-screen editing.
z-index: auto !important;
}
// Following overrides are done to
// elements within `issuable_body.vue`
// and are specific to requirements.
.title-container {
@include gl-border-b-solid;
@include gl-border-b-gray-100;
@include gl-border-b-1;
&,
.title {
@include gl-mb-3;
}
.title {
@include gl-font-size-markdown-h2;
}
}
.issuable-details {
@include gl-py-0;
li.md-header-toolbar {
@include gl-py-3;
}
.detail-page-description {
@include gl-border-none;
}
}
}
}
......
---
title: Improve accessibility of keyboard navigation for Requirements
merge_request: 48325
author:
type: added
......@@ -16,8 +16,8 @@ RSpec.describe 'Requirements list', :js do
find('button.js-new-requirement').click
end
page.within('.requirements-list-container') do
find('textarea#requirementTitle').native.send_keys title
page.within('.requirement-form-drawer') do
find('input#issuable-title').native.send_keys title
find('button.js-requirement-save').click
wait_for_all_requests
......@@ -66,7 +66,7 @@ RSpec.describe 'Requirements list', :js do
end
page.within('.requirements-list-container') do
expect(page).to have_selector('.requirement-form')
expect(page).to have_selector('.requirement-form-drawer')
end
end
......@@ -137,7 +137,7 @@ RSpec.describe 'Requirements list', :js do
page.within('.requirement-form-drawer') do
expect(page.find('.title-container')).to have_content(requirement1.title)
expect(page.find('.title-container')).to have_selector('button.btn-edit')
expect(page.find('.description-container')).to have_content(requirement1.description)
expect(page.find('.description')).to have_content(requirement1.description)
end
end
......@@ -148,8 +148,8 @@ RSpec.describe 'Requirements list', :js do
page.within('.requirement-form-drawer') do
expect(page.find('.gl-drawer-header span', match: :first)).to have_content("REQ-#{requirement1.iid}")
expect(page.find('textarea#requirementTitle')['value']).to have_content("#{requirement1.title}")
expect(page.find('textarea#requirementDescription')['value']).to have_content("#{requirement1.description}")
expect(page.find('input#issuable-title')['value']).to have_content("#{requirement1.title}")
expect(page.find('textarea#issuable-description')['value']).to have_content("#{requirement1.description}")
expect(page.find('input[type="checkbox"]')['checked']).to eq(requirement1.last_test_report_state)
expect(page.find('.js-requirement-save')).to have_content('Save changes')
end
......@@ -164,8 +164,8 @@ RSpec.describe 'Requirements list', :js do
end
page.within('.requirement-form-drawer') do
find('textarea#requirementTitle').native.send_keys requirement_title
find('textarea#requirementDescription').native.send_keys requirement_description
find('input#issuable-title').native.send_keys requirement_title
find('textarea#issuable-description').native.send_keys requirement_description
find('input[type="checkbox"]').click
click_button 'Save changes'
......
import { GlDrawer, GlFormTextarea, GlFormCheckbox } from '@gitlab/ui';
import { GlDrawer, GlFormCheckbox } from '@gitlab/ui';
import { getByText } from '@testing-library/dom';
import { shallowMount } from '@vue/test-utils';
import $ from 'jquery';
......@@ -6,8 +6,10 @@ import $ from 'jquery';
import RequirementForm from 'ee/requirements/components/requirement_form.vue';
import RequirementStatusBadge from 'ee/requirements/components/requirement_status_badge.vue';
import { TestReportStatus, MAX_TITLE_LENGTH } from 'ee/requirements/constants';
import { TestReportStatus } from 'ee/requirements/constants';
import IssuableBody from '~/issuable_show/components/issuable_body.vue';
import IssuableEditForm from '~/issuable_show/components/issuable_edit_form.vue';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import ZenMode from '~/zen_mode';
......@@ -30,6 +32,8 @@ const createComponent = ({
},
stubs: {
GlDrawer,
IssuableBody,
IssuableEditForm,
MarkdownField,
},
});
......@@ -85,21 +89,30 @@ describe('RequirementForm', () => {
});
});
describe('titleInvalid', () => {
it('returns `false` when `title` length is less than max title limit', () => {
expect(wrapper.vm.titleInvalid).toBe(false);
describe('requirementObject', () => {
it('returns requirement object while in show/edit mode', async () => {
wrapper.setProps({
requirement: mockRequirementsOpen[0],
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.requirementObject).toBe(mockRequirementsOpen[0]);
});
it('returns `true` when `title` length is more than max title limit', () => {
wrapper.setData({
title: Array(MAX_TITLE_LENGTH + 1)
.fill()
.map(() => 'a')
.join(''),
it('returns empty requirement object while in create mode', async () => {
wrapper.setProps({
requirement: null,
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.titleInvalid).toBe(true);
await wrapper.vm.$nextTick();
expect(wrapper.vm.requirementObject).toMatchObject({
iid: '',
title: '',
titleHtml: '',
description: '',
descriptionHtml: '',
});
});
});
......@@ -108,26 +121,6 @@ describe('RequirementForm', () => {
describe('watchers', () => {
describe('requirement', () => {
describe('when requirement is not null', () => {
it('renders the value of `requirement.title` as title and `requirement.description` as description', async () => {
wrapper.setProps({
requirement: mockRequirementsOpen[0],
enableRequirementEdit: true,
});
await wrapper.vm.$nextTick();
expect(
wrapper
.find('[data-testid="title"]')
.find(GlFormTextarea)
.attributes('value'),
).toBe(mockRequirementsOpen[0].title);
expect(wrapper.find('[data-testid="description"] textarea').element.value).toBe(
mockRequirementsOpen[0].description,
);
});
it.each`
requirement | satisfied
${mockRequirementsOpen[0]} | ${true}
......@@ -153,37 +146,35 @@ describe('RequirementForm', () => {
});
});
it('renders empty string as title and description', async () => {
it('does not render the satisfied checkbox', async () => {
await wrapper.vm.$nextTick();
expect(
wrapper
.find('[data-testid="title"]')
.find(GlFormTextarea)
.attributes('value'),
).toBe('');
expect(wrapper.find('[data-testid="description"] textarea').element.value).toBe('');
expect(wrapper.find(GlFormCheckbox).exists()).toBe(false);
});
});
});
describe('drawerOpen', () => {
it('clears `title` value when `drawerOpen` prop is changed to false', async () => {
wrapper.setData({
title: 'Foo',
});
it('sets `satisfied` value to false when `drawerOpen` prop is changed to false', async () => {
wrapper.setProps({
drawerOpen: false,
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.title).toBe('');
expect(wrapper.vm.description).toBe('');
expect(wrapper.vm.satisfied).toBe(false);
});
it('binds `keydown` event listener on document when `drawerOpen` prop is changed to true', async () => {
jest.spyOn(document, 'addEventListener');
wrapper.setProps({
drawerOpen: true,
});
await wrapper.vm.$nextTick();
expect(document.addEventListener).toHaveBeenCalledWith('keydown', expect.any(Function));
});
});
});
......@@ -246,27 +237,29 @@ describe('RequirementForm', () => {
describe('handleSave', () => {
it('emits `save` event on component with object as param containing `title` & `description` when form is in create mode', () => {
const title = 'foo';
const description = '_bar_';
wrapper.setData({
title,
description,
});
const issuableTitle = 'foo';
const issuableDescription = '_bar_';
wrapper.vm.handleSave();
wrapper.vm.handleSave({
issuableTitle,
issuableDescription,
});
expect(wrapper.emitted('save')).toBeTruthy();
expect(wrapper.emitted('save')[0]).toEqual([
{
title,
description,
title: issuableTitle,
description: issuableDescription,
},
]);
});
it('emits `save` event on component with object as param containing `iid`, `title`, `description` & `lastTestReportState` when form is in update mode', () => {
const { iid, title, description } = mockRequirementsOpen[0];
wrapperWithRequirement.vm.handleSave();
wrapperWithRequirement.vm.handleSave({
issuableTitle: title,
issuableDescription: description,
});
expect(wrapperWithRequirement.emitted('save')).toBeTruthy();
expect(wrapperWithRequirement.emitted('save')[0]).toEqual([
......@@ -300,128 +293,43 @@ describe('RequirementForm', () => {
expect(wrapper.find(GlDrawer).exists()).toBe(true);
});
describe('create requirement', () => {
it('renders drawer header with string "New Requirement"', () => {
expect(getByText(wrapper.element, 'New Requirement')).not.toBeNull();
});
it('renders title and description input fields', () => {
expect(wrapper.find('[data-testid="title"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="description"]').exists()).toBe(true);
});
it('renders save button component', () => {
const saveButton = wrapper.find('.js-requirement-save');
expect(saveButton.exists()).toBe(true);
expect(saveButton.text()).toBe('Create requirement');
});
it('renders cancel button component', () => {
const cancelButton = wrapper.find('.js-requirement-cancel');
expect(cancelButton.exists()).toBe(true);
expect(cancelButton.text()).toBe('Cancel');
});
it('renders drawer header with `requirement.reference` and test report badge', () => {
expect(
getByText(wrapperWithRequirement.element, `REQ-${mockRequirementsOpen[0].iid}`),
).not.toBeNull();
expect(wrapperWithRequirement.find(RequirementStatusBadge).exists()).toBe(true);
expect(wrapperWithRequirement.find(RequirementStatusBadge).props('testReport')).toBe(
mockTestReport,
);
});
describe('view requirement', () => {
it('renders drawer header with `requirement.reference` and test report badge', () => {
expect(
getByText(wrapperWithRequirement.element, `REQ-${mockRequirementsOpen[0].iid}`),
).not.toBeNull();
expect(wrapperWithRequirement.find(RequirementStatusBadge).exists()).toBe(true);
expect(wrapperWithRequirement.find(RequirementStatusBadge).props('testReport')).toBe(
mockTestReport,
);
});
it('renders requirement title', () => {
expect(
getByText(wrapperWithRequirement.element, mockRequirementsOpen[0].titleHtml),
).not.toBeNull();
it('renders issuable-body component', () => {
const issuableBody = wrapperWithRequirement.find(IssuableBody);
expect(issuableBody.exists()).toBe(true);
expect(issuableBody.props()).toMatchObject({
enableEdit: wrapper.vm.canEditRequirement,
enableAutocomplete: false,
enableAutosave: false,
editFormVisible: false,
showFieldTitle: true,
descriptionPreviewPath: '/gitlab-org/gitlab-test/preview_markdown',
descriptionHelpPath: '/help/user/markdown',
});
});
it('renders edit button', () => {
const editButtonEl = wrapperWithRequirement.find('[data-testid="edit"]');
expect(editButtonEl.exists()).toBe(true);
expect(editButtonEl.props('icon')).toBe('pencil');
expect(editButtonEl.attributes('title')).toBe('Edit title and description');
});
it('renders requirement description', () => {
const descriptionEl = wrapperWithRequirement.find('[data-testid="descriptionContainer"]');
expect(descriptionEl.exists()).toBe(true);
expect(descriptionEl.text()).toBe('fortitudinis fomentis dolor mitigari solet.');
it('renders edit-form-actions slot contents within issuable-body', async () => {
wrapperWithRequirement.setProps({
enableRequirementEdit: true,
});
describe('edit', () => {
beforeEach(async () => {
wrapperWithRequirement.setProps({
enableRequirementEdit: true,
});
await wrapperWithRequirement.vm.$nextTick();
await wrapperWithRequirement.vm.$nextTick();
});
const issuableBody = wrapperWithRequirement.find(IssuableBody);
it('renders flash error container', () => {
expect(wrapperWithRequirement.find('[data-testid="form-error-container"]').exists()).toBe(
true,
);
});
it('renders title input field', () => {
const titleInputEl = wrapperWithRequirement.find('[data-testid="title"]');
const titleTextarea = titleInputEl.find(GlFormTextarea);
expect(titleInputEl.exists()).toBe(true);
expect(titleInputEl.attributes()).toMatchObject({
label: 'Title',
state: 'true',
'label-for': 'requirementTitle',
'invalid-feedback': `Requirement title cannot have more than ${MAX_TITLE_LENGTH} characters.`,
});
expect(titleTextarea.exists()).toBe(true);
expect(titleTextarea.attributes()).toMatchObject({
id: 'requirementTitle',
placeholder: 'Requirement title',
value: mockRequirementsOpen[0].title,
'max-rows': '25',
});
});
it('renders description input field', () => {
const descriptionInputEl = wrapperWithRequirement.find('[data-testid="description"]');
const markdownEl = descriptionInputEl.find(MarkdownField);
const descriptionTextarea = markdownEl.find('textarea');
expect(descriptionInputEl.exists()).toBe(true);
expect(descriptionInputEl.find('label').text()).toBe('Description');
expect(markdownEl.exists()).toBe(true);
expect(markdownEl.props()).toMatchObject({
markdownPreviewPath: '/gitlab-org/gitlab-test/preview_markdown',
markdownDocsPath: '/help/user/markdown',
enableAutocomplete: false,
textareaValue: mockRequirementsOpen[0].description,
});
expect(descriptionTextarea.exists()).toBe(true);
expect(descriptionTextarea.attributes()).toMatchObject({
id: 'requirementDescription',
placeholder: 'Describe the requirement here',
'aria-label': 'Description',
});
});
it('renders satisfied checkbox field', () => {
expect(wrapperWithRequirement.find(GlFormCheckbox).exists()).toBe(true);
expect(wrapperWithRequirement.find(GlFormCheckbox).text()).toBe('Satisfied');
});
});
expect(issuableBody.find(GlFormCheckbox).exists()).toBe(true);
expect(issuableBody.find('[data-testid="requirement-save"]').exists()).toBe(true);
expect(issuableBody.find('[data-testid="requirement-cancel"]').exists()).toBe(true);
});
});
});
......@@ -9415,9 +9415,6 @@ msgstr ""
msgid "Describe the goal of the changes and what reviewers should be aware of."
msgstr ""
msgid "Describe the requirement here"
msgstr ""
msgid "Description"
msgstr ""
......@@ -23561,9 +23558,6 @@ msgstr ""
msgid "Requirement %{reference} has been updated"
msgstr ""
msgid "Requirement title"
msgstr ""
msgid "Requirement title cannot have more than %{limit} characters."
msgstr ""
......
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