Merge branch 'issuable-suggestions' into 'master'
Suggest issues when typing title Closes #22071 See merge request gitlab-org/gitlab-ce!22866
This commit is contained in:
commit
a99f342b42
36 changed files with 1185 additions and 1 deletions
|
@ -0,0 +1,96 @@
|
|||
<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>
|
137
app/assets/javascripts/issuable_suggestions/components/item.vue
Normal file
137
app/assets/javascripts/issuable_suggestions/components/item.vue
Normal file
|
@ -0,0 +1,137 @@
|
|||
<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>
|
38
app/assets/javascripts/issuable_suggestions/index.js
Normal file
38
app/assets/javascripts/issuable_suggestions/index.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
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,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
app/assets/javascripts/lib/graphql.js
Normal file
9
app/assets/javascripts/lib/graphql.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
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,
|
||||
},
|
||||
});
|
|
@ -7,6 +7,7 @@ import LabelsSelect from '~/labels_select';
|
|||
import MilestoneSelect from '~/milestone_select';
|
||||
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
|
||||
import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
|
||||
import initSuggestions from '~/issuable_suggestions';
|
||||
|
||||
export default () => {
|
||||
new ShortcutsNavigation();
|
||||
|
@ -15,4 +16,8 @@ export default () => {
|
|||
new LabelsSelect();
|
||||
new MilestoneSelect();
|
||||
new IssuableTemplateSelectors();
|
||||
|
||||
if (gon.features.issueSuggestions && gon.features.graphql) {
|
||||
initSuggestions();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -938,3 +938,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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,8 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
# 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 :set_suggested_issues_feature_flags, only: [:new]
|
||||
|
||||
respond_to :html
|
||||
|
||||
def index
|
||||
|
@ -263,4 +265,9 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
# 3. https://gitlab.com/gitlab-org/gitlab-ce/issues/42426
|
||||
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42422')
|
||||
end
|
||||
|
||||
def set_suggested_issues_feature_flags
|
||||
push_frontend_feature_flag(:graphql)
|
||||
push_frontend_feature_flag(:issue_suggestions)
|
||||
end
|
||||
end
|
||||
|
|
25
app/graphql/resolvers/issues_resolver.rb
Normal file
25
app/graphql/resolvers/issues_resolver.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
# 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
|
47
app/graphql/types/issue_type.rb
Normal file
47
app/graphql/types/issue_type.rb
Normal file
|
@ -0,0 +1,47 @@
|
|||
# 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
|
12
app/graphql/types/label_type.rb
Normal file
12
app/graphql/types/label_type.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
# 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
|
17
app/graphql/types/milestone_type.rb
Normal file
17
app/graphql/types/milestone_type.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
# 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
|
8
app/graphql/types/order.rb
Normal file
8
app/graphql/types/order.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
class Types::Order < Types::BaseEnum
|
||||
value "id", "Created at date"
|
||||
value "updated_at", "Updated at date"
|
||||
end
|
||||
end
|
14
app/graphql/types/permission_types/issue.rb
Normal file
14
app/graphql/types/permission_types/issue.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
# 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
|
|
@ -73,6 +73,11 @@ module Types
|
|||
authorize :read_merge_request
|
||||
end
|
||||
|
||||
field :issues,
|
||||
Types::IssueType.connection_type,
|
||||
null: true,
|
||||
resolver: Resolvers::IssuesResolver
|
||||
|
||||
field :pipelines,
|
||||
Types::Ci::PipelineType.connection_type,
|
||||
null: false,
|
||||
|
|
10
app/graphql/types/sort.rb
Normal file
10
app/graphql/types/sort.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
# 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
|
14
app/graphql/types/user_type.rb
Normal file
14
app/graphql/types/user_type.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
# 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
|
5
app/policies/milestone_policy.rb
Normal file
5
app/policies/milestone_policy.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MilestonePolicy < BasePolicy
|
||||
delegate { @subject.project }
|
||||
end
|
9
app/presenters/issue_presenter.rb
Normal file
9
app/presenters/issue_presenter.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class IssuePresenter < Gitlab::View::Presenter::Delegated
|
||||
presents :issue
|
||||
|
||||
def web_url
|
||||
Gitlab::UrlBuilder.build(issue)
|
||||
end
|
||||
end
|
9
app/presenters/user_presenter.rb
Normal file
9
app/presenters/user_presenter.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UserPresenter < Gitlab::View::Presenter::Delegated
|
||||
presents :user
|
||||
|
||||
def web_url
|
||||
Gitlab::Routing.url_helpers.user_url(user)
|
||||
end
|
||||
end
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
= 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?)
|
||||
- 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
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ module.exports = {
|
|||
},
|
||||
|
||||
resolve: {
|
||||
extensions: ['.js'],
|
||||
extensions: ['.js', '.gql', '.graphql'],
|
||||
alias: {
|
||||
'~': path.join(ROOT_PATH, 'app/assets/javascripts'),
|
||||
emojis: path.join(ROOT_PATH, 'fixtures/emojis'),
|
||||
|
@ -100,6 +100,11 @@ module.exports = {
|
|||
module: {
|
||||
strictExportPresence: true,
|
||||
rules: [
|
||||
{
|
||||
type: 'javascript/auto',
|
||||
test: /\.mjs$/,
|
||||
use: [],
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: path => /node_modules|vendor[\\/]assets/.test(path) && !/\.vue\.js/.test(path),
|
||||
|
@ -121,6 +126,11 @@ module.exports = {
|
|||
].join('|'),
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(graphql|gql)$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'graphql-tag/loader',
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
loader: 'raw-loader',
|
||||
|
|
29
lib/gitlab/graphql/loaders/batch_model_loader.rb
Normal file
29
lib/gitlab/graphql/loaders/batch_model_loader.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
# 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
|
|
@ -1379,6 +1379,9 @@ msgstr ""
|
|||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
msgid "Closed"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
|
||||
msgstr ""
|
||||
|
||||
|
@ -4473,6 +4476,9 @@ msgstr ""
|
|||
msgid "Open source software to collaborate on code"
|
||||
msgstr ""
|
||||
|
||||
msgid "Opened"
|
||||
msgstr ""
|
||||
|
||||
msgid "OpenedNDaysAgo|Opened"
|
||||
msgstr ""
|
||||
|
||||
|
@ -5862,6 +5868,9 @@ msgstr ""
|
|||
msgid "Sign-up restrictions"
|
||||
msgstr ""
|
||||
|
||||
msgid "Similar issues"
|
||||
msgstr ""
|
||||
|
||||
msgid "Size and domain settings for static websites"
|
||||
msgstr ""
|
||||
|
||||
|
@ -6398,6 +6407,9 @@ msgstr ""
|
|||
msgid "There was an error when unsubscribing from this label."
|
||||
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}."
|
||||
msgstr ""
|
||||
|
||||
|
@ -7846,6 +7858,9 @@ msgstr ""
|
|||
msgid "this document"
|
||||
msgstr ""
|
||||
|
||||
msgid "updated"
|
||||
msgstr ""
|
||||
|
||||
msgid "username"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
"@babel/preset-env": "^7.1.0",
|
||||
"@gitlab/svgs": "^1.38.0",
|
||||
"@gitlab/ui": "^1.11.0",
|
||||
"apollo-boost": "^0.1.20",
|
||||
"apollo-client": "^2.4.5",
|
||||
"autosize": "^4.0.0",
|
||||
"axios": "^0.17.1",
|
||||
"babel-loader": "^8.0.4",
|
||||
|
@ -60,6 +62,7 @@
|
|||
"formdata-polyfill": "^3.0.11",
|
||||
"fuzzaldrin-plus": "^0.5.0",
|
||||
"glob": "^7.1.2",
|
||||
"graphql": "^14.0.2",
|
||||
"imports-loader": "^0.8.0",
|
||||
"jed": "^1.1.1",
|
||||
"jquery": "^3.2.1",
|
||||
|
@ -97,6 +100,7 @@
|
|||
"url-loader": "^1.1.1",
|
||||
"visibilityjs": "^1.2.4",
|
||||
"vue": "^2.5.17",
|
||||
"vue-apollo": "^3.0.0-beta.25",
|
||||
"vue-loader": "^15.4.2",
|
||||
"vue-resource": "^1.5.0",
|
||||
"vue-router": "^3.0.1",
|
||||
|
@ -127,6 +131,7 @@
|
|||
"eslint-plugin-jasmine": "^2.10.1",
|
||||
"gettext-extractor": "^3.3.2",
|
||||
"gettext-extractor-vue": "^4.0.1",
|
||||
"graphql-tag": "^2.10.0",
|
||||
"istanbul": "^0.4.5",
|
||||
"jasmine-core": "^2.9.0",
|
||||
"jasmine-diff": "^0.1.3",
|
||||
|
|
|
@ -682,6 +682,18 @@ describe 'Issues' do
|
|||
expect(find('.js-issuable-selector .dropdown-toggle-text')).to have_content('bug')
|
||||
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
|
||||
|
||||
describe 'new issue by email' do
|
||||
|
|
40
spec/graphql/resolvers/issues_resolver_spec.rb
Normal file
40
spec/graphql/resolvers/issues_resolver_spec.rb
Normal file
|
@ -0,0 +1,40 @@
|
|||
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
|
7
spec/graphql/types/issue_type_spec.rb
Normal file
7
spec/graphql/types/issue_type_spec.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
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
|
12
spec/graphql/types/permission_types/issue_spec.rb
Normal file
12
spec/graphql/types/permission_types/issue_spec.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
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
|
|
@ -14,5 +14,9 @@ describe GitlabSchema.types['Project'] do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'nested issues' do
|
||||
it { expect(described_class).to have_graphql_field(:issues) }
|
||||
end
|
||||
|
||||
it { is_expected.to have_graphql_field(:pipelines) }
|
||||
end
|
||||
|
|
96
spec/javascripts/issuable_suggestions/components/app_spec.js
Normal file
96
spec/javascripts/issuable_suggestions/components/app_spec.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
});
|
139
spec/javascripts/issuable_suggestions/components/item_spec.js
Normal file
139
spec/javascripts/issuable_suggestions/components/item_spec.js
Normal file
|
@ -0,0 +1,139 @@
|
|||
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');
|
||||
});
|
||||
});
|
||||
});
|
26
spec/javascripts/issuable_suggestions/mock_data.js
Normal file
26
spec/javascripts/issuable_suggestions/mock_data.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
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`,
|
||||
},
|
||||
});
|
28
spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb
Normal file
28
spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb
Normal file
|
@ -0,0 +1,28 @@
|
|||
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
|
59
spec/requests/api/graphql/project/issues_spec.rb
Normal file
59
spec/requests/api/graphql/project/issues_spec.rb
Normal file
|
@ -0,0 +1,59 @@
|
|||
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
|
173
yarn.lock
173
yarn.lock
|
@ -654,6 +654,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
|
||||
integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==
|
||||
|
||||
"@types/async@2.0.50":
|
||||
version "2.0.50"
|
||||
resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.50.tgz#117540e026d64e1846093abbd5adc7e27fda7bcb"
|
||||
integrity sha512-VMhZMMQgV1zsR+lX/0IBfAk+8Eb7dPVMWiQGFAt3qjo5x7Ml6b77jUo0e1C3ToD+XRDXqtrfw+6AB0uUsPEr3Q==
|
||||
|
||||
"@types/events@*":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"
|
||||
|
@ -693,6 +698,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45"
|
||||
integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==
|
||||
|
||||
"@types/zen-observable@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d"
|
||||
integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg==
|
||||
|
||||
"@vue/component-compiler-utils@^2.0.0":
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-2.2.0.tgz#bbbb7ed38a9a8a7c93abe7ef2e54a90a04b631b4"
|
||||
|
@ -991,6 +1001,103 @@ anymatch@^2.0.0:
|
|||
micromatch "^3.1.4"
|
||||
normalize-path "^2.1.1"
|
||||
|
||||
apollo-boost@^0.1.20:
|
||||
version "0.1.20"
|
||||
resolved "https://registry.yarnpkg.com/apollo-boost/-/apollo-boost-0.1.20.tgz#cc3e418ebd2bea857656685d32a7a20443493363"
|
||||
integrity sha512-n2MiEY5IGpD/cy0RH+pM9vbmobM/JZ5qz38XQAUA41FxxMPlLFQxf0IUMm0tijLOJvJJBub3pDt+Of4TVPBCqA==
|
||||
dependencies:
|
||||
apollo-cache "^1.1.20"
|
||||
apollo-cache-inmemory "^1.3.9"
|
||||
apollo-client "^2.4.5"
|
||||
apollo-link "^1.0.6"
|
||||
apollo-link-error "^1.0.3"
|
||||
apollo-link-http "^1.3.1"
|
||||
apollo-link-state "^0.4.0"
|
||||
graphql-tag "^2.4.2"
|
||||
|
||||
apollo-cache-inmemory@^1.3.9:
|
||||
version "1.3.9"
|
||||
resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.3.9.tgz#10738ba6a04faaeeb0da21bbcc1f7c0b5902910c"
|
||||
integrity sha512-Q2k84p/OqIuMUyeWGc6XbVXXZu0erYOO+wTx9p+CnQUspnNvf7zmvFNgFnmudXzfuG1m1CSzePk6fC/M1ehOqQ==
|
||||
dependencies:
|
||||
apollo-cache "^1.1.20"
|
||||
apollo-utilities "^1.0.25"
|
||||
optimism "^0.6.6"
|
||||
|
||||
apollo-cache@1.1.20, apollo-cache@^1.1.20:
|
||||
version "1.1.20"
|
||||
resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.1.20.tgz#6152cc4baf6a63e376efee79f75de4f5c84bf90e"
|
||||
integrity sha512-+Du0/4kUSuf5PjPx0+pvgMGV12ezbHA8/hubYuqRQoy/4AWb4faa61CgJNI6cKz2mhDd9m94VTNKTX11NntwkQ==
|
||||
dependencies:
|
||||
apollo-utilities "^1.0.25"
|
||||
|
||||
apollo-client@^2.4.5:
|
||||
version "2.4.5"
|
||||
resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.4.5.tgz#545beda1ef60814943b5622f0feabc9f29ee9822"
|
||||
integrity sha512-nUm06EGa4TP/IY68OzmC3lTD32TqkjLOQdb69uYo+lHl8NnwebtrAw3qFtsQtTEz6ueBp/Z/HasNZng4jwafVQ==
|
||||
dependencies:
|
||||
"@types/zen-observable" "^0.8.0"
|
||||
apollo-cache "1.1.20"
|
||||
apollo-link "^1.0.0"
|
||||
apollo-link-dedup "^1.0.0"
|
||||
apollo-utilities "1.0.25"
|
||||
symbol-observable "^1.0.2"
|
||||
zen-observable "^0.8.0"
|
||||
optionalDependencies:
|
||||
"@types/async" "2.0.50"
|
||||
|
||||
apollo-link-dedup@^1.0.0:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/apollo-link-dedup/-/apollo-link-dedup-1.0.10.tgz#7b94589fe7f969777efd18a129043c78430800ae"
|
||||
integrity sha512-tpUI9lMZsidxdNygSY1FxflXEkUZnvKRkMUsXXuQUNoSLeNtEvUX7QtKRAl4k9ubLl8JKKc9X3L3onAFeGTK8w==
|
||||
dependencies:
|
||||
apollo-link "^1.2.3"
|
||||
|
||||
apollo-link-error@^1.0.3:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/apollo-link-error/-/apollo-link-error-1.1.1.tgz#69d7124d4dc11ce60f505c940f05d4f1aa0945fb"
|
||||
integrity sha512-/yPcaQWcBdB94vpJ4FsiCJt1dAGGRm+6Tsj3wKwP+72taBH+UsGRQQZk7U/1cpZwl1yqhHZn+ZNhVOebpPcIlA==
|
||||
dependencies:
|
||||
apollo-link "^1.2.3"
|
||||
|
||||
apollo-link-http-common@^0.2.5:
|
||||
version "0.2.5"
|
||||
resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.5.tgz#d094beb7971523203359bf830bfbfa7b4e7c30ed"
|
||||
integrity sha512-6FV1wr5AqAyJ64Em1dq5hhGgiyxZE383VJQmhIoDVc3MyNcFL92TkhxREOs4rnH2a9X2iJMko7nodHSGLC6d8w==
|
||||
dependencies:
|
||||
apollo-link "^1.2.3"
|
||||
|
||||
apollo-link-http@^1.3.1:
|
||||
version "1.5.5"
|
||||
resolved "https://registry.yarnpkg.com/apollo-link-http/-/apollo-link-http-1.5.5.tgz#7dbe851821771ad67fa29e3900c57f38cbd80da8"
|
||||
integrity sha512-C5N6N/mRwmepvtzO27dgMEU3MMtRKSqcljBkYNZmWwH11BxkUQ5imBLPM3V4QJXNE7NFuAQAB5PeUd4ligivTQ==
|
||||
dependencies:
|
||||
apollo-link "^1.2.3"
|
||||
apollo-link-http-common "^0.2.5"
|
||||
|
||||
apollo-link-state@^0.4.0:
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/apollo-link-state/-/apollo-link-state-0.4.2.tgz#ac00e9be9b0ca89eae0be6ba31fe904b80bbe2e8"
|
||||
integrity sha512-xMPcAfuiPVYXaLwC6oJFIZrKgV3GmdO31Ag2eufRoXpvT0AfJZjdaPB4450Nu9TslHRePN9A3quxNueILlQxlw==
|
||||
dependencies:
|
||||
apollo-utilities "^1.0.8"
|
||||
graphql-anywhere "^4.1.0-alpha.0"
|
||||
|
||||
apollo-link@^1.0.0, apollo-link@^1.0.6, apollo-link@^1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.3.tgz#9bd8d5fe1d88d31dc91dae9ecc22474d451fb70d"
|
||||
integrity sha512-iL9yS2OfxYhigme5bpTbmRyC+Htt6tyo2fRMHT3K1XRL/C5IQDDz37OjpPy4ndx7WInSvfSZaaOTKFja9VWqSw==
|
||||
dependencies:
|
||||
apollo-utilities "^1.0.0"
|
||||
zen-observable-ts "^0.8.10"
|
||||
|
||||
apollo-utilities@1.0.25, apollo-utilities@^1.0.0, apollo-utilities@^1.0.25, apollo-utilities@^1.0.8:
|
||||
version "1.0.25"
|
||||
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.25.tgz#899b00f5f990fb451675adf84cb3de82eb6372ea"
|
||||
integrity sha512-AXvqkhni3Ir1ffm4SA1QzXn8k8I5BBl4PVKEyak734i4jFdp+xgfUyi2VCqF64TJlFTA/B73TRDUvO2D+tKtZg==
|
||||
dependencies:
|
||||
fast-json-stable-stringify "^2.0.0"
|
||||
|
||||
append-transform@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab"
|
||||
|
@ -3958,6 +4065,25 @@ graphlibrary@^2.2.0:
|
|||
dependencies:
|
||||
lodash "^4.17.5"
|
||||
|
||||
graphql-anywhere@^4.1.0-alpha.0:
|
||||
version "4.1.22"
|
||||
resolved "https://registry.yarnpkg.com/graphql-anywhere/-/graphql-anywhere-4.1.22.tgz#1c831ba3c9e5664a0dd24d10d23a9e9512d92056"
|
||||
integrity sha512-qm2/1cKM8nfotxDhm4J0r1znVlK0Yge/yEKt26EVVBgpIhvxjXYFALCGbr7cvfDlvzal1iSPpaYa+8YTtjsxQA==
|
||||
dependencies:
|
||||
apollo-utilities "^1.0.25"
|
||||
|
||||
graphql-tag@^2.10.0, graphql-tag@^2.4.2:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.0.tgz#87da024be863e357551b2b8700e496ee2d4353ae"
|
||||
integrity sha512-9FD6cw976TLLf9WYIUPCaaTpniawIjHWZSwIRZSjrfufJamcXbVVYfN2TWvJYbw0Xf2JjYbl1/f2+wDnBVw3/w==
|
||||
|
||||
graphql@^14.0.2:
|
||||
version "14.0.2"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.0.2.tgz#7dded337a4c3fd2d075692323384034b357f5650"
|
||||
integrity sha512-gUC4YYsaiSJT1h40krG3J+USGlwhzNTXSb4IOZljn9ag5Tj+RkoXrWp+Kh7WyE3t1NCfab5kzCuxBIvOMERMXw==
|
||||
dependencies:
|
||||
iterall "^1.2.2"
|
||||
|
||||
gzip-size@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.0.0.tgz#a55ecd99222f4c48fd8c01c625ce3b349d0a0e80"
|
||||
|
@ -4254,6 +4380,11 @@ immediate@~3.0.5:
|
|||
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
|
||||
integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
|
||||
|
||||
immutable-tuple@^0.4.9:
|
||||
version "0.4.9"
|
||||
resolved "https://registry.yarnpkg.com/immutable-tuple/-/immutable-tuple-0.4.9.tgz#473ebdd6c169c461913a454bf87ef8f601a20ff0"
|
||||
integrity sha512-LWbJPZnidF8eczu7XmcnLBsumuyRBkpwIRPCZxlojouhBo5jEBO4toj6n7hMy6IxHU/c+MqDSWkvaTpPlMQcyA==
|
||||
|
||||
import-lazy@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
|
||||
|
@ -4801,6 +4932,11 @@ isurl@^1.0.0-alpha5:
|
|||
has-to-string-tag-x "^1.2.0"
|
||||
is-object "^1.0.1"
|
||||
|
||||
iterall@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7"
|
||||
integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==
|
||||
|
||||
jasmine-core@^2.9.0:
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.9.0.tgz#bfbb56defcd30789adec5a3fbba8504233289c72"
|
||||
|
@ -5940,6 +6076,13 @@ opn@^5.1.0:
|
|||
dependencies:
|
||||
is-wsl "^1.1.0"
|
||||
|
||||
optimism@^0.6.6:
|
||||
version "0.6.8"
|
||||
resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.6.8.tgz#0780b546da8cd0a72e5207e0c3706c990c8673a6"
|
||||
integrity sha512-bN5n1KCxSqwBDnmgDnzMtQTHdL+uea2HYFx1smvtE+w2AMl0Uy31g0aXnP/Nt85OINnMJPRpJyfRQLTCqn5Weg==
|
||||
dependencies:
|
||||
immutable-tuple "^0.4.9"
|
||||
|
||||
optimist@^0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
|
||||
|
@ -7586,6 +7729,11 @@ svg4everybody@2.1.9:
|
|||
resolved "https://registry.yarnpkg.com/svg4everybody/-/svg4everybody-2.1.9.tgz#5bd9f6defc133859a044646d4743fabc28db7e2d"
|
||||
integrity sha1-W9n23vwTOFmgRGRtR0P6vCjbfi0=
|
||||
|
||||
symbol-observable@^1.0.2:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
|
||||
|
||||
table@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "http://registry.npmjs.org/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc"
|
||||
|
@ -7663,6 +7811,11 @@ three@^0.84.0:
|
|||
resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918"
|
||||
integrity sha1-lb6FpVoPoAKqYl7VWRMJV9z/2Rg=
|
||||
|
||||
throttle-debounce@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.0.1.tgz#7307ddd6cd9acadb349132fbf6c18d78c88a5e62"
|
||||
integrity sha512-Sr6jZBlWShsAaSXKyNXyNicOrJW/KtkDqIEwHt4wYwWA2wa/q67Luhqoujg48V8hTk60wB56tYrJJn6jc2R7VA==
|
||||
|
||||
through2@^2.0.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"
|
||||
|
@ -8101,6 +8254,14 @@ void-elements@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
|
||||
integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
|
||||
|
||||
vue-apollo@^3.0.0-beta.25:
|
||||
version "3.0.0-beta.25"
|
||||
resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.0.0-beta.25.tgz#05a9a699b2ba6103639e9bd6c3bb88ca04c4b637"
|
||||
integrity sha512-M7/l3h0NlFvaZ/s/wrtRiOt3xXMbaNNuteGaCY+U5D0ABrQqvCgy5mayIZHurQxbloluNkbCt18wRKAgJTAuKA==
|
||||
dependencies:
|
||||
chalk "^2.4.1"
|
||||
throttle-debounce "^2.0.0"
|
||||
|
||||
vue-eslint-parser@^3.2.1:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-3.2.2.tgz#47c971ee4c39b0ee7d7f5e154cb621beb22f7a34"
|
||||
|
@ -8516,3 +8677,15 @@ yeast@0.1.2:
|
|||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
|
||||
integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=
|
||||
|
||||
zen-observable-ts@^0.8.10:
|
||||
version "0.8.10"
|
||||
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.10.tgz#18e2ce1c89fe026e9621fd83cc05168228fce829"
|
||||
integrity sha512-5vqMtRggU/2GhePC9OU4sYEWOdvmayp2k3gjPf4F0mXwB3CSbbNznfDUvDJx9O2ZTa1EIXdJhPchQveFKwNXPQ==
|
||||
dependencies:
|
||||
zen-observable "^0.8.0"
|
||||
|
||||
zen-observable@^0.8.0:
|
||||
version "0.8.11"
|
||||
resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.11.tgz#d3415885eeeb42ee5abb9821c95bb518fcd6d199"
|
||||
integrity sha512-N3xXQVr4L61rZvGMpWe8XoCGX8vhU35dPyQ4fm5CY/KDlG0F75un14hjbckPXTDuKUY6V0dqR2giT6xN8Y4GEQ==
|
||||
|
|
Loading…
Reference in a new issue