From ebf4c51d8003e24f29cbc980d09df29c3bba1072 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 27 Nov 2018 12:06:36 +0000 Subject: Suggests issues when typing title This suggests possibly related issues when the user types a title. This uses GraphQL to allow the frontend to request the exact data that is requires. We also get free caching through the Vue Apollo plugin. With this we can include the ability to import .graphql files in JS and Vue files. Also we now have the Vue test utils library to make testing Vue components easier. Closes #22071 --- .../issuable_suggestions/components/app.vue | 96 ++++++++++++ .../issuable_suggestions/components/item.vue | 137 ++++++++++++++++ .../javascripts/issuable_suggestions/index.js | 38 +++++ .../issuable_suggestions/queries/issues.graphql | 26 ++++ app/assets/javascripts/lib/graphql.js | 9 ++ .../javascripts/pages/projects/issues/form.js | 5 + app/assets/stylesheets/pages/issuable.scss | 34 ++++ app/controllers/projects/issues_controller.rb | 7 + app/graphql/resolvers/issues_resolver.rb | 25 +++ app/graphql/types/issue_type.rb | 47 ++++++ app/graphql/types/label_type.rb | 12 ++ app/graphql/types/milestone_type.rb | 17 ++ app/graphql/types/order.rb | 8 + app/graphql/types/permission_types/issue.rb | 14 ++ app/graphql/types/project_type.rb | 5 + app/graphql/types/sort.rb | 10 ++ app/graphql/types/user_type.rb | 14 ++ app/policies/milestone_policy.rb | 5 + app/presenters/issue_presenter.rb | 9 ++ app/presenters/user_presenter.rb | 9 ++ app/views/shared/issuable/_form.html.haml | 2 + config/webpack.config.js | 12 +- lib/gitlab/graphql/loaders/batch_model_loader.rb | 29 ++++ locale/gitlab.pot | 15 ++ package.json | 5 + spec/features/issues_spec.rb | 12 ++ spec/graphql/resolvers/issues_resolver_spec.rb | 40 +++++ spec/graphql/types/issue_type_spec.rb | 7 + spec/graphql/types/permission_types/issue_spec.rb | 12 ++ spec/graphql/types/project_type_spec.rb | 4 + .../issuable_suggestions/components/app_spec.js | 92 +++++++++++ .../issuable_suggestions/components/item_spec.js | 135 ++++++++++++++++ spec/javascripts/issuable_suggestions/mock_data.js | 26 ++++ .../graphql/loaders/batch_model_loader_spec.rb | 28 ++++ spec/requests/api/graphql/project/issues_spec.rb | 59 +++++++ yarn.lock | 173 +++++++++++++++++++++ 36 files changed, 1177 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/issuable_suggestions/components/app.vue create mode 100644 app/assets/javascripts/issuable_suggestions/components/item.vue create mode 100644 app/assets/javascripts/issuable_suggestions/index.js create mode 100644 app/assets/javascripts/issuable_suggestions/queries/issues.graphql create mode 100644 app/assets/javascripts/lib/graphql.js create mode 100644 app/graphql/resolvers/issues_resolver.rb create mode 100644 app/graphql/types/issue_type.rb create mode 100644 app/graphql/types/label_type.rb create mode 100644 app/graphql/types/milestone_type.rb create mode 100644 app/graphql/types/order.rb create mode 100644 app/graphql/types/permission_types/issue.rb create mode 100644 app/graphql/types/sort.rb create mode 100644 app/graphql/types/user_type.rb create mode 100644 app/policies/milestone_policy.rb create mode 100644 app/presenters/issue_presenter.rb create mode 100644 app/presenters/user_presenter.rb create mode 100644 lib/gitlab/graphql/loaders/batch_model_loader.rb create mode 100644 spec/graphql/resolvers/issues_resolver_spec.rb create mode 100644 spec/graphql/types/issue_type_spec.rb create mode 100644 spec/graphql/types/permission_types/issue_spec.rb create mode 100644 spec/javascripts/issuable_suggestions/components/app_spec.js create mode 100644 spec/javascripts/issuable_suggestions/components/item_spec.js create mode 100644 spec/javascripts/issuable_suggestions/mock_data.js create mode 100644 spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb create mode 100644 spec/requests/api/graphql/project/issues_spec.rb diff --git a/app/assets/javascripts/issuable_suggestions/components/app.vue b/app/assets/javascripts/issuable_suggestions/components/app.vue new file mode 100644 index 00000000000..eea0701312b --- /dev/null +++ b/app/assets/javascripts/issuable_suggestions/components/app.vue @@ -0,0 +1,96 @@ + + + diff --git a/app/assets/javascripts/issuable_suggestions/components/item.vue b/app/assets/javascripts/issuable_suggestions/components/item.vue new file mode 100644 index 00000000000..9a16b486bf5 --- /dev/null +++ b/app/assets/javascripts/issuable_suggestions/components/item.vue @@ -0,0 +1,137 @@ + + + diff --git a/app/assets/javascripts/issuable_suggestions/index.js b/app/assets/javascripts/issuable_suggestions/index.js new file mode 100644 index 00000000000..2c80cf1797a --- /dev/null +++ b/app/assets/javascripts/issuable_suggestions/index.js @@ -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, + }, + }); + }, + }); +} diff --git a/app/assets/javascripts/issuable_suggestions/queries/issues.graphql b/app/assets/javascripts/issuable_suggestions/queries/issues.graphql new file mode 100644 index 00000000000..2384b381344 --- /dev/null +++ b/app/assets/javascripts/issuable_suggestions/queries/issues.graphql @@ -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 + } + } + } + } + } +} diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js new file mode 100644 index 00000000000..20a0f142d9e --- /dev/null +++ b/app/assets/javascripts/lib/graphql.js @@ -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, + }, +}); diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js index 197bfa8a394..02a56685a35 100644 --- a/app/assets/javascripts/pages/projects/issues/form.js +++ b/app/assets/javascripts/pages/projects/issues/form.js @@ -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(); + } }; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 78c5ae9ae63..5b5f486ea63 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -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; + } +} diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 308f666394c..d6d7110355b 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -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 diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb new file mode 100644 index 00000000000..4ab3c13787a --- /dev/null +++ b/app/graphql/resolvers/issues_resolver.rb @@ -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 diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb new file mode 100644 index 00000000000..a8f2f7914a8 --- /dev/null +++ b/app/graphql/types/issue_type.rb @@ -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 diff --git a/app/graphql/types/label_type.rb b/app/graphql/types/label_type.rb new file mode 100644 index 00000000000..ccd466edc1a --- /dev/null +++ b/app/graphql/types/label_type.rb @@ -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 diff --git a/app/graphql/types/milestone_type.rb b/app/graphql/types/milestone_type.rb new file mode 100644 index 00000000000..af31b572c9a --- /dev/null +++ b/app/graphql/types/milestone_type.rb @@ -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 diff --git a/app/graphql/types/order.rb b/app/graphql/types/order.rb new file mode 100644 index 00000000000..c5e1cc406b4 --- /dev/null +++ b/app/graphql/types/order.rb @@ -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 diff --git a/app/graphql/types/permission_types/issue.rb b/app/graphql/types/permission_types/issue.rb new file mode 100644 index 00000000000..199540c7d6d --- /dev/null +++ b/app/graphql/types/permission_types/issue.rb @@ -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 diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 7b879608b34..050706f97be 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -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, diff --git a/app/graphql/types/sort.rb b/app/graphql/types/sort.rb new file mode 100644 index 00000000000..1f756fdab69 --- /dev/null +++ b/app/graphql/types/sort.rb @@ -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 diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb new file mode 100644 index 00000000000..a13e65207df --- /dev/null +++ b/app/graphql/types/user_type.rb @@ -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 diff --git a/app/policies/milestone_policy.rb b/app/policies/milestone_policy.rb new file mode 100644 index 00000000000..ac4f5b08504 --- /dev/null +++ b/app/policies/milestone_policy.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class MilestonePolicy < BasePolicy + delegate { @subject.project } +end diff --git a/app/presenters/issue_presenter.rb b/app/presenters/issue_presenter.rb new file mode 100644 index 00000000000..c12a202efbc --- /dev/null +++ b/app/presenters/issue_presenter.rb @@ -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 diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb new file mode 100644 index 00000000000..14ef53e9ec8 --- /dev/null +++ b/app/presenters/user_presenter.rb @@ -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 diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index b33c758b464..1618655182c 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -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 diff --git a/config/webpack.config.js b/config/webpack.config.js index 9ecae9790fd..b9044e13f50 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -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', diff --git a/lib/gitlab/graphql/loaders/batch_model_loader.rb b/lib/gitlab/graphql/loaders/batch_model_loader.rb new file mode 100644 index 00000000000..5a0099dc6b1 --- /dev/null +++ b/lib/gitlab/graphql/loaders/batch_model_loader.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 71374388a7d..805a2f2a327 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1379,6 +1379,9 @@ msgstr "" msgid "Close" msgstr "" +msgid "Closed" +msgstr "" + msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster" msgstr "" @@ -4467,6 +4470,9 @@ msgstr "" msgid "Open source software to collaborate on code" msgstr "" +msgid "Opened" +msgstr "" + msgid "OpenedNDaysAgo|Opened" msgstr "" @@ -5856,6 +5862,9 @@ msgstr "" msgid "Sign-up restrictions" msgstr "" +msgid "Similar issues" +msgstr "" + msgid "Size and domain settings for static websites" msgstr "" @@ -6392,6 +6401,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 "" @@ -7840,6 +7852,9 @@ msgstr "" msgid "this document" msgstr "" +msgid "updated" +msgstr "" + msgid "username" msgstr "" diff --git a/package.json b/package.json index 64df2532977..62e48625c05 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 5c1ffb76351..406e80e91aa 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -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 diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb new file mode 100644 index 00000000000..ca90673521c --- /dev/null +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -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 diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb new file mode 100644 index 00000000000..63a07647a60 --- /dev/null +++ b/spec/graphql/types/issue_type_spec.rb @@ -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 diff --git a/spec/graphql/types/permission_types/issue_spec.rb b/spec/graphql/types/permission_types/issue_spec.rb new file mode 100644 index 00000000000..c3f84629aa2 --- /dev/null +++ b/spec/graphql/types/permission_types/issue_spec.rb @@ -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 diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 49606c397b9..61d4c42665a 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -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 diff --git a/spec/javascripts/issuable_suggestions/components/app_spec.js b/spec/javascripts/issuable_suggestions/components/app_spec.js new file mode 100644 index 00000000000..f26f25679db --- /dev/null +++ b/spec/javascripts/issuable_suggestions/components/app_spec.js @@ -0,0 +1,92 @@ +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', + }, + }); + } + + 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); + }); + }); +}); diff --git a/spec/javascripts/issuable_suggestions/components/item_spec.js b/spec/javascripts/issuable_suggestions/components/item_spec.js new file mode 100644 index 00000000000..c5291d05397 --- /dev/null +++ b/spec/javascripts/issuable_suggestions/components/item_spec.js @@ -0,0 +1,135 @@ +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, + }, + }, + }); + } + + 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'); + }); + }); +}); diff --git a/spec/javascripts/issuable_suggestions/mock_data.js b/spec/javascripts/issuable_suggestions/mock_data.js new file mode 100644 index 00000000000..4f0f9ef8d62 --- /dev/null +++ b/spec/javascripts/issuable_suggestions/mock_data.js @@ -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`, + }, +}); diff --git a/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb b/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb new file mode 100644 index 00000000000..4609593ef6a --- /dev/null +++ b/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb @@ -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 diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb new file mode 100644 index 00000000000..355336ad7e2 --- /dev/null +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -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 diff --git a/yarn.lock b/yarn.lock index 63a8913fa3d..9872c85c13d 100644 --- a/yarn.lock +++ b/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" @@ -698,6 +703,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" @@ -996,6 +1006,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" @@ -3992,6 +4099,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" @@ -4288,6 +4414,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" @@ -4835,6 +4966,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" @@ -5982,6 +6118,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" @@ -7638,6 +7781,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" @@ -7715,6 +7863,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" @@ -8166,6 +8319,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" @@ -8584,3 +8745,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== -- cgit v1.2.1