Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
dc0cb770
Commit
dc0cb770
authored
Nov 15, 2017
by
Jarka Kadlecova
Committed by
Jacob Schatz
Nov 15, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add create epic
parent
a4308d4d
Changes
18
Show whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
534 additions
and
98 deletions
+534
-98
app/assets/stylesheets/framework/dropdowns.scss
app/assets/stylesheets/framework/dropdowns.scss
+29
-0
changelogs/unreleased-ee/3937-new-epic.yml
changelogs/unreleased-ee/3937-new-epic.yml
+5
-0
config/webpack.config.js
config/webpack.config.js
+1
-0
ee/app/assets/javascripts/epics/new_epic/components/new_epic.vue
...assets/javascripts/epics/new_epic/components/new_epic.vue
+92
-0
ee/app/assets/javascripts/epics/new_epic/new_epic_bundle.js
ee/app/assets/javascripts/epics/new_epic/new_epic_bundle.js
+17
-0
ee/app/assets/javascripts/epics/new_epic/services/new_epic_service.js
...s/javascripts/epics/new_epic/services/new_epic_service.js
+17
-0
ee/app/controllers/groups/epic_issues_controller.rb
ee/app/controllers/groups/epic_issues_controller.rb
+2
-1
ee/app/controllers/groups/epics_controller.rb
ee/app/controllers/groups/epics_controller.rb
+18
-1
ee/app/models/ee/epic.rb
ee/app/models/ee/epic.rb
+4
-0
ee/app/services/epics/create_service.rb
ee/app/services/epics/create_service.rb
+20
-0
ee/app/views/groups/epics/new.html.haml
ee/app/views/groups/epics/new.html.haml
+0
-0
ee/app/views/shared/_epics.html.haml
ee/app/views/shared/_epics.html.haml
+6
-0
ee/app/views/shared/empty_states/_epics.html.haml
ee/app/views/shared/empty_states/_epics.html.haml
+5
-2
spec/ee/spec/controllers/groups/epic_issues_controller_spec.rb
...ee/spec/controllers/groups/epic_issues_controller_spec.rb
+90
-94
spec/ee/spec/controllers/groups/epics_controller_spec.rb
spec/ee/spec/controllers/groups/epics_controller_spec.rb
+54
-0
spec/ee/spec/features/epics/new_epic_spec.rb
spec/ee/spec/features/epics/new_epic_spec.rb
+68
-0
spec/ee/spec/services/epics/create_service_spec.rb
spec/ee/spec/services/epics/create_service_spec.rb
+20
-0
spec/javascripts/epics/new_epic/components/new_epic_spec.js
spec/javascripts/epics/new_epic/components/new_epic_spec.js
+86
-0
No files found.
app/assets/stylesheets/framework/dropdowns.scss
View file @
dc0cb770
...
@@ -1029,3 +1029,32 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
...
@@ -1029,3 +1029,32 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
}
}
}
}
}
.new-epic-dropdown
{
.dropdown-menu
{
padding-left
:
$gl-padding-top
;
padding-right
:
$gl-padding-top
;
}
.form-control
{
width
:
100%
;
}
.btn-save
{
display
:
flex
;
margin-top
:
$gl-btn-padding
;
}
}
.empty-state
.new-epic-dropdown
{
display
:
inline-flex
;
.btn-save
{
margin-left
:
0
;
margin-bottom
:
0
;
}
.btn-new
{
margin
:
0
;
}
}
changelogs/unreleased-ee/3937-new-epic.yml
0 → 100644
View file @
dc0cb770
---
title
:
Add ability to create new epics
merge_request
:
author
:
type
:
added
config/webpack.config.js
View file @
dc0cb770
...
@@ -41,6 +41,7 @@ var config = {
...
@@ -41,6 +41,7 @@ var config = {
environments
:
'
./environments/environments_bundle.js
'
,
environments
:
'
./environments/environments_bundle.js
'
,
environments_folder
:
'
./environments/folder/environments_folder_bundle.js
'
,
environments_folder
:
'
./environments/folder/environments_folder_bundle.js
'
,
epic_show
:
'
ee/epics/epic_show/epic_show_bundle.js
'
,
epic_show
:
'
ee/epics/epic_show/epic_show_bundle.js
'
,
new_epic
:
'
ee/epics/new_epic/new_epic_bundle.js
'
,
filtered_search
:
'
./filtered_search/filtered_search_bundle.js
'
,
filtered_search
:
'
./filtered_search/filtered_search_bundle.js
'
,
graphs
:
'
./graphs/graphs_bundle.js
'
,
graphs
:
'
./graphs/graphs_bundle.js
'
,
graphs_charts
:
'
./graphs/graphs_charts.js
'
,
graphs_charts
:
'
./graphs/graphs_charts.js
'
,
...
...
ee/app/assets/javascripts/epics/new_epic/components/new_epic.vue
0 → 100644
View file @
dc0cb770
<
script
>
import
Flash
from
'
~/flash
'
;
import
{
visitUrl
}
from
'
~/lib/utils/url_utility
'
;
import
{
s__
}
from
'
~/locale
'
;
import
loadingButton
from
'
~/vue_shared/components/loading_button.vue
'
;
import
NewEpicService
from
'
../services/new_epic_service
'
;
export
default
{
name
:
'
newEpic
'
,
props
:
{
endpoint
:
{
type
:
String
,
required
:
true
,
},
alignRight
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
data
()
{
return
{
service
:
new
NewEpicService
(
this
.
endpoint
),
creating
:
false
,
title
:
''
,
};
},
components
:
{
loadingButton
,
},
computed
:
{
buttonLabel
()
{
return
this
.
creating
?
s__
(
'
Creating epic
'
)
:
s__
(
'
Create epic
'
);
},
isCreatingDisabled
()
{
return
this
.
title
.
length
===
0
;
},
},
methods
:
{
createEpic
()
{
this
.
creating
=
true
;
this
.
service
.
createEpic
(
this
.
title
)
.
then
(
res
=>
res
.
json
())
.
then
((
data
)
=>
{
visitUrl
(
data
.
web_url
);
})
.
catch
(()
=>
{
this
.
creating
=
false
;
Flash
(
s__
(
'
Error creating epic
'
));
});
},
focusInput
()
{
// Wait for dropdown to appear because of transition CSS
setTimeout
(()
=>
{
this
.
$refs
.
title
.
focus
();
},
25
);
},
},
};
</
script
>
<
template
>
<div
class=
"dropdown new-epic-dropdown"
>
<button
class=
"btn btn-new"
type=
"button"
data-toggle=
"dropdown"
@
click=
"focusInput"
>
{{
s__
(
'
New epic
'
)
}}
</button>
<div
class=
"dropdown-menu"
:class=
"
{ 'dropdown-menu-align-right' : alignRight }"
>
<input
ref=
"title"
type=
"text"
class=
"form-control"
:placeholder=
"s__('Title')"
v-model=
"title"
/>
<loading-button
container-class=
"btn btn-save btn-inverted"
:disabled=
"isCreatingDisabled"
:loading=
"creating"
:label=
"buttonLabel"
@
click.stop=
"createEpic"
/>
</div>
</div>
</
template
>
ee/app/assets/javascripts/epics/new_epic/new_epic_bundle.js
0 → 100644
View file @
dc0cb770
import
Vue
from
'
vue
'
;
import
NewEpicApp
from
'
./components/new_epic.vue
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
{
const
el
=
document
.
querySelector
(
'
#new-epic-app
'
);
const
props
=
el
.
dataset
;
return
new
Vue
({
el
,
components
:
{
'
new-epic-app
'
:
NewEpicApp
,
},
render
:
createElement
=>
createElement
(
'
new-epic-app
'
,
{
props
,
}),
});
});
ee/app/assets/javascripts/epics/new_epic/services/new_epic_service.js
0 → 100644
View file @
dc0cb770
import
Vue
from
'
vue
'
;
import
VueResource
from
'
vue-resource
'
;
Vue
.
use
(
VueResource
);
export
default
class
NewEpicService
{
constructor
(
endpoint
)
{
this
.
endpoint
=
endpoint
;
this
.
resource
=
Vue
.
resource
(
this
.
endpoint
,
{});
}
createEpic
(
title
)
{
return
this
.
resource
.
save
({
title
,
});
}
}
ee/app/controllers/groups/epic_issues_controller.rb
View file @
dc0cb770
class
Groups::EpicIssuesController
<
Groups
::
EpicsController
class
Groups::EpicIssuesController
<
Groups
::
EpicsController
include
IssuableLinks
include
IssuableLinks
before_action
:check_epics_available!
skip_before_action
:authorize_destroy_issuable!
skip_before_action
:authorize_destroy_issuable!
skip_before_action
:authorize_create_epic!
before_action
:authorize_admin_epic!
,
only:
[
:create
,
:destroy
]
before_action
:authorize_admin_epic!
,
only:
[
:create
,
:destroy
]
before_action
:authorize_issue_link_association!
,
only: :destroy
before_action
:authorize_issue_link_association!
,
only: :destroy
...
...
ee/app/controllers/groups/epics_controller.rb
View file @
dc0cb770
...
@@ -3,9 +3,10 @@ class Groups::EpicsController < Groups::ApplicationController
...
@@ -3,9 +3,10 @@ class Groups::EpicsController < Groups::ApplicationController
include
IssuableCollections
include
IssuableCollections
before_action
:check_epics_available!
before_action
:check_epics_available!
before_action
:epic
,
except:
:index
before_action
:epic
,
except:
[
:index
,
:create
]
before_action
:set_issuables_index
,
only: :index
before_action
:set_issuables_index
,
only: :index
before_action
:authorize_update_issuable!
,
only: :update
before_action
:authorize_update_issuable!
,
only: :update
before_action
:authorize_create_epic!
,
only:
[
:create
]
skip_before_action
:labels
skip_before_action
:labels
...
@@ -23,6 +24,18 @@ class Groups::EpicsController < Groups::ApplicationController
...
@@ -23,6 +24,18 @@ class Groups::EpicsController < Groups::ApplicationController
end
end
end
end
def
create
@epic
=
Epics
::
CreateService
.
new
(
@group
,
current_user
,
epic_params
).
execute
if
@epic
.
persisted?
render
json:
{
web_url:
group_epic_path
(
@group
,
@epic
)
}
else
head
:unprocessable_entity
end
end
private
private
def
epic
def
epic
...
@@ -73,4 +86,8 @@ class Groups::EpicsController < Groups::ApplicationController
...
@@ -73,4 +86,8 @@ class Groups::EpicsController < Groups::ApplicationController
def
set_default_state
def
set_default_state
params
[
:state
]
=
'all'
params
[
:state
]
=
'all'
end
end
def
authorize_create_epic!
return
render_404
unless
can?
(
current_user
,
:create_epic
,
group
)
end
end
end
ee/app/models/ee/epic.rb
View file @
dc0cb770
...
@@ -27,6 +27,10 @@ module EE
...
@@ -27,6 +27,10 @@ module EE
false
false
end
end
# we don't support project epics for epics yet, planned in the future #4019
def
update_project_counter_caches
end
def
issues
(
current_user
)
def
issues
(
current_user
)
related_issues
=
::
Issue
.
select
(
'issues.*, epic_issues.id as epic_issue_id'
)
related_issues
=
::
Issue
.
select
(
'issues.*, epic_issues.id as epic_issue_id'
)
.
joins
(
:epic_issue
)
.
joins
(
:epic_issue
)
...
...
ee/app/services/epics/create_service.rb
0 → 100644
View file @
dc0cb770
module
Epics
class
CreateService
<
IssuableBaseService
attr_reader
:group
def
initialize
(
group
,
current_user
,
params
)
@group
,
@current_user
,
@params
=
group
,
current_user
,
params
end
def
execute
@epic
=
group
.
epics
.
new
(
whitelisted_epic_params
)
create
(
@epic
)
end
private
def
whitelisted_epic_params
params
.
slice
(
:title
,
:description
,
:start_date
,
:end_date
)
end
end
end
ee/app/views/groups/epics/new.html.haml
0 → 100644
View file @
dc0cb770
ee/app/views/shared/_epics.html.haml
View file @
dc0cb770
.top-area
.top-area
=
render
'shared/issuable/nav'
,
type: :epics
=
render
'shared/issuable/nav'
,
type: :epics
.nav-controls
-
if
can?
(
current_user
,
:create_epic
,
@group
)
-
content_for
:page_specific_javascripts
do
=
webpack_bundle_tag
'common_vue'
=
webpack_bundle_tag
'new_epic'
#new-epic-app
{
data:
{
endpoint:
request
.
url
,
'align-right'
=>
true
}
}
%ul
.content-list.issuable-list
%ul
.content-list.issuable-list
=
render
partial:
'groups/epics/epic'
,
collection:
@epics
=
render
partial:
'groups/epics/epic'
,
collection:
@epics
...
...
ee/app/views/shared/empty_states/_epics.html.haml
View file @
dc0cb770
...
@@ -8,5 +8,8 @@
...
@@ -8,5 +8,8 @@
=
_
(
'Epics let you manage your portfolio of projects more efficiently and with less effort'
)
=
_
(
'Epics let you manage your portfolio of projects more efficiently and with less effort'
)
%p
%p
=
_
(
'Track groups of issues that share a theme, across projects and milestones'
)
=
_
(
'Track groups of issues that share a theme, across projects and milestones'
)
%button
.btn.btn-new
{
type:
'button'
}
-
if
can?
(
current_user
,
:create_epic
,
@group
)
New epic
-
content_for
:page_specific_javascripts
do
=
webpack_bundle_tag
'common_vue'
=
webpack_bundle_tag
'new_epic'
#new-epic-app
{
data:
{
endpoint:
request
.
url
}
}
spec/ee/spec/controllers/groups/epic_issues_controller_spec.rb
View file @
dc0cb770
...
@@ -7,13 +7,10 @@ describe Groups::EpicIssuesController do
...
@@ -7,13 +7,10 @@ describe Groups::EpicIssuesController do
let
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
let
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
let
(
:user
)
{
create
(
:user
)
}
let
(
:user
)
{
create
(
:user
)
}
before
do
sign_in
(
user
)
end
context
'when epics feature is enabled'
do
before
do
before
do
stub_licensed_features
(
epics:
true
)
stub_licensed_features
(
epics:
true
)
sign_in
(
user
)
end
end
describe
'GET #index'
do
describe
'GET #index'
do
...
@@ -147,5 +144,4 @@ describe Groups::EpicIssuesController do
...
@@ -147,5 +144,4 @@ describe Groups::EpicIssuesController do
end
end
end
end
end
end
end
end
end
spec/ee/spec/controllers/groups/epics_controller_spec.rb
View file @
dc0cb770
...
@@ -139,6 +139,7 @@ describe Groups::EpicsController do
...
@@ -139,6 +139,7 @@ describe Groups::EpicsController do
describe
'GET #realtime_changes'
do
describe
'GET #realtime_changes'
do
subject
{
get
:realtime_changes
,
group_id:
group
,
id:
epic
.
to_param
}
subject
{
get
:realtime_changes
,
group_id:
group
,
id:
epic
.
to_param
}
it
'returns epic'
do
it
'returns epic'
do
group
.
add_developer
(
user
)
group
.
add_developer
(
user
)
subject
subject
...
@@ -156,6 +157,59 @@ describe Groups::EpicsController do
...
@@ -156,6 +157,59 @@ describe Groups::EpicsController do
end
end
end
end
describe
'#create'
do
subject
do
post
:create
,
group_id:
group
,
epic:
{
title:
'new epic'
,
description:
'some descripition'
}
end
context
'when user has permissions to create an epic'
do
before
do
group
.
add_developer
(
user
)
end
context
'when all required parameters are passed'
do
it
'returns 200 response'
do
subject
expect
(
response
).
to
have_http_status
(
200
)
end
it
'creates a new epic'
do
expect
{
subject
}.
to
change
{
Epic
.
count
}.
from
(
0
).
to
(
1
)
end
it
'returns the correct json'
do
subject
expect
(
JSON
.
parse
(
response
.
body
)).
to
eq
({
'web_url'
=>
group_epic_path
(
group
,
Epic
.
last
)
})
end
end
context
'when required parameter is missing'
do
before
do
post
:create
,
group_id:
group
,
epic:
{
description:
'some descripition'
}
end
it
'returns 422 response'
do
expect
(
response
).
to
have_gitlab_http_status
(
422
)
end
it
'does not create a new epic'
do
expect
(
Epic
.
count
).
to
eq
(
0
)
end
end
end
context
'with unauthorized user'
do
it
'returns a not found 404 response'
do
group
.
add_guest
(
user
)
subject
expect
(
response
).
to
have_http_status
(
404
)
end
end
end
describe
"DELETE #destroy"
do
describe
"DELETE #destroy"
do
before
do
before
do
sign_in
(
user
)
sign_in
(
user
)
...
...
spec/ee/spec/features/epics/new_epic_spec.rb
0 → 100644
View file @
dc0cb770
require
'spec_helper'
feature
'New Epic'
,
:js
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:group
)
{
create
(
:group
,
:public
)
}
before
do
stub_licensed_features
(
epics:
true
)
sign_in
(
user
)
end
context
'empty epic list'
do
context
'when user who is not a group member views the epic list'
do
it
'does not show the create button'
do
visit
group_epics_path
(
group
)
expect
(
page
).
not_to
have_selector
(
'.new-epic-dropdown .btn-new'
)
end
end
context
'when user with owner views the epic list'
do
before
do
group
.
add_owner
(
user
)
visit
group_epics_path
(
group
)
end
it
'does show the create button'
do
expect
(
page
).
to
have_selector
(
'.new-epic-dropdown .btn-new'
)
end
end
end
context
'has epics in list'
do
let!
(
:epics
)
{
create_list
(
:epic
,
2
,
group:
group
)
}
context
'when user who is not a group member views the epic list'
do
before
do
visit
group_epics_path
(
group
)
end
it
'does not show the create button'
do
expect
(
page
).
not_to
have_selector
(
'.new-epic-dropdown .btn-new'
)
end
end
context
'when user with owner views the epic list'
do
before
do
group
.
add_owner
(
user
)
visit
group_epics_path
(
group
)
end
it
'does show the create button'
do
expect
(
page
).
to
have_selector
(
'.new-epic-dropdown .btn-new'
)
end
it
'can create epic'
do
find
(
'.new-epic-dropdown .btn-new'
).
click
find
(
'.new-epic-dropdown input'
).
set
(
'test epic title'
)
find
(
'.new-epic-dropdown .btn-save'
).
click
wait_for_requests
expect
(
find
(
'.issuable-details h2.title'
)).
to
have_content
(
'test epic title'
)
end
end
end
end
spec/ee/spec/services/epics/create_service_spec.rb
0 → 100644
View file @
dc0cb770
require
'spec_helper'
describe
Epics
::
CreateService
do
let
(
:group
)
{
create
(
:group
,
:internal
)}
let
(
:user
)
{
create
(
:user
)
}
let
(
:params
)
{
{
title:
'new epic'
,
description:
'epic description'
}
}
subject
{
described_class
.
new
(
group
,
user
,
params
).
execute
}
describe
'#execute'
do
it
'creates one issue correctly'
do
expect
{
subject
}.
to
change
{
Epic
.
count
}.
from
(
0
).
to
(
1
)
epic
=
Epic
.
last
expect
(
epic
).
to
be_persisted
expect
(
epic
.
title
).
to
eq
(
'new epic'
)
expect
(
epic
.
description
).
to
eq
(
'epic description'
)
end
end
end
spec/javascripts/epics/new_epic/components/new_epic_spec.js
0 → 100644
View file @
dc0cb770
import
Vue
from
'
vue
'
;
import
newEpic
from
'
ee/epics/new_epic/components/new_epic.vue
'
;
import
*
as
urlUtility
from
'
~/lib/utils/url_utility
'
;
import
mountComponent
from
'
../../../helpers/vue_mount_component_helper
'
;
describe
(
'
newEpic
'
,
()
=>
{
let
vm
;
const
interceptor
=
(
request
,
next
)
=>
{
next
(
request
.
respondWith
(
JSON
.
stringify
({
web_url
:
gl
.
TEST_HOST
,
}),
{
status
:
200
,
}));
};
beforeEach
(()
=>
{
Vue
.
http
.
interceptors
.
push
(
interceptor
);
const
NewEpic
=
Vue
.
extend
(
newEpic
);
vm
=
mountComponent
(
NewEpic
,
{
endpoint
:
gl
.
TEST_HOST
,
});
});
afterEach
(()
=>
{
Vue
.
http
.
interceptors
=
_
.
without
(
Vue
.
http
.
interceptors
,
interceptor
);
});
describe
(
'
alignRight
'
,
()
=>
{
it
(
'
should not add dropdown-menu-align-right by default
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.dropdown-menu
'
).
classList
.
contains
(
'
dropdown-menu-align-right
'
)).
toEqual
(
false
);
});
it
(
'
should add dropdown-menu-align-right when alignRight
'
,
(
done
)
=>
{
vm
.
alignRight
=
true
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.dropdown-menu
'
).
classList
.
contains
(
'
dropdown-menu-align-right
'
)).
toEqual
(
true
);
done
();
});
});
});
describe
(
'
creating epic
'
,
()
=>
{
it
(
'
should call createEpic service
'
,
(
done
)
=>
{
spyOn
(
urlUtility
,
'
visitUrl
'
).
and
.
callFake
(()
=>
{});
spyOn
(
vm
.
service
,
'
createEpic
'
).
and
.
callThrough
();
vm
.
title
=
'
test
'
;
Vue
.
nextTick
(()
=>
{
vm
.
$el
.
querySelector
(
'
.btn-save
'
).
click
();
expect
(
vm
.
service
.
createEpic
).
toHaveBeenCalled
();
done
();
});
});
it
(
'
should redirect to epic url after epic creation
'
,
(
done
)
=>
{
spyOn
(
urlUtility
,
'
visitUrl
'
).
and
.
callFake
((
url
)
=>
{
expect
(
url
).
toEqual
(
gl
.
TEST_HOST
);
done
();
});
vm
.
title
=
'
test
'
;
Vue
.
nextTick
(()
=>
{
vm
.
$el
.
querySelector
(
'
.btn-save
'
).
click
();
});
});
it
(
'
should toggle loading button while creating
'
,
(
done
)
=>
{
spyOn
(
urlUtility
,
'
visitUrl
'
).
and
.
callFake
(()
=>
{});
vm
.
title
=
'
test
'
;
Vue
.
nextTick
(()
=>
{
const
btnSave
=
vm
.
$el
.
querySelector
(
'
.btn-save
'
);
const
loadingIcon
=
btnSave
.
querySelector
(
'
.js-loading-button-icon
'
);
expect
(
loadingIcon
).
toBeNull
();
btnSave
.
click
();
expect
(
loadingIcon
).
toBeDefined
();
done
();
});
});
});
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment