diff options
36 files changed, 1185 insertions, 1 deletions
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 @@ +<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> 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 @@ +<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> 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'), @@ -101,6 +101,11 @@ module.exports = { strictExportPresence: true, rules: [ { + type: 'javascript/auto', + test: /\.mjs$/, + use: [], + }, + { test: /\.js$/, exclude: path => /node_modules|vendor[\\/]assets/.test(path) && !/\.vue\.js/.test(path), loader: 'babel-loader', @@ -122,6 +127,11 @@ module.exports = { }, }, { + 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..7bb8e26b81a --- /dev/null +++ b/spec/javascripts/issuable_suggestions/components/app_spec.js @@ -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); + }); + }); +}); 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..7bd1fe678f4 --- /dev/null +++ b/spec/javascripts/issuable_suggestions/components/item_spec.js @@ -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'); + }); + }); +}); 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== |