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
d186d8c7
Commit
d186d8c7
authored
Nov 27, 2018
by
Phil Hughes
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
EE port of issuable-suggestions
parent
9f05c6ac
Changes
40
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
40 changed files
with
1186 additions
and
35 deletions
+1186
-35
app/assets/javascripts/issuable_suggestions/components/app.vue
...ssets/javascripts/issuable_suggestions/components/app.vue
+96
-0
app/assets/javascripts/issuable_suggestions/components/item.vue
...sets/javascripts/issuable_suggestions/components/item.vue
+137
-0
app/assets/javascripts/issuable_suggestions/index.js
app/assets/javascripts/issuable_suggestions/index.js
+38
-0
app/assets/javascripts/issuable_suggestions/queries/issues.graphql
...s/javascripts/issuable_suggestions/queries/issues.graphql
+26
-0
app/assets/javascripts/lib/graphql.js
app/assets/javascripts/lib/graphql.js
+9
-0
app/assets/javascripts/pages/projects/issues/form.js
app/assets/javascripts/pages/projects/issues/form.js
+6
-0
app/assets/stylesheets/pages/issuable.scss
app/assets/stylesheets/pages/issuable.scss
+34
-0
app/controllers/projects/issues_controller.rb
app/controllers/projects/issues_controller.rb
+7
-0
app/graphql/resolvers/issues_resolver.rb
app/graphql/resolvers/issues_resolver.rb
+25
-0
app/graphql/types/issue_type.rb
app/graphql/types/issue_type.rb
+47
-0
app/graphql/types/label_type.rb
app/graphql/types/label_type.rb
+12
-0
app/graphql/types/milestone_type.rb
app/graphql/types/milestone_type.rb
+17
-0
app/graphql/types/order.rb
app/graphql/types/order.rb
+8
-0
app/graphql/types/permission_types/issue.rb
app/graphql/types/permission_types/issue.rb
+14
-0
app/graphql/types/project_type.rb
app/graphql/types/project_type.rb
+5
-0
app/graphql/types/sort.rb
app/graphql/types/sort.rb
+10
-0
app/graphql/types/user_type.rb
app/graphql/types/user_type.rb
+14
-0
app/policies/milestone_policy.rb
app/policies/milestone_policy.rb
+5
-0
app/presenters/issue_presenter.rb
app/presenters/issue_presenter.rb
+9
-0
app/presenters/user_presenter.rb
app/presenters/user_presenter.rb
+9
-0
app/views/shared/issuable/_form.html.haml
app/views/shared/issuable/_form.html.haml
+2
-0
config/webpack.config.js
config/webpack.config.js
+11
-1
ee/spec/javascripts/operations/components/dashboard/alerts_spec.js
...avascripts/operations/components/dashboard/alerts_spec.js
+1
-9
ee/spec/javascripts/operations/components/dashboard/project_search_spec.js
...ts/operations/components/dashboard/project_search_spec.js
+1
-8
ee/spec/javascripts/operations/components/dashboard/project_spec.js
...vascripts/operations/components/dashboard/project_spec.js
+1
-8
ee/spec/javascripts/operations/components/tokenized_input/input_spec.js
...ripts/operations/components/tokenized_input/input_spec.js
+3
-9
lib/gitlab/graphql/loaders/batch_model_loader.rb
lib/gitlab/graphql/loaders/batch_model_loader.rb
+29
-0
locale/gitlab.pot
locale/gitlab.pot
+9
-0
package.json
package.json
+5
-0
spec/features/issues_spec.rb
spec/features/issues_spec.rb
+12
-0
spec/graphql/resolvers/issues_resolver_spec.rb
spec/graphql/resolvers/issues_resolver_spec.rb
+40
-0
spec/graphql/types/issue_type_spec.rb
spec/graphql/types/issue_type_spec.rb
+7
-0
spec/graphql/types/permission_types/issue_spec.rb
spec/graphql/types/permission_types/issue_spec.rb
+12
-0
spec/graphql/types/project_type_spec.rb
spec/graphql/types/project_type_spec.rb
+4
-0
spec/javascripts/issuable_suggestions/components/app_spec.js
spec/javascripts/issuable_suggestions/components/app_spec.js
+96
-0
spec/javascripts/issuable_suggestions/components/item_spec.js
.../javascripts/issuable_suggestions/components/item_spec.js
+139
-0
spec/javascripts/issuable_suggestions/mock_data.js
spec/javascripts/issuable_suggestions/mock_data.js
+26
-0
spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb
spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb
+28
-0
spec/requests/api/graphql/project/issues_spec.rb
spec/requests/api/graphql/project/issues_spec.rb
+59
-0
yarn.lock
yarn.lock
+173
-0
No files found.
app/assets/javascripts/issuable_suggestions/components/app.vue
0 → 100644
View file @
d186d8c7
<
script
>
import
_
from
'
underscore
'
;
import
{
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
{
__
}
from
'
~/locale
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
Suggestion
from
'
./item.vue
'
;
import
query
from
'
../queries/issues.graphql
'
;
export
default
{
components
:
{
Suggestion
,
Icon
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
props
:
{
projectPath
:
{
type
:
String
,
required
:
true
,
},
search
:
{
type
:
String
,
required
:
true
,
},
},
apollo
:
{
issues
:
{
query
,
debounce
:
250
,
skip
()
{
return
this
.
isSearchEmpty
;
},
update
:
data
=>
data
.
project
.
issues
.
edges
.
map
(({
node
})
=>
node
),
variables
()
{
return
{
fullPath
:
this
.
projectPath
,
search
:
this
.
search
,
};
},
},
},
data
()
{
return
{
issues
:
[],
loading
:
0
,
};
},
computed
:
{
isSearchEmpty
()
{
return
_
.
isEmpty
(
this
.
search
);
},
showSuggestions
()
{
return
!
this
.
isSearchEmpty
&&
this
.
issues
.
length
&&
!
this
.
loading
;
},
},
watch
:
{
search
()
{
if
(
this
.
isSearchEmpty
)
{
this
.
issues
=
[];
}
},
},
helpText
:
__
(
'
These existing issues have a similar title. It might be better to comment there instead of creating another similar issue.
'
,
),
};
</
script
>
<
template
>
<div
v-show=
"showSuggestions"
class=
"form-group row issuable-suggestions"
>
<div
v-once
class=
"col-form-label col-sm-2 pt-0"
>
{{
__
(
'
Similar issues
'
)
}}
<icon
v-gl-tooltip
.
bottom
:title=
"$options.helpText"
:aria-label=
"$options.helpText"
name=
"question-o"
class=
"text-secondary suggestion-help-hover"
/>
</div>
<div
class=
"col-sm-10"
>
<ul
class=
"list-unstyled m-0"
>
<li
v-for=
"(suggestion, index) in issues"
:key=
"suggestion.id"
:class=
"
{
'append-bottom-default': index !== issues.length - 1,
}"
>
<suggestion
:suggestion=
"suggestion"
/>
</li>
</ul>
</div>
</div>
</
template
>
app/assets/javascripts/issuable_suggestions/components/item.vue
0 → 100644
View file @
d186d8c7
<
script
>
import
_
from
'
underscore
'
;
import
{
GlLink
,
GlTooltip
,
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
{
__
}
from
'
~/locale
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
UserAvatarImage
from
'
~/vue_shared/components/user_avatar/user_avatar_image.vue
'
;
import
TimeagoTooltip
from
'
~/vue_shared/components/time_ago_tooltip.vue
'
;
import
timeago
from
'
~/vue_shared/mixins/timeago
'
;
export
default
{
components
:
{
GlTooltip
,
GlLink
,
Icon
,
UserAvatarImage
,
TimeagoTooltip
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
mixins
:
[
timeago
],
props
:
{
suggestion
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
isOpen
()
{
return
this
.
suggestion
.
state
===
'
opened
'
;
},
isClosed
()
{
return
this
.
suggestion
.
state
===
'
closed
'
;
},
counts
()
{
return
[
{
id
:
_
.
uniqueId
(),
icon
:
'
thumb-up
'
,
tooltipTitle
:
__
(
'
Upvotes
'
),
count
:
this
.
suggestion
.
upvotes
,
},
{
id
:
_
.
uniqueId
(),
icon
:
'
comment
'
,
tooltipTitle
:
__
(
'
Comments
'
),
count
:
this
.
suggestion
.
userNotesCount
,
},
].
filter
(({
count
})
=>
count
);
},
stateIcon
()
{
return
this
.
isClosed
?
'
issue-close
'
:
'
issue-open-m
'
;
},
stateTitle
()
{
return
this
.
isClosed
?
__
(
'
Closed
'
)
:
__
(
'
Opened
'
);
},
closedOrCreatedDate
()
{
return
this
.
suggestion
.
closedAt
||
this
.
suggestion
.
createdAt
;
},
hasUpdated
()
{
return
this
.
suggestion
.
updatedAt
!==
this
.
suggestion
.
createdAt
;
},
},
};
</
script
>
<
template
>
<div
class=
"suggestion-item"
>
<div
class=
"d-flex align-items-center"
>
<icon
v-if=
"suggestion.confidential"
v-gl-tooltip
.
bottom
:title=
"__('Confidential')"
name=
"eye-slash"
class=
"suggestion-help-hover mr-1 suggestion-confidential"
/>
<gl-link
:href=
"suggestion.webUrl"
target=
"_blank"
class=
"suggestion bold str-truncated-100"
>
{{
suggestion
.
title
}}
</gl-link>
</div>
<div
class=
"text-secondary suggestion-footer"
>
<icon
ref=
"state"
:name=
"stateIcon"
:class=
"
{
'suggestion-state-open': isOpen,
'suggestion-state-closed': isClosed,
}"
class="suggestion-help-hover"
/>
<gl-tooltip
:target=
"() => $refs.state"
placement=
"bottom"
>
<span
class=
"d-block"
>
<span
class=
"bold"
>
{{
stateTitle
}}
</span>
{{
timeFormated
(
closedOrCreatedDate
)
}}
</span>
<span
class=
"text-tertiary"
>
{{
tooltipTitle
(
closedOrCreatedDate
)
}}
</span>
</gl-tooltip>
#
{{
suggestion
.
iid
}}
•
<timeago-tooltip
:time=
"suggestion.createdAt"
tooltip-placement=
"bottom"
class=
"suggestion-help-hover"
/>
by
<gl-link
:href=
"suggestion.author.webUrl"
>
<user-avatar-image
:img-src=
"suggestion.author.avatarUrl"
:size=
"16"
css-classes=
"mr-0 float-none"
tooltip-placement=
"bottom"
class=
"d-inline-block"
>
<span
class=
"bold d-block"
>
{{
__
(
'
Author
'
)
}}
</span>
{{
suggestion
.
author
.
name
}}
<span
class=
"text-tertiary"
>
@
{{
suggestion
.
author
.
username
}}
</span>
</user-avatar-image>
</gl-link>
<template
v-if=
"hasUpdated"
>
•
{{
__
(
'
updated
'
)
}}
<timeago-tooltip
:time=
"suggestion.updatedAt"
tooltip-placement=
"bottom"
class=
"suggestion-help-hover"
/>
</
template
>
<span
class=
"suggestion-counts"
>
<span
v-for=
"{ count, icon, tooltipTitle, id } in counts"
:key=
"id"
v-gl-tooltip
.
bottom
:title=
"tooltipTitle"
class=
"suggestion-help-hover prepend-left-8 text-tertiary"
>
<icon
:name=
"icon"
/>
{{ count }}
</span>
</span>
</div>
</div>
</template>
app/assets/javascripts/issuable_suggestions/index.js
0 → 100644
View file @
d186d8c7
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
defaultClient
from
'
~/lib/graphql
'
;
import
App
from
'
./components/app.vue
'
;
Vue
.
use
(
VueApollo
);
export
default
function
()
{
const
el
=
document
.
getElementById
(
'
js-suggestions
'
);
const
issueTitle
=
document
.
getElementById
(
'
issue_title
'
);
const
{
projectPath
}
=
el
.
dataset
;
const
apolloProvider
=
new
VueApollo
({
defaultClient
,
});
return
new
Vue
({
el
,
apolloProvider
,
data
()
{
return
{
search
:
issueTitle
.
value
,
};
},
mounted
()
{
issueTitle
.
addEventListener
(
'
input
'
,
()
=>
{
this
.
search
=
issueTitle
.
value
;
});
},
render
(
h
)
{
return
h
(
App
,
{
props
:
{
projectPath
,
search
:
this
.
search
,
},
});
},
});
}
app/assets/javascripts/issuable_suggestions/queries/issues.graphql
0 → 100644
View file @
d186d8c7
query
issueSuggestion
(
$fullPath
:
ID
!,
$search
:
String
)
{
project
(
fullPath
:
$fullPath
)
{
issues
(
search
:
$search
,
sort
:
updated_desc
,
first
:
5
)
{
edges
{
node
{
iid
title
confidential
userNotesCount
upvotes
webUrl
state
closedAt
createdAt
updatedAt
author
{
name
username
avatarUrl
webUrl
}
}
}
}
}
}
app/assets/javascripts/lib/graphql.js
0 → 100644
View file @
d186d8c7
import
ApolloClient
from
'
apollo-boost
'
;
import
csrf
from
'
~/lib/utils/csrf
'
;
export
default
new
ApolloClient
({
uri
:
`
${
gon
.
relative_url_root
}
/api/graphql`
,
headers
:
{
[
csrf
.
headerKey
]:
csrf
.
token
,
},
});
app/assets/javascripts/pages/projects/issues/form.js
View file @
d186d8c7
...
@@ -7,6 +7,7 @@ import LabelsSelect from '~/labels_select';
...
@@ -7,6 +7,7 @@ import LabelsSelect from '~/labels_select';
import
MilestoneSelect
from
'
~/milestone_select
'
;
import
MilestoneSelect
from
'
~/milestone_select
'
;
import
ShortcutsNavigation
from
'
~/behaviors/shortcuts/shortcuts_navigation
'
;
import
ShortcutsNavigation
from
'
~/behaviors/shortcuts/shortcuts_navigation
'
;
import
IssuableTemplateSelectors
from
'
~/templates/issuable_template_selectors
'
;
import
IssuableTemplateSelectors
from
'
~/templates/issuable_template_selectors
'
;
import
initSuggestions
from
'
~/issuable_suggestions
'
;
import
WeightSelect
from
'
ee/weight_select
'
;
import
WeightSelect
from
'
ee/weight_select
'
;
export
default
()
=>
{
export
default
()
=>
{
...
@@ -16,5 +17,10 @@ export default () => {
...
@@ -16,5 +17,10 @@ export default () => {
new
LabelsSelect
();
new
LabelsSelect
();
new
MilestoneSelect
();
new
MilestoneSelect
();
new
IssuableTemplateSelectors
();
new
IssuableTemplateSelectors
();
if
(
gon
.
features
.
issueSuggestions
&&
gon
.
features
.
graphql
)
{
initSuggestions
();
}
new
WeightSelect
();
new
WeightSelect
();
};
};
app/assets/stylesheets/pages/issuable.scss
View file @
d186d8c7
...
@@ -993,3 +993,37 @@
...
@@ -993,3 +993,37 @@
}
}
}
}
}
}
.issuable-suggestions
svg
{
vertical-align
:
sub
;
}
.suggestion-item
a
{
color
:
initial
;
}
.suggestion-confidential
{
color
:
$orange-600
;
}
.suggestion-state-open
{
color
:
$green-500
;
}
.suggestion-state-closed
{
color
:
$blue-500
;
}
.suggestion-help-hover
{
cursor
:
help
;
}
.suggestion-footer
{
font-size
:
12px
;
line-height
:
15px
;
.avatar
{
margin-top
:
-3px
;
border
:
0
;
}
}
app/controllers/projects/issues_controller.rb
View file @
d186d8c7
...
@@ -40,6 +40,8 @@ class Projects::IssuesController < Projects::ApplicationController
...
@@ -40,6 +40,8 @@ class Projects::IssuesController < Projects::ApplicationController
# Allow create a new branch and empty WIP merge request from current issue
# Allow create a new branch and empty WIP merge request from current issue
before_action
:authorize_create_merge_request_from!
,
only:
[
:create_merge_request
]
before_action
:authorize_create_merge_request_from!
,
only:
[
:create_merge_request
]
before_action
:set_suggested_issues_feature_flags
,
only:
[
:new
]
respond_to
:html
respond_to
:html
def
index
def
index
...
@@ -265,4 +267,9 @@ class Projects::IssuesController < Projects::ApplicationController
...
@@ -265,4 +267,9 @@ class Projects::IssuesController < Projects::ApplicationController
# 3. https://gitlab.com/gitlab-org/gitlab-ce/issues/42426
# 3. https://gitlab.com/gitlab-org/gitlab-ce/issues/42426
Gitlab
::
QueryLimiting
.
whitelist
(
'https://gitlab.com/gitlab-org/gitlab-ce/issues/42422'
)
Gitlab
::
QueryLimiting
.
whitelist
(
'https://gitlab.com/gitlab-org/gitlab-ce/issues/42422'
)
end
end
def
set_suggested_issues_feature_flags
push_frontend_feature_flag
(
:graphql
)
push_frontend_feature_flag
(
:issue_suggestions
)
end
end
end
app/graphql/resolvers/issues_resolver.rb
0 → 100644
View file @
d186d8c7
# frozen_string_literal: true
module
Resolvers
class
IssuesResolver
<
BaseResolver
extend
ActiveSupport
::
Concern
argument
:search
,
GraphQL
::
STRING_TYPE
,
required:
false
argument
:sort
,
Types
::
Sort
,
required:
false
,
default_value:
'created_desc'
type
Types
::
IssueType
,
null:
true
alias_method
:project
,
:object
def
resolve
(
**
args
)
# Will need to be be made group & namespace aware with
# https://gitlab.com/gitlab-org/gitlab-ce/issues/54520
args
[
:project_id
]
=
project
.
id
IssuesFinder
.
new
(
context
[
:current_user
],
args
).
execute
end
end
end
app/graphql/types/issue_type.rb
0 → 100644
View file @
d186d8c7
# frozen_string_literal: true
module
Types
class
IssueType
<
BaseObject
expose_permissions
Types
::
PermissionTypes
::
Issue
graphql_name
'Issue'
present_using
IssuePresenter
field
:iid
,
GraphQL
::
ID_TYPE
,
null:
false
field
:title
,
GraphQL
::
STRING_TYPE
,
null:
false
field
:description
,
GraphQL
::
STRING_TYPE
,
null:
true
field
:state
,
GraphQL
::
STRING_TYPE
,
null:
false
field
:author
,
Types
::
UserType
,
null:
false
,
resolve:
->
(
obj
,
_args
,
_ctx
)
{
Gitlab
::
Graphql
::
Loaders
::
BatchModelLoader
.
new
(
User
,
obj
.
author_id
).
find
}
do
authorize
:read_user
end
field
:assignees
,
Types
::
UserType
.
connection_type
,
null:
true
field
:labels
,
Types
::
LabelType
.
connection_type
,
null:
true
field
:milestone
,
Types
::
MilestoneType
,
null:
true
,
resolve:
->
(
obj
,
_args
,
_ctx
)
{
Gitlab
::
Graphql
::
Loaders
::
BatchModelLoader
.
new
(
Milestone
,
obj
.
milestone_id
).
find
}
do
authorize
:read_milestone
end
field
:due_date
,
Types
::
TimeType
,
null:
true
field
:confidential
,
GraphQL
::
BOOLEAN_TYPE
,
null:
false
field
:discussion_locked
,
GraphQL
::
BOOLEAN_TYPE
,
null:
false
,
resolve:
->
(
obj
,
_args
,
_ctx
)
{
!!
obj
.
discussion_locked
}
field
:upvotes
,
GraphQL
::
INT_TYPE
,
null:
false
field
:downvotes
,
GraphQL
::
INT_TYPE
,
null:
false
field
:user_notes_count
,
GraphQL
::
INT_TYPE
,
null:
false
field
:web_url
,
GraphQL
::
STRING_TYPE
,
null:
false
field
:closed_at
,
Types
::
TimeType
,
null:
true
field
:created_at
,
Types
::
TimeType
,
null:
false
field
:updated_at
,
Types
::
TimeType
,
null:
false
end
end
app/graphql/types/label_type.rb
0 → 100644
View file @
d186d8c7
# frozen_string_literal: true
module
Types
class
LabelType
<
BaseObject
graphql_name
'Label'
field
:description
,
GraphQL
::
STRING_TYPE
,
null:
true
field
:title
,
GraphQL
::
STRING_TYPE
,
null:
false
field
:color
,
GraphQL
::
STRING_TYPE
,
null:
false
field
:text_color
,
GraphQL
::
STRING_TYPE
,
null:
false
end
end
app/graphql/types/milestone_type.rb
0 → 100644
View file @
d186d8c7
# frozen_string_literal: true
module
Types
class
MilestoneType
<
BaseObject
graphql_name
'Milestone'
field
:description
,
GraphQL
::
STRING_TYPE
,
null:
true
field
:title
,
GraphQL
::
STRING_TYPE
,
null:
false
field
:state
,
GraphQL
::
STRING_TYPE
,
null:
false
field
:due_date
,
Types
::
TimeType
,
null:
true
field
:start_date
,
Types
::
TimeType
,
null:
true
field
:created_at
,
Types
::
TimeType
,
null:
false
field
:updated_at
,
Types
::
TimeType
,
null:
false
end
end
app/graphql/types/order.rb
0 → 100644
View file @
d186d8c7
# frozen_string_literal: true
module
Types
class
Types::Order
<
Types
::
BaseEnum
value
"id"
,
"Created at date"
value
"updated_at"
,
"Updated at date"
end
end
app/graphql/types/permission_types/issue.rb
0 → 100644
View file @
d186d8c7
# frozen_string_literal: true
module
Types
module
PermissionTypes
class
Issue
<
BasePermissionType
description
'Check permissions for the current user on a issue'
graphql_name
'IssuePermissions'
abilities
:read_issue
,
:admin_issue
,
:update_issue
,
:create_note
,
:reopen_issue
end
end
end
app/graphql/types/project_type.rb
View file @
d186d8c7
...
@@ -73,6 +73,11 @@ module Types
...
@@ -73,6 +73,11 @@ module Types
authorize
:read_merge_request
authorize
:read_merge_request
end
end
field
:issues
,
Types
::
IssueType
.
connection_type
,
null:
true
,
resolver:
Resolvers
::
IssuesResolver
field
:pipelines
,
field
:pipelines
,
Types
::
Ci
::
PipelineType
.
connection_type
,
Types
::
Ci
::
PipelineType
.
connection_type
,
null:
false
,
null:
false
,
...
...
app/graphql/types/sort.rb
0 → 100644
View file @
d186d8c7
# frozen_string_literal: true
module
Types
class
Types::Sort
<
Types
::
BaseEnum
value
"updated_desc"
,
"Updated at descending order"
value
"updated_asc"
,
"Updated at ascending order"
value
"created_desc"
,
"Created at descending order"
value
"created_asc"
,
"Created at ascending order"
end
end
app/graphql/types/user_type.rb
0 → 100644
View file @
d186d8c7
# frozen_string_literal: true
module
Types
class
UserType
<
BaseObject
graphql_name
'User'
present_using
UserPresenter
field
:name
,
GraphQL
::
STRING_TYPE
,
null:
false
field
:username
,
GraphQL
::
STRING_TYPE
,
null:
false
field
:avatar_url
,
GraphQL
::
STRING_TYPE
,
null:
false
field
:web_url
,
GraphQL
::
STRING_TYPE
,
null:
false
end
end
app/policies/milestone_policy.rb
0 → 100644
View file @
d186d8c7
# frozen_string_literal: true
class
MilestonePolicy
<
BasePolicy
delegate
{
@subject
.
project
}
end
app/presenters/issue_presenter.rb
0 → 100644
View file @
d186d8c7
# frozen_string_literal: true
class
IssuePresenter
<
Gitlab
::
View
::
Presenter
::
Delegated
presents
:issue
def
web_url
Gitlab
::
UrlBuilder
.
build
(
issue
)
end
end
app/presenters/user_presenter.rb
0 → 100644
View file @
d186d8c7
# frozen_string_literal: true
class
UserPresenter
<
Gitlab
::
View
::
Presenter
::
Delegated
presents
:user
def
web_url
Gitlab
::
Routing
.
url_helpers
.
user_url
(
user
)
end
end
app/views/shared/issuable/_form.html.haml
View file @
d186d8c7
...
@@ -17,6 +17,8 @@
...
@@ -17,6 +17,8 @@
=
render
'shared/issuable/form/template_selector'
,
issuable:
issuable
=
render
'shared/issuable/form/template_selector'
,
issuable:
issuable
=
render
'shared/issuable/form/title'
,
issuable:
issuable
,
form:
form
,
has_wip_commits:
commits
&&
commits
.
detect
(
&
:work_in_progress?
)
=
render
'shared/issuable/form/title'
,
issuable:
issuable
,
form:
form
,
has_wip_commits:
commits
&&
commits
.
detect
(
&
:work_in_progress?
)
-
if
Feature
.
enabled?
(
:issue_suggestions
)
&&
Feature
.
enabled?
(
:graphql
)
#js-suggestions
{
data:
{
project_path:
@project
.
full_path
}
}
=
render
'shared/form_elements/description'
,
model:
issuable
,
form:
form
,
project:
project
=
render
'shared/form_elements/description'
,
model:
issuable
,
form:
form
,
project:
project
...
...
config/webpack.config.js
View file @
d186d8c7
...
@@ -91,7 +91,7 @@ module.exports = {
...
@@ -91,7 +91,7 @@ module.exports = {
},
},
resolve
:
{
resolve
:
{
extensions
:
[
'
.js
'
],
extensions
:
[
'
.js
'
,
'
.gql
'
,
'
.graphql
'
],
alias
:
{
alias
:
{
'
~
'
:
path
.
join
(
ROOT_PATH
,
'
app/assets/javascripts
'
),
'
~
'
:
path
.
join
(
ROOT_PATH
,
'
app/assets/javascripts
'
),
emojis
:
path
.
join
(
ROOT_PATH
,
'
fixtures/emojis
'
),
emojis
:
path
.
join
(
ROOT_PATH
,
'
fixtures/emojis
'
),
...
@@ -114,6 +114,11 @@ module.exports = {
...
@@ -114,6 +114,11 @@ module.exports = {
module
:
{
module
:
{
strictExportPresence
:
true
,
strictExportPresence
:
true
,
rules
:
[
rules
:
[
{
type
:
'
javascript/auto
'
,
test
:
/
\.
mjs$/
,
use
:
[],
},
{
{
test
:
/
\.
js$/
,
test
:
/
\.
js$/
,
exclude
:
path
=>
/node_modules|vendor
[\\/]
assets/
.
test
(
path
)
&&
!
/
\.
vue
\.
js/
.
test
(
path
),
exclude
:
path
=>
/node_modules|vendor
[\\/]
assets/
.
test
(
path
)
&&
!
/
\.
vue
\.
js/
.
test
(
path
),
...
@@ -135,6 +140,11 @@ module.exports = {
...
@@ -135,6 +140,11 @@ module.exports = {
].
join
(
'
|
'
),
].
join
(
'
|
'
),
},
},
},
},
{
test
:
/
\.(
graphql|gql
)
$/
,
exclude
:
/node_modules/
,
loader
:
'
graphql-tag/loader
'
,
},
{
{
test
:
/
\.
svg$/
,
test
:
/
\.
svg$/
,
loader
:
'
raw-loader
'
,
loader
:
'
raw-loader
'
,
...
...
ee/spec/javascripts/operations/components/dashboard/alerts_spec.js
View file @
d186d8c7
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
Alerts
from
'
ee/operations/components/dashboard/alerts.vue
'
;
import
Alerts
from
'
ee/operations/components/dashboard/alerts.vue
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
{
removeWhitespace
}
from
'
spec/helpers/vue_component_helper
'
;
import
{
removeWhitespace
}
from
'
spec/helpers/vue_component_helper
'
;
import
{
getChildInstances
}
from
'
../../helpers
'
;
import
{
mockOneProject
}
from
'
../../mock_data
'
;
import
{
mockOneProject
}
from
'
../../mock_data
'
;
describe
(
'
alerts component
'
,
()
=>
{
describe
(
'
alerts component
'
,
()
=>
{
const
AlertsComponent
=
Vue
.
extend
(
Alerts
);
const
AlertsComponent
=
Vue
.
extend
(
Alerts
);
const
IconComponent
=
Vue
.
extend
(
Icon
);
const
mockPath
=
'
https://mock-alert_path/
'
;
const
mockPath
=
'
https://mock-alert_path/
'
;
const
mount
=
(
props
=
{})
=>
mountComponentWithStore
(
AlertsComponent
,
{
props
});
const
mount
=
(
props
=
{})
=>
mountComponentWithStore
(
AlertsComponent
,
{
props
});
let
vm
;
let
vm
;
...
@@ -65,12 +62,7 @@ describe('alerts component', () => {
...
@@ -65,12 +62,7 @@ describe('alerts component', () => {
describe
(
'
wrapped components
'
,
()
=>
{
describe
(
'
wrapped components
'
,
()
=>
{
describe
(
'
icon
'
,
()
=>
{
describe
(
'
icon
'
,
()
=>
{
it
(
'
renders warning
'
,
()
=>
{
it
(
'
renders warning
'
,
()
=>
{
const
icons
=
getChildInstances
(
vm
,
IconComponent
);
expect
(
vm
.
$el
.
querySelector
(
'
.ic-warning
'
)).
not
.
toBe
(
null
);
expect
(
icons
.
length
).
toBe
(
1
);
const
[
icon
]
=
icons
;
expect
(
icon
.
name
).
toBe
(
'
warning
'
);
});
});
});
});
});
});
...
...
ee/spec/javascripts/operations/components/dashboard/project_search_spec.js
View file @
d186d8c7
...
@@ -2,7 +2,6 @@ import Vue from 'vue';
...
@@ -2,7 +2,6 @@ import Vue from 'vue';
import
store
from
'
ee/operations/store/index
'
;
import
store
from
'
ee/operations/store/index
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
ProjectAvatar
from
'
~/vue_shared/components/project_avatar/default.vue
'
;
import
ProjectAvatar
from
'
~/vue_shared/components/project_avatar/default.vue
'
;
import
ProjectSearch
from
'
ee/operations/components/dashboard/project_search.vue
'
;
import
ProjectSearch
from
'
ee/operations/components/dashboard/project_search.vue
'
;
import
TokenizedInput
from
'
ee/operations/components/tokenized_input/input.vue
'
;
import
TokenizedInput
from
'
ee/operations/components/tokenized_input/input.vue
'
;
...
@@ -11,7 +10,6 @@ import { getChildInstances, mouseEvent, clearState } from '../../helpers';
...
@@ -11,7 +10,6 @@ import { getChildInstances, mouseEvent, clearState } from '../../helpers';
describe
(
'
project search component
'
,
()
=>
{
describe
(
'
project search component
'
,
()
=>
{
const
ProjectSearchComponent
=
Vue
.
extend
(
ProjectSearch
);
const
ProjectSearchComponent
=
Vue
.
extend
(
ProjectSearch
);
const
IconComponent
=
Vue
.
extend
(
Icon
);
const
GlLoadingIconComponent
=
Vue
.
extend
(
GlLoadingIcon
);
const
GlLoadingIconComponent
=
Vue
.
extend
(
GlLoadingIcon
);
const
TokenizedInputComponent
=
Vue
.
extend
(
TokenizedInput
);
const
TokenizedInputComponent
=
Vue
.
extend
(
TokenizedInput
);
const
ProjectAvatarComponent
=
Vue
.
extend
(
ProjectAvatar
);
const
ProjectAvatarComponent
=
Vue
.
extend
(
ProjectAvatar
);
...
@@ -56,12 +54,7 @@ describe('project search component', () => {
...
@@ -56,12 +54,7 @@ describe('project search component', () => {
});
});
it
(
'
renders search icon
'
,
()
=>
{
it
(
'
renders search icon
'
,
()
=>
{
const
icons
=
getChildInstances
(
vm
,
IconComponent
);
expect
(
vm
.
$el
.
querySelector
(
'
.ic-search
'
)).
not
.
toBe
(
null
);
expect
(
icons
.
length
).
toBe
(
1
);
const
[
searchIcon
]
=
icons
;
expect
(
searchIcon
.
name
).
toBe
(
'
search
'
);
});
});
it
(
'
renders search description
'
,
()
=>
{
it
(
'
renders search description
'
,
()
=>
{
...
...
ee/spec/javascripts/operations/components/dashboard/project_spec.js
View file @
d186d8c7
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
Commit
from
'
~/vue_shared/components/commit.vue
'
;
import
Commit
from
'
~/vue_shared/components/commit.vue
'
;
import
Project
from
'
ee/operations/components/dashboard/project.vue
'
;
import
Project
from
'
ee/operations/components/dashboard/project.vue
'
;
import
ProjectHeader
from
'
ee/operations/components/dashboard/project_header.vue
'
;
import
ProjectHeader
from
'
ee/operations/components/dashboard/project_header.vue
'
;
...
@@ -13,7 +12,6 @@ describe('project component', () => {
...
@@ -13,7 +12,6 @@ describe('project component', () => {
const
ProjectHeaderComponent
=
Vue
.
extend
(
ProjectHeader
);
const
ProjectHeaderComponent
=
Vue
.
extend
(
ProjectHeader
);
const
AlertsComponent
=
Vue
.
extend
(
Alerts
);
const
AlertsComponent
=
Vue
.
extend
(
Alerts
);
const
CommitComponent
=
Vue
.
extend
(
Commit
);
const
CommitComponent
=
Vue
.
extend
(
Commit
);
const
IconComponent
=
Vue
.
extend
(
Icon
);
let
vm
;
let
vm
;
beforeEach
(()
=>
{
beforeEach
(()
=>
{
...
@@ -93,12 +91,7 @@ describe('project component', () => {
...
@@ -93,12 +91,7 @@ describe('project component', () => {
describe
(
'
last deploy
'
,
()
=>
{
describe
(
'
last deploy
'
,
()
=>
{
it
(
'
renders calendar icon
'
,
()
=>
{
it
(
'
renders calendar icon
'
,
()
=>
{
const
icons
=
getChildInstances
(
vm
,
IconComponent
);
expect
(
vm
.
$el
.
querySelector
(
'
.ic-calendar
'
)).
not
.
toBe
(
null
);
expect
(
icons
.
length
).
toBe
(
1
);
const
[
icon
]
=
icons
;
expect
(
icon
.
name
).
toBe
(
'
calendar
'
);
});
});
it
(
'
renders time ago of last deploy
'
,
()
=>
{
it
(
'
renders time ago of last deploy
'
,
()
=>
{
...
...
ee/spec/javascripts/operations/components/tokenized_input/input_spec.js
View file @
d186d8c7
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
store
from
'
ee/operations/store/index
'
;
import
store
from
'
ee/operations/store/index
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
mountComponentWithStore
}
from
'
spec/helpers/vue_mount_component_helper
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
TokenizedInput
from
'
ee/operations/components/tokenized_input/input.vue
'
;
import
TokenizedInput
from
'
ee/operations/components/tokenized_input/input.vue
'
;
import
{
getChildInstances
,
clearState
}
from
'
../../helpers
'
;
import
{
clearState
}
from
'
../../helpers
'
;
import
{
mockProjectData
}
from
'
../../mock_data
'
;
import
{
mockProjectData
}
from
'
../../mock_data
'
;
describe
(
'
tokenized input component
'
,
()
=>
{
describe
(
'
tokenized input component
'
,
()
=>
{
const
TokenizedInputComponent
=
Vue
.
extend
(
TokenizedInput
);
const
TokenizedInputComponent
=
Vue
.
extend
(
TokenizedInput
);
const
IconComponent
=
Vue
.
extend
(
Icon
);
const
mockProjects
=
mockProjectData
(
1
);
const
mockProjects
=
mockProjectData
(
1
);
const
[
mockOneProject
]
=
mockProjects
;
const
[
mockOneProject
]
=
mockProjects
;
const
mockInputValue
=
'
mock-inputValue
'
;
const
mockInputValue
=
'
mock-inputValue
'
;
...
@@ -74,15 +72,11 @@ describe('tokenized input component', () => {
...
@@ -74,15 +72,11 @@ describe('tokenized input component', () => {
describe
(
'
wrapped components
'
,
()
=>
{
describe
(
'
wrapped components
'
,
()
=>
{
describe
(
'
icon
'
,
()
=>
{
describe
(
'
icon
'
,
()
=>
{
it
(
'
should render close for input tokens
'
,
()
=>
{
it
(
'
should render close for input tokens
'
,
()
=>
{
expect
(
expect
(
vm
.
$el
.
querySelectorAll
(
'
.ic-close
'
).
length
).
toBe
(
mockProjects
.
length
);
getChildInstances
(
vm
,
IconComponent
).
filter
(
icon
=>
icon
.
name
===
'
close
'
).
length
,
).
toBe
(
mockProjects
.
length
);
});
});
it
(
'
should render search
'
,
()
=>
{
it
(
'
should render search
'
,
()
=>
{
const
search
=
getChildInstances
(
vm
,
IconComponent
)[
1
];
expect
(
vm
.
$el
.
querySelector
(
'
.ic-search
'
)).
not
.
toBe
(
null
);
expect
(
search
.
name
).
toBe
(
'
search
'
);
});
});
});
});
});
});
...
...
lib/gitlab/graphql/loaders/batch_model_loader.rb
0 → 100644
View file @
d186d8c7
# frozen_string_literal: true
module
Gitlab
module
Graphql
module
Loaders
class
BatchModelLoader
attr_reader
:model_class
,
:model_id
def
initialize
(
model_class
,
model_id
)
@model_class
,
@model_id
=
model_class
,
model_id
end
# rubocop: disable CodeReuse/ActiveRecord
def
find
BatchLoader
.
for
({
model:
model_class
,
id:
model_id
}).
batch
do
|
loader_info
,
loader
|
per_model
=
loader_info
.
group_by
{
|
info
|
info
[
:model
]
}
per_model
.
each
do
|
model
,
info
|
ids
=
info
.
map
{
|
i
|
i
[
:id
]
}
results
=
model
.
where
(
id:
ids
)
results
.
each
{
|
record
|
loader
.
call
({
model:
model
,
id:
record
.
id
},
record
)
}
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
end
end
locale/gitlab.pot
View file @
d186d8c7
...
@@ -7642,6 +7642,9 @@ msgstr ""
...
@@ -7642,6 +7642,9 @@ msgstr ""
msgid "Sign-up restrictions"
msgid "Sign-up restrictions"
msgstr ""
msgstr ""
msgid "Similar issues"
msgstr ""
msgid "Size"
msgid "Size"
msgstr ""
msgstr ""
...
@@ -8280,6 +8283,9 @@ msgstr ""
...
@@ -8280,6 +8283,9 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgid "There was an error when unsubscribing from this label."
msgstr ""
msgstr ""
msgid "These existing issues have a similar title. It might be better to comment there instead of creating another similar issue."
msgstr ""
msgid "They can be managed using the %{link}."
msgid "They can be managed using the %{link}."
msgstr ""
msgstr ""
...
@@ -10229,6 +10235,9 @@ msgstr ""
...
@@ -10229,6 +10235,9 @@ msgstr ""
msgid "toggle collapse"
msgid "toggle collapse"
msgstr ""
msgstr ""
msgid "updated"
msgstr ""
msgid "username"
msgid "username"
msgstr ""
msgstr ""
...
...
package.json
View file @
d186d8c7
...
@@ -26,6 +26,8 @@
...
@@ -26,6 +26,8 @@
"
@babel/preset-env
"
:
"
^7.1.0
"
,
"
@babel/preset-env
"
:
"
^7.1.0
"
,
"
@gitlab/svgs
"
:
"
^1.38.0
"
,
"
@gitlab/svgs
"
:
"
^1.38.0
"
,
"
@gitlab/ui
"
:
"
^1.11.0
"
,
"
@gitlab/ui
"
:
"
^1.11.0
"
,
"
apollo-boost
"
:
"
^0.1.20
"
,
"
apollo-client
"
:
"
^2.4.5
"
,
"
autosize
"
:
"
^4.0.0
"
,
"
autosize
"
:
"
^4.0.0
"
,
"
axios
"
:
"
^0.17.1
"
,
"
axios
"
:
"
^0.17.1
"
,
"
babel-loader
"
:
"
^8.0.4
"
,
"
babel-loader
"
:
"
^8.0.4
"
,
...
@@ -62,6 +64,7 @@
...
@@ -62,6 +64,7 @@
"
formdata-polyfill
"
:
"
^3.0.11
"
,
"
formdata-polyfill
"
:
"
^3.0.11
"
,
"
fuzzaldrin-plus
"
:
"
^0.5.0
"
,
"
fuzzaldrin-plus
"
:
"
^0.5.0
"
,
"
glob
"
:
"
^7.1.2
"
,
"
glob
"
:
"
^7.1.2
"
,
"
graphql
"
:
"
^14.0.2
"
,
"
imports-loader
"
:
"
^0.8.0
"
,
"
imports-loader
"
:
"
^0.8.0
"
,
"
jed
"
:
"
^1.1.1
"
,
"
jed
"
:
"
^1.1.1
"
,
"
jquery
"
:
"
^3.2.1
"
,
"
jquery
"
:
"
^3.2.1
"
,
...
@@ -99,6 +102,7 @@
...
@@ -99,6 +102,7 @@
"
url-loader
"
:
"
^1.1.1
"
,
"
url-loader
"
:
"
^1.1.1
"
,
"
visibilityjs
"
:
"
^1.2.4
"
,
"
visibilityjs
"
:
"
^1.2.4
"
,
"
vue
"
:
"
^2.5.17
"
,
"
vue
"
:
"
^2.5.17
"
,
"
vue-apollo
"
:
"
^3.0.0-beta.25
"
,
"
vue-loader
"
:
"
^15.4.2
"
,
"
vue-loader
"
:
"
^15.4.2
"
,
"
vue-resource
"
:
"
^1.5.0
"
,
"
vue-resource
"
:
"
^1.5.0
"
,
"
vue-router
"
:
"
^3.0.1
"
,
"
vue-router
"
:
"
^3.0.1
"
,
...
@@ -129,6 +133,7 @@
...
@@ -129,6 +133,7 @@
"
eslint-plugin-jasmine
"
:
"
^2.10.1
"
,
"
eslint-plugin-jasmine
"
:
"
^2.10.1
"
,
"
gettext-extractor
"
:
"
^3.3.2
"
,
"
gettext-extractor
"
:
"
^3.3.2
"
,
"
gettext-extractor-vue
"
:
"
^4.0.1
"
,
"
gettext-extractor-vue
"
:
"
^4.0.1
"
,
"
graphql-tag
"
:
"
^2.10.0
"
,
"
istanbul
"
:
"
^0.4.5
"
,
"
istanbul
"
:
"
^0.4.5
"
,
"
jasmine-core
"
:
"
^2.9.0
"
,
"
jasmine-core
"
:
"
^2.9.0
"
,
"
jasmine-diff
"
:
"
^0.1.3
"
,
"
jasmine-diff
"
:
"
^0.1.3
"
,
...
...
spec/features/issues_spec.rb
View file @
d186d8c7
...
@@ -718,6 +718,18 @@ describe 'Issues' do
...
@@ -718,6 +718,18 @@ describe 'Issues' do
expect
(
find
(
'.js-issuable-selector .dropdown-toggle-text'
)).
to
have_content
(
'bug'
)
expect
(
find
(
'.js-issuable-selector .dropdown-toggle-text'
)).
to
have_content
(
'bug'
)
end
end
end
end
context
'suggestions'
,
:js
do
it
'displays list of related issues'
do
create
(
:issue
,
project:
project
,
title:
'test issue'
)
visit
new_project_issue_path
(
project
)
fill_in
'issue_title'
,
with:
issue
.
title
expect
(
page
).
to
have_selector
(
'.suggestion-item'
,
count:
1
)
end
end
end
end
describe
'new issue by email'
do
describe
'new issue by email'
do
...
...
spec/graphql/resolvers/issues_resolver_spec.rb
0 → 100644
View file @
d186d8c7
require
'spec_helper'
describe
Resolvers
::
IssuesResolver
do
include
GraphqlHelpers
let
(
:current_user
)
{
create
(
:user
)
}
set
(
:project
)
{
create
(
:project
)
}
set
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
set
(
:issue2
)
{
create
(
:issue
,
project:
project
,
title:
'foo'
)
}
before
do
project
.
add_developer
(
current_user
)
end
describe
'#resolve'
do
it
'finds all issues'
do
expect
(
resolve_issues
).
to
contain_exactly
(
issue
,
issue2
)
end
it
'searches issues'
do
expect
(
resolve_issues
(
search:
'foo'
)).
to
contain_exactly
(
issue2
)
end
it
'sort issues'
do
expect
(
resolve_issues
(
sort:
'created_desc'
)).
to
eq
[
issue2
,
issue
]
end
it
'returns issues user can see'
do
project
.
add_guest
(
current_user
)
create
(
:issue
,
confidential:
true
)
expect
(
resolve_issues
).
to
contain_exactly
(
issue
,
issue2
)
end
end
def
resolve_issues
(
args
=
{},
context
=
{
current_user:
current_user
})
resolve
(
described_class
,
obj:
project
,
args:
args
,
ctx:
context
)
end
end
spec/graphql/types/issue_type_spec.rb
0 → 100644
View file @
d186d8c7
require
'spec_helper'
describe
GitlabSchema
.
types
[
'Issue'
]
do
it
{
expect
(
described_class
).
to
expose_permissions_using
(
Types
::
PermissionTypes
::
Issue
)
}
it
{
expect
(
described_class
.
graphql_name
).
to
eq
(
'Issue'
)
}
end
spec/graphql/types/permission_types/issue_spec.rb
0 → 100644
View file @
d186d8c7
require
'spec_helper'
describe
Types
::
PermissionTypes
::
Issue
do
it
do
expected_permissions
=
[
:read_issue
,
:admin_issue
,
:update_issue
,
:create_note
,
:reopen_issue
]
expect
(
described_class
).
to
have_graphql_fields
(
expected_permissions
)
end
end
spec/graphql/types/project_type_spec.rb
View file @
d186d8c7
...
@@ -14,5 +14,9 @@ describe GitlabSchema.types['Project'] do
...
@@ -14,5 +14,9 @@ describe GitlabSchema.types['Project'] do
end
end
end
end
describe
'nested issues'
do
it
{
expect
(
described_class
).
to
have_graphql_field
(
:issues
)
}
end
it
{
is_expected
.
to
have_graphql_field
(
:pipelines
)
}
it
{
is_expected
.
to
have_graphql_field
(
:pipelines
)
}
end
end
spec/javascripts/issuable_suggestions/components/app_spec.js
0 → 100644
View file @
d186d8c7
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
App
from
'
~/issuable_suggestions/components/app.vue
'
;
import
Suggestion
from
'
~/issuable_suggestions/components/item.vue
'
;
describe
(
'
Issuable suggestions app component
'
,
()
=>
{
let
vm
;
function
createComponent
(
search
=
'
search
'
)
{
vm
=
shallowMount
(
App
,
{
propsData
:
{
search
,
projectPath
:
'
project
'
,
},
});
}
afterEach
(()
=>
{
vm
.
destroy
();
});
it
(
'
does not render with empty search
'
,
()
=>
{
createComponent
(
''
);
expect
(
vm
.
isVisible
()).
toBe
(
false
);
});
describe
(
'
with data
'
,
()
=>
{
let
data
;
beforeEach
(()
=>
{
data
=
{
issues
:
[{
id
:
1
},
{
id
:
2
}]
};
});
it
(
'
renders component
'
,
()
=>
{
createComponent
();
vm
.
setData
(
data
);
expect
(
vm
.
isEmpty
()).
toBe
(
false
);
});
it
(
'
does not render with empty search
'
,
()
=>
{
createComponent
(
''
);
vm
.
setData
(
data
);
expect
(
vm
.
isVisible
()).
toBe
(
false
);
});
it
(
'
does not render when loading
'
,
()
=>
{
createComponent
();
vm
.
setData
({
...
data
,
loading
:
1
,
});
expect
(
vm
.
isVisible
()).
toBe
(
false
);
});
it
(
'
does not render with empty issues data
'
,
()
=>
{
createComponent
();
vm
.
setData
({
issues
:
[]
});
expect
(
vm
.
isVisible
()).
toBe
(
false
);
});
it
(
'
renders list of issues
'
,
()
=>
{
createComponent
();
vm
.
setData
(
data
);
expect
(
vm
.
findAll
(
Suggestion
).
length
).
toBe
(
2
);
});
it
(
'
adds margin class to first item
'
,
()
=>
{
createComponent
();
vm
.
setData
(
data
);
expect
(
vm
.
findAll
(
'
li
'
)
.
at
(
0
)
.
is
(
'
.append-bottom-default
'
),
).
toBe
(
true
);
});
it
(
'
does not add margin class to last item
'
,
()
=>
{
createComponent
();
vm
.
setData
(
data
);
expect
(
vm
.
findAll
(
'
li
'
)
.
at
(
1
)
.
is
(
'
.append-bottom-default
'
),
).
toBe
(
false
);
});
});
});
spec/javascripts/issuable_suggestions/components/item_spec.js
0 → 100644
View file @
d186d8c7
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
GlTooltip
,
GlLink
}
from
'
@gitlab/ui
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
UserAvatarImage
from
'
~/vue_shared/components/user_avatar/user_avatar_image.vue
'
;
import
Suggestion
from
'
~/issuable_suggestions/components/item.vue
'
;
import
mockData
from
'
../mock_data
'
;
describe
(
'
Issuable suggestions suggestion component
'
,
()
=>
{
let
vm
;
function
createComponent
(
suggestion
=
{})
{
vm
=
shallowMount
(
Suggestion
,
{
propsData
:
{
suggestion
:
{
...
mockData
(),
...
suggestion
,
},
},
});
}
afterEach
(()
=>
{
vm
.
destroy
();
});
it
(
'
renders title
'
,
()
=>
{
createComponent
();
expect
(
vm
.
text
()).
toContain
(
'
Test issue
'
);
});
it
(
'
renders issue link
'
,
()
=>
{
createComponent
();
const
link
=
vm
.
find
(
GlLink
);
expect
(
link
.
attributes
(
'
href
'
)).
toBe
(
`
${
gl
.
TEST_HOST
}
/test/issue/1`
);
});
it
(
'
renders IID
'
,
()
=>
{
createComponent
();
expect
(
vm
.
text
()).
toContain
(
'
#1
'
);
});
describe
(
'
opened state
'
,
()
=>
{
it
(
'
renders icon
'
,
()
=>
{
createComponent
();
const
icon
=
vm
.
find
(
Icon
);
expect
(
icon
.
props
(
'
name
'
)).
toBe
(
'
issue-open-m
'
);
});
it
(
'
renders created timeago
'
,
()
=>
{
createComponent
({
closedAt
:
''
,
});
const
tooltip
=
vm
.
find
(
GlTooltip
);
expect
(
tooltip
.
find
(
'
.d-block
'
).
text
()).
toContain
(
'
Opened
'
);
expect
(
tooltip
.
text
()).
toContain
(
'
3 days ago
'
);
});
});
describe
(
'
closed state
'
,
()
=>
{
it
(
'
renders icon
'
,
()
=>
{
createComponent
({
state
:
'
closed
'
,
});
const
icon
=
vm
.
find
(
Icon
);
expect
(
icon
.
props
(
'
name
'
)).
toBe
(
'
issue-close
'
);
});
it
(
'
renders closed timeago
'
,
()
=>
{
createComponent
();
const
tooltip
=
vm
.
find
(
GlTooltip
);
expect
(
tooltip
.
find
(
'
.d-block
'
).
text
()).
toContain
(
'
Opened
'
);
expect
(
tooltip
.
text
()).
toContain
(
'
1 day ago
'
);
});
});
describe
(
'
author
'
,
()
=>
{
it
(
'
renders author info
'
,
()
=>
{
createComponent
();
const
link
=
vm
.
findAll
(
GlLink
).
at
(
1
);
expect
(
link
.
text
()).
toContain
(
'
Author Name
'
);
expect
(
link
.
text
()).
toContain
(
'
@author.username
'
);
});
it
(
'
renders author image
'
,
()
=>
{
createComponent
();
const
image
=
vm
.
find
(
UserAvatarImage
);
expect
(
image
.
props
(
'
imgSrc
'
)).
toBe
(
`
${
gl
.
TEST_HOST
}
/avatar`
);
});
});
describe
(
'
counts
'
,
()
=>
{
it
(
'
renders upvotes count
'
,
()
=>
{
createComponent
();
const
count
=
vm
.
findAll
(
'
.suggestion-counts span
'
).
at
(
0
);
expect
(
count
.
text
()).
toContain
(
'
1
'
);
expect
(
count
.
find
(
Icon
).
props
(
'
name
'
)).
toBe
(
'
thumb-up
'
);
});
it
(
'
renders notes count
'
,
()
=>
{
createComponent
();
const
count
=
vm
.
findAll
(
'
.suggestion-counts span
'
).
at
(
1
);
expect
(
count
.
text
()).
toContain
(
'
2
'
);
expect
(
count
.
find
(
Icon
).
props
(
'
name
'
)).
toBe
(
'
comment
'
);
});
});
describe
(
'
confidential
'
,
()
=>
{
it
(
'
renders confidential icon
'
,
()
=>
{
createComponent
({
confidential
:
true
,
});
const
icon
=
vm
.
find
(
Icon
);
expect
(
icon
.
props
(
'
name
'
)).
toBe
(
'
eye-slash
'
);
expect
(
icon
.
attributes
(
'
data-original-title
'
)).
toBe
(
'
Confidential
'
);
});
});
});
spec/javascripts/issuable_suggestions/mock_data.js
0 → 100644
View file @
d186d8c7
function
getDate
(
daysMinus
)
{
const
today
=
new
Date
();
today
.
setDate
(
today
.
getDate
()
-
daysMinus
);
return
today
.
toISOString
();
}
export
default
()
=>
({
id
:
1
,
iid
:
1
,
state
:
'
opened
'
,
upvotes
:
1
,
userNotesCount
:
2
,
closedAt
:
getDate
(
1
),
createdAt
:
getDate
(
3
),
updatedAt
:
getDate
(
2
),
confidential
:
false
,
webUrl
:
`
${
gl
.
TEST_HOST
}
/test/issue/1`
,
title
:
'
Test issue
'
,
author
:
{
avatarUrl
:
`
${
gl
.
TEST_HOST
}
/avatar`
,
name
:
'
Author Name
'
,
username
:
'
author.username
'
,
webUrl
:
`
${
gl
.
TEST_HOST
}
/author`
,
},
});
spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb
0 → 100644
View file @
d186d8c7
require
'spec_helper'
describe
Gitlab
::
Graphql
::
Loaders
::
BatchModelLoader
do
describe
'#find'
do
let
(
:issue
)
{
create
(
:issue
)
}
let
(
:user
)
{
create
(
:user
)
}
it
'finds a model by id'
do
issue_result
=
described_class
.
new
(
Issue
,
issue
.
id
).
find
user_result
=
described_class
.
new
(
User
,
user
.
id
).
find
expect
(
issue_result
.
__sync
).
to
eq
(
issue
)
expect
(
user_result
.
__sync
).
to
eq
(
user
)
end
it
'only queries once per model'
do
other_user
=
create
(
:user
)
user
issue
expect
do
[
described_class
.
new
(
User
,
other_user
.
id
).
find
,
described_class
.
new
(
User
,
user
.
id
).
find
,
described_class
.
new
(
Issue
,
issue
.
id
).
find
].
map
(
&
:__sync
)
end
.
not_to
exceed_query_limit
(
2
)
end
end
end
spec/requests/api/graphql/project/issues_spec.rb
0 → 100644
View file @
d186d8c7
require
'spec_helper'
describe
'getting an issue list for a project'
do
include
GraphqlHelpers
let
(
:project
)
{
create
(
:project
,
:repository
,
:public
)
}
let
(
:current_user
)
{
create
(
:user
)
}
let
(
:issues_data
)
{
graphql_data
[
'project'
][
'issues'
][
'edges'
]
}
let!
(
:issues
)
do
create
(
:issue
,
project:
project
,
discussion_locked:
true
)
create
(
:issue
,
project:
project
)
end
let
(
:fields
)
do
<<~
QUERY
edges {
node {
#{
all_graphql_fields_for
(
'issues'
.
classify
)
}
}
}
QUERY
end
let
(
:query
)
do
graphql_query_for
(
'project'
,
{
'fullPath'
=>
project
.
full_path
},
query_graphql_field
(
'issues'
,
{},
fields
)
)
end
it_behaves_like
'a working graphql query'
do
before
do
post_graphql
(
query
,
current_user:
current_user
)
end
end
it
'includes a web_url'
do
post_graphql
(
query
,
current_user:
current_user
)
expect
(
issues_data
[
0
][
'node'
][
'webUrl'
]).
to
be_present
end
it
'includes discussion locked'
do
post_graphql
(
query
,
current_user:
current_user
)
expect
(
issues_data
[
0
][
'node'
][
'discussionLocked'
]).
to
eq
false
expect
(
issues_data
[
1
][
'node'
][
'discussionLocked'
]).
to
eq
true
end
context
'when the user does not have access to the issue'
do
it
'returns nil'
do
project
.
project_feature
.
update!
(
issues_access_level:
ProjectFeature
::
PRIVATE
)
post_graphql
(
query
)
expect
(
issues_data
).
to
eq
[]
end
end
end
yarn.lock
View file @
d186d8c7
This diff is collapsed.
Click to expand it.
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