Commit ff51c552 authored by Rajat Jain's avatar Rajat Jain

Add confidential flag in epic create

Adds an ability to create epics as confidential, whose
access will be limited to a certain access group
parent 7c15e864
......@@ -146,12 +146,14 @@
display: inline-block;
position: relative;
&:not[type='checkbox'] {
/* Medium devices (desktops, 992px and up) */
@include media-breakpoint-up(md) { width: 200px; }
/* Large devices (large desktops, 1200px and up) */
@include media-breakpoint-up(lg) { width: 250px; }
}
}
@include media-breakpoint-down(sm) {
padding-bottom: 0;
......
......@@ -17,7 +17,11 @@ selected group. From your group page:
1. Go to **Epics**.
1. Click **New epic**.
1. Enter a descriptive title and click **Create epic**.
1. Enter a descriptive title.
1. To make the new epic confidential, select the **Make this epic confidential** checkbox
([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213068) in
[GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0). **(ULTIMATE)**
1. Click **Create epic** button.
You will be taken to the new epic where can edit the following details:
......@@ -29,9 +33,8 @@ You will be taken to the new epic where can edit the following details:
An epic's page contains the following tabs:
- **Epics and Issues**: epics and issues added to this epic. Child epics and their issues appear in
a tree view.
- Click the <kbd>></kbd> beside a parent epic to reveal the child epics and issues.
- **Epics and Issues**: epics and issues added to this epic. Child epics, and their issues, are shown in a tree view.
- Click on the <kbd>></kbd> beside a parent epic to reveal the child epics and issues.
- Hover over the total counts to see a breakdown of open and closed items.
- **Roadmap**: a roadmap view of child epics which have start and due dates.
......
<script>
import { mapState, mapActions } from 'vuex';
import { GlDeprecatedButton } from '@gitlab/ui';
import {
GlForm,
GlFormInput,
GlFormCheckbox,
GlIcon,
GlButton,
GlTooltipDirective,
GlDeprecatedButton,
} from '@gitlab/ui';
import { __ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
GlFormCheckbox,
GlIcon,
GlDeprecatedButton,
LoadingButton,
GlButton,
GlForm,
GlFormInput,
},
directives: {
autofocusonshow,
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
alignRight: {
type: Boolean,
......@@ -22,7 +36,7 @@ export default {
},
},
computed: {
...mapState(['newEpicTitle', 'epicCreateInProgress']),
...mapState(['newEpicTitle', 'newEpicConfidential', 'epicCreateInProgress']),
buttonLabel() {
return this.epicCreateInProgress ? __('Creating epic') : __('Create epic');
},
......@@ -39,9 +53,19 @@ export default {
return this.newEpicTitle;
},
},
epicConfidential: {
set(value) {
this.setEpicCreateConfidential({
newEpicConfidential: value,
});
},
get() {
return this.newEpicConfidential;
},
},
},
methods: {
...mapActions(['setEpicCreateTitle', 'createEpic']),
...mapActions(['setEpicCreateTitle', 'createEpic', 'setEpicCreateConfidential']),
},
};
</script>
......@@ -51,8 +75,10 @@ export default {
<gl-deprecated-button variant="success" class="qa-new-epic-button" data-toggle="dropdown">
{{ __('New epic') }}
</gl-deprecated-button>
<div :class="{ 'dropdown-menu-right': alignRight }" class="dropdown-menu">
<input
<gl-form>
<gl-form-input
ref="epicTitleInput"
v-model="epicTitle"
v-autofocusonshow
......@@ -63,13 +89,37 @@ export default {
data-qa-selector="epic_title_field"
@keyup.enter.exact="createEpic"
/>
<loading-button
<gl-form-checkbox
v-if="glFeatures.confidentialEpics"
v-model="epicConfidential"
class="mt-3 mb-3 mr-0"
><span> {{ __('Make this epic confidential') }} </span>
<span
v-gl-tooltip.viewport.top.hover
:title="
__(
'This epic and its child elements will only be visible to team members with at minimum Reporter access.',
)
"
:aria-label="
__(
'This epic and its child elements will only be visible to team members with at minimum Reporter access.',
)
"
>
<gl-icon name="question" :size="12"
/></span>
</gl-form-checkbox>
<gl-button
:disabled="isEpicCreateDisabled"
:loading="epicCreateInProgress"
:label="buttonLabel"
container-class="btn btn-success btn-inverted prepend-top-10 qa-create-epic-button"
category="primary"
variant="success"
class="prepend-top-10 qa-create-epic-button"
@click.stop="createEpic"
/>
>{{ buttonLabel }}</gl-button
>
</gl-form>
</div>
</div>
</template>
......@@ -282,6 +282,8 @@ export const toggleEpicSubscription = ({ state, dispatch }) => {
* Methods to handle Epic create from Epics index page
*/
export const setEpicCreateTitle = ({ commit }, data) => commit(types.SET_EPIC_CREATE_TITLE, data);
export const setEpicCreateConfidential = ({ commit }, data) =>
commit(types.SET_EPIC_CREATE_CONFIDENTIAL, data);
export const requestEpicCreate = ({ commit }) => commit(types.REQUEST_EPIC_CREATE);
export const requestEpicCreateSuccess = (_, webUrl) => visitUrl(webUrl);
export const requestEpicCreateFailure = ({ commit }) => {
......@@ -293,6 +295,7 @@ export const createEpic = ({ state, dispatch }) => {
axios
.post(state.endpoint, {
title: state.newEpicTitle,
confidential: state.newEpicConfidential,
})
.then(({ data }) => {
dispatch('requestEpicCreateSuccess', data.web_url);
......
......@@ -23,6 +23,7 @@ export const REQUEST_EPIC_SUBSCRIPTION_TOGGLE_SUCCESS = 'REQUEST_EPIC_SUBSCRIPTI
export const REQUEST_EPIC_SUBSCRIPTION_TOGGLE_FAILURE = 'REQUEST_EPIC_SUBSCRIPTION_TOGGLE_FAILURE';
export const SET_EPIC_CREATE_TITLE = 'SET_EPIC_CREATE_TITLE';
export const SET_EPIC_CREATE_CONFIDENTIAL = 'SET_EPIC_CREATE_CONFIDENTIAL';
export const REQUEST_EPIC_CREATE = 'REQUEST_EPIC_CREATE';
export const REQUEST_EPIC_CREATE_FAILURE = 'REQUEST_EPIC_CREATE_FAILURE';
......
......@@ -96,6 +96,9 @@ export default {
[types.SET_EPIC_CREATE_TITLE](state, { newEpicTitle }) {
state.newEpicTitle = newEpicTitle;
},
[types.SET_EPIC_CREATE_CONFIDENTIAL](state, { newEpicConfidential }) {
state.newEpicConfidential = newEpicConfidential;
},
[types.REQUEST_EPIC_CREATE](state) {
state.epicCreateInProgress = true;
},
......
......@@ -59,6 +59,7 @@ export default () => ({
// Create Epic Props
newEpicTitle: '',
newEpicConfidential: false,
// UI status flags
epicStatusChangeInProgress: false,
......
......@@ -18,6 +18,7 @@ class Groups::EpicsController < Groups::ApplicationController
before_action do
push_frontend_feature_flag(:vue_issuable_epic_sidebar, @group)
push_frontend_feature_flag(:confidential_epics, @group)
end
def index
......
......@@ -11,6 +11,7 @@ module Groups
before_action :persist_roadmap_layout, only: [:show]
before_action do
push_frontend_feature_flag(:roadmap_buffered_rendering, @group)
push_frontend_feature_flag(:confidential_epics, @group)
end
# show roadmap for a group
......
---
title: Add confidential flag in epic create
merge_request: 30370
author:
type: added
......@@ -58,7 +58,7 @@ describe 'New Epic', :js do
it 'can create epic' do
find('.epic-create-dropdown .btn-success').click
find('.epic-create-dropdown .dropdown-menu input').set('test epic title')
find('.epic-create-dropdown .dropdown-menu input[type="text"]').set('test epic title')
find('.epic-create-dropdown .dropdown-menu .btn-success').click
wait_for_requests
......
......@@ -80,6 +80,33 @@ describe('EpicCreateComponent', () => {
});
});
});
describe('epicConfidential', () => {
describe('set', () => {
it('calls `setEpicCreateConfidential` with param `value`', () => {
jest.spyOn(vm, 'setEpicCreateConfidential');
const newEpicConfidential = true;
vm.epicConfidential = newEpicConfidential;
expect(vm.setEpicCreateConfidential).toHaveBeenCalledWith(
expect.objectContaining({
newEpicConfidential,
}),
);
});
});
describe('get', () => {
it('returns value of `newEpicConfidential` from state', () => {
const newEpicConfidential = true;
vm.$store.state.newEpicConfidential = newEpicConfidential;
expect(vm.epicConfidential).toBe(newEpicConfidential);
});
});
});
});
describe('template', () => {
......
......@@ -1122,6 +1122,23 @@ describe('Epic Store Actions', () => {
});
});
describe('setEpicCreateConfidential', () => {
it('should set `state.newEpicConfidential` value to the value of `newEpicConfidential` param', done => {
const data = {
newEpicConfidential: true,
};
testAction(
actions.setEpicCreateConfidential,
data,
{ newEpicConfidential: true },
[{ type: 'SET_EPIC_CREATE_CONFIDENTIAL', payload: { ...data } }],
[],
done,
);
});
});
describe('requestEpicCreate', () => {
it('should set `state.epicCreateInProgress` flag to `true`', done => {
testAction(
......@@ -1166,6 +1183,7 @@ describe('Epic Store Actions', () => {
let mock;
const stateCreateEpic = {
newEpicTitle: 'foobar',
newEpicConfidential: true,
};
beforeEach(() => {
......
......@@ -320,6 +320,20 @@ describe('Epic Store Mutations', () => {
});
});
describe('SET_EPIC_CREATE_CONFIDENTIAL', () => {
it('Should set `newEpicConfidential` prop on state as with the value of provided `newEpicConfidential` param', () => {
const state = {
newEpicConfidential: true,
};
mutations[types.SET_EPIC_CREATE_CONFIDENTIAL](state, {
newEpicConfidential: true,
});
expect(state.newEpicConfidential).toBe(true);
});
});
describe('REQUEST_EPIC_CREATE', () => {
it('Should set `epicCreateInProgress` flag on state as `true`', () => {
const state = {
......
......@@ -12893,6 +12893,9 @@ msgstr ""
msgid "Make sure you're logged into the account that owns the projects you'd like to import."
msgstr ""
msgid "Make this epic confidential"
msgstr ""
msgid "Makes this issue confidential."
msgstr ""
......@@ -21734,6 +21737,9 @@ msgstr ""
msgid "This epic already has the maximum number of child epics."
msgstr ""
msgid "This epic and its child elements will only be visible to team members with at minimum Reporter access."
msgstr ""
msgid "This epic does not exist or you don't have sufficient permission."
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