Commit 95f6a9f9 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'create-confidential-epic' into 'master'

Add confidential flag in epic create

See merge request gitlab-org/gitlab!30370
parents 416f2b18 095bf2c1
......@@ -146,11 +146,13 @@
display: inline-block;
position: relative;
/* Medium devices (desktops, 992px and up) */
@include media-breakpoint-up(md) { width: 200px; }
&: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; }
/* Large devices (large desktops, 1200px and up) */
@include media-breakpoint-up(lg) { width: 250px; }
}
}
@include media-breakpoint-down(sm) {
......
......@@ -31,6 +31,7 @@ To learn what you can do with an epic, see [Manage epics](manage_epics.md). Poss
- [Reopen a closed epic](manage_epics.md#reopen-a-closed-epic)
- [Go to an epic from an issue](manage_epics.md#go-to-an-epic-from-an-issue)
- [Search for an epic from epics list page](manage_epics.md#search-for-an-epic-from-epics-list-page)
- [Make an epic confidential](manage_epics.md#make-an-epic-confidential)
- [Manage issues assigned to an epic](manage_epics.md#manage-issues-assigned-to-an-epic)
- [Manage multi-level child epics **(ULTIMATE)**](manage_epics.md#manage-multi-level-child-epics-ultimate)
......
......@@ -17,7 +17,8 @@ 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. Click **Create epic**.
You will be taken to the new epic where can edit the following details:
......@@ -29,8 +30,7 @@ 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.
- **Epics and Issues**: epics and issues added to this epic. Child epics, and their issues, are shown in a tree view.
- 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.
- **Roadmap**: a roadmap view of child epics which have start and due dates.
......@@ -121,6 +121,36 @@ The sort option and order is saved and used wherever you browse epics, including
![epics sort](img/epics_sort.png)
## Make an epic confidential
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213068) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0.
> - It's deployed behind a feature flag, disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-confidential-epics-premium-only). **(PREMIUM ONLY)**
When you're creating an epic, you can make it confidential by selecting the **Make this epic
confidential** checkbox.
### Enable Confidential Epics **(PREMIUM ONLY)**
The Confidential Epics feature is under development and not ready for production use. It's deployed behind a
feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can enable it for your instance.
To enable it:
```ruby
Feature.enable(:confidential_epics)
```
To disable it:
```ruby
Feature.disable(:confidential_epics)
```
## Manage issues assigned to an epic
### Add an issue to an epic
......
<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,25 +75,51 @@ 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
ref="epicTitleInput"
v-model="epicTitle"
v-autofocusonshow
:disabled="epicCreateInProgress"
:placeholder="__('Title')"
type="text"
class="form-control"
data-qa-selector="epic_title_field"
@keyup.enter.exact="createEpic"
/>
<loading-button
:disabled="isEpicCreateDisabled"
:loading="epicCreateInProgress"
:label="buttonLabel"
container-class="btn btn-success btn-inverted prepend-top-10 qa-create-epic-button"
@click.stop="createEpic"
/>
<gl-form>
<gl-form-input
ref="epicTitleInput"
v-model="epicTitle"
v-autofocusonshow
:disabled="epicCreateInProgress"
:placeholder="__('Title')"
type="text"
class="form-control"
data-qa-selector="epic_title_field"
@keyup.enter.exact="createEpic"
/>
<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"
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 = {
......
......@@ -12920,6 +12920,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 ""
......@@ -21773,6 +21776,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