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,11 +146,13 @@ ...@@ -146,11 +146,13 @@
display: inline-block; display: inline-block;
position: relative; position: relative;
/* Medium devices (desktops, 992px and up) */ &:not[type='checkbox'] {
@include media-breakpoint-up(md) { width: 200px; } /* Medium devices (desktops, 992px and up) */
@include media-breakpoint-up(md) { width: 200px; }
/* Large devices (large desktops, 1200px and up) */ /* Large devices (large desktops, 1200px and up) */
@include media-breakpoint-up(lg) { width: 250px; } @include media-breakpoint-up(lg) { width: 250px; }
}
} }
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {
......
...@@ -17,7 +17,11 @@ selected group. From your group page: ...@@ -17,7 +17,11 @@ selected group. From your group page:
1. Go to **Epics**. 1. Go to **Epics**.
1. Click **New epic**. 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: 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: ...@@ -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: 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 - **Epics and Issues**: epics and issues added to this epic. Child epics, and their issues, are shown in a tree view.
a tree view. - Click on the <kbd>></kbd> beside a parent epic to reveal the child epics and issues.
- Click 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. - 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. - **Roadmap**: a roadmap view of child epics which have start and due dates.
......
<script> <script>
import { mapState, mapActions } from 'vuex'; 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 { __ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
components: { components: {
GlFormCheckbox,
GlIcon,
GlDeprecatedButton, GlDeprecatedButton,
LoadingButton, GlButton,
GlForm,
GlFormInput,
}, },
directives: { directives: {
autofocusonshow, autofocusonshow,
GlTooltip: GlTooltipDirective,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
alignRight: { alignRight: {
type: Boolean, type: Boolean,
...@@ -22,7 +36,7 @@ export default { ...@@ -22,7 +36,7 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['newEpicTitle', 'epicCreateInProgress']), ...mapState(['newEpicTitle', 'newEpicConfidential', 'epicCreateInProgress']),
buttonLabel() { buttonLabel() {
return this.epicCreateInProgress ? __('Creating epic') : __('Create epic'); return this.epicCreateInProgress ? __('Creating epic') : __('Create epic');
}, },
...@@ -39,9 +53,19 @@ export default { ...@@ -39,9 +53,19 @@ export default {
return this.newEpicTitle; return this.newEpicTitle;
}, },
}, },
epicConfidential: {
set(value) {
this.setEpicCreateConfidential({
newEpicConfidential: value,
});
},
get() {
return this.newEpicConfidential;
},
},
}, },
methods: { methods: {
...mapActions(['setEpicCreateTitle', 'createEpic']), ...mapActions(['setEpicCreateTitle', 'createEpic', 'setEpicCreateConfidential']),
}, },
}; };
</script> </script>
...@@ -51,25 +75,51 @@ export default { ...@@ -51,25 +75,51 @@ export default {
<gl-deprecated-button variant="success" class="qa-new-epic-button" data-toggle="dropdown"> <gl-deprecated-button variant="success" class="qa-new-epic-button" data-toggle="dropdown">
{{ __('New epic') }} {{ __('New epic') }}
</gl-deprecated-button> </gl-deprecated-button>
<div :class="{ 'dropdown-menu-right': alignRight }" class="dropdown-menu"> <div :class="{ 'dropdown-menu-right': alignRight }" class="dropdown-menu">
<input <gl-form>
ref="epicTitleInput" <gl-form-input
v-model="epicTitle" ref="epicTitleInput"
v-autofocusonshow v-model="epicTitle"
:disabled="epicCreateInProgress" v-autofocusonshow
:placeholder="__('Title')" :disabled="epicCreateInProgress"
type="text" :placeholder="__('Title')"
class="form-control" type="text"
data-qa-selector="epic_title_field" class="form-control"
@keyup.enter.exact="createEpic" data-qa-selector="epic_title_field"
/> @keyup.enter.exact="createEpic"
<loading-button />
:disabled="isEpicCreateDisabled" <gl-form-checkbox
:loading="epicCreateInProgress" v-if="glFeatures.confidentialEpics"
:label="buttonLabel" v-model="epicConfidential"
container-class="btn btn-success btn-inverted prepend-top-10 qa-create-epic-button" class="mt-3 mb-3 mr-0"
@click.stop="createEpic" ><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"
category="primary"
variant="success"
class="prepend-top-10 qa-create-epic-button"
@click.stop="createEpic"
>{{ buttonLabel }}</gl-button
>
</gl-form>
</div> </div>
</div> </div>
</template> </template>
...@@ -282,6 +282,8 @@ export const toggleEpicSubscription = ({ state, dispatch }) => { ...@@ -282,6 +282,8 @@ export const toggleEpicSubscription = ({ state, dispatch }) => {
* Methods to handle Epic create from Epics index page * Methods to handle Epic create from Epics index page
*/ */
export const setEpicCreateTitle = ({ commit }, data) => commit(types.SET_EPIC_CREATE_TITLE, data); 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 requestEpicCreate = ({ commit }) => commit(types.REQUEST_EPIC_CREATE);
export const requestEpicCreateSuccess = (_, webUrl) => visitUrl(webUrl); export const requestEpicCreateSuccess = (_, webUrl) => visitUrl(webUrl);
export const requestEpicCreateFailure = ({ commit }) => { export const requestEpicCreateFailure = ({ commit }) => {
...@@ -293,6 +295,7 @@ export const createEpic = ({ state, dispatch }) => { ...@@ -293,6 +295,7 @@ export const createEpic = ({ state, dispatch }) => {
axios axios
.post(state.endpoint, { .post(state.endpoint, {
title: state.newEpicTitle, title: state.newEpicTitle,
confidential: state.newEpicConfidential,
}) })
.then(({ data }) => { .then(({ data }) => {
dispatch('requestEpicCreateSuccess', data.web_url); dispatch('requestEpicCreateSuccess', data.web_url);
......
...@@ -23,6 +23,7 @@ export const REQUEST_EPIC_SUBSCRIPTION_TOGGLE_SUCCESS = 'REQUEST_EPIC_SUBSCRIPTI ...@@ -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 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_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 = 'REQUEST_EPIC_CREATE';
export const REQUEST_EPIC_CREATE_FAILURE = 'REQUEST_EPIC_CREATE_FAILURE'; export const REQUEST_EPIC_CREATE_FAILURE = 'REQUEST_EPIC_CREATE_FAILURE';
......
...@@ -96,6 +96,9 @@ export default { ...@@ -96,6 +96,9 @@ export default {
[types.SET_EPIC_CREATE_TITLE](state, { newEpicTitle }) { [types.SET_EPIC_CREATE_TITLE](state, { newEpicTitle }) {
state.newEpicTitle = newEpicTitle; state.newEpicTitle = newEpicTitle;
}, },
[types.SET_EPIC_CREATE_CONFIDENTIAL](state, { newEpicConfidential }) {
state.newEpicConfidential = newEpicConfidential;
},
[types.REQUEST_EPIC_CREATE](state) { [types.REQUEST_EPIC_CREATE](state) {
state.epicCreateInProgress = true; state.epicCreateInProgress = true;
}, },
......
...@@ -59,6 +59,7 @@ export default () => ({ ...@@ -59,6 +59,7 @@ export default () => ({
// Create Epic Props // Create Epic Props
newEpicTitle: '', newEpicTitle: '',
newEpicConfidential: false,
// UI status flags // UI status flags
epicStatusChangeInProgress: false, epicStatusChangeInProgress: false,
......
...@@ -18,6 +18,7 @@ class Groups::EpicsController < Groups::ApplicationController ...@@ -18,6 +18,7 @@ class Groups::EpicsController < Groups::ApplicationController
before_action do before_action do
push_frontend_feature_flag(:vue_issuable_epic_sidebar, @group) push_frontend_feature_flag(:vue_issuable_epic_sidebar, @group)
push_frontend_feature_flag(:confidential_epics, @group)
end end
def index def index
......
...@@ -11,6 +11,7 @@ module Groups ...@@ -11,6 +11,7 @@ module Groups
before_action :persist_roadmap_layout, only: [:show] before_action :persist_roadmap_layout, only: [:show]
before_action do before_action do
push_frontend_feature_flag(:roadmap_buffered_rendering, @group) push_frontend_feature_flag(:roadmap_buffered_rendering, @group)
push_frontend_feature_flag(:confidential_epics, @group)
end end
# show roadmap for a group # 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 ...@@ -58,7 +58,7 @@ describe 'New Epic', :js do
it 'can create epic' do it 'can create epic' do
find('.epic-create-dropdown .btn-success').click 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 find('.epic-create-dropdown .dropdown-menu .btn-success').click
wait_for_requests wait_for_requests
......
...@@ -80,6 +80,33 @@ describe('EpicCreateComponent', () => { ...@@ -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', () => { describe('template', () => {
......
...@@ -1122,6 +1122,23 @@ describe('Epic Store Actions', () => { ...@@ -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', () => { describe('requestEpicCreate', () => {
it('should set `state.epicCreateInProgress` flag to `true`', done => { it('should set `state.epicCreateInProgress` flag to `true`', done => {
testAction( testAction(
...@@ -1166,6 +1183,7 @@ describe('Epic Store Actions', () => { ...@@ -1166,6 +1183,7 @@ describe('Epic Store Actions', () => {
let mock; let mock;
const stateCreateEpic = { const stateCreateEpic = {
newEpicTitle: 'foobar', newEpicTitle: 'foobar',
newEpicConfidential: true,
}; };
beforeEach(() => { beforeEach(() => {
......
...@@ -320,6 +320,20 @@ describe('Epic Store Mutations', () => { ...@@ -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', () => { describe('REQUEST_EPIC_CREATE', () => {
it('Should set `epicCreateInProgress` flag on state as `true`', () => { it('Should set `epicCreateInProgress` flag on state as `true`', () => {
const state = { const state = {
......
...@@ -12893,6 +12893,9 @@ msgstr "" ...@@ -12893,6 +12893,9 @@ msgstr ""
msgid "Make sure you're logged into the account that owns the projects you'd like to import." msgid "Make sure you're logged into the account that owns the projects you'd like to import."
msgstr "" msgstr ""
msgid "Make this epic confidential"
msgstr ""
msgid "Makes this issue confidential." msgid "Makes this issue confidential."
msgstr "" msgstr ""
...@@ -21734,6 +21737,9 @@ msgstr "" ...@@ -21734,6 +21737,9 @@ msgstr ""
msgid "This epic already has the maximum number of child epics." msgid "This epic already has the maximum number of child epics."
msgstr "" 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." msgid "This epic does not exist or you don't have sufficient permission."
msgstr "" 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