diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-25 18:12:32 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-25 18:12:32 +0300 |
commit | 7d8d5a3dab415672a41ab29c3bfa9581f275dc50 (patch) | |
tree | 7b9249d8ca8c12ad899b4e6d968193d58e63f458 /app | |
parent | 868c8c35fbddd439f4df76a5954e2a1caa2af3cc (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/issues/show/components/description.vue | 118 | ||||
-rw-r--r-- | app/assets/javascripts/sidebar/graphql.js | 3 | ||||
-rw-r--r-- | app/assets/javascripts/work_items/pages/create_work_item.vue | 46 | ||||
-rw-r--r-- | app/assets/stylesheets/pages/issues.scss | 29 | ||||
-rw-r--r-- | app/controllers/projects/issues_controller.rb | 1 | ||||
-rw-r--r-- | app/controllers/registrations_controller.rb | 9 | ||||
-rw-r--r-- | app/graphql/types/ci/runner_status_enum.rb | 12 | ||||
-rw-r--r-- | app/graphql/types/ci/runner_type.rb | 5 | ||||
-rw-r--r-- | app/models/ci/namespace_mirror.rb | 4 | ||||
-rw-r--r-- | app/models/ci/pipeline.rb | 6 | ||||
-rw-r--r-- | app/models/ci/runner.rb | 2 | ||||
-rw-r--r-- | app/models/project.rb | 4 | ||||
-rw-r--r-- | app/presenters/ci/runner_presenter.rb | 4 | ||||
-rw-r--r-- | app/services/groups/create_service.rb | 11 | ||||
-rw-r--r-- | app/views/layouts/_page.html.haml | 1 | ||||
-rw-r--r-- | app/views/layouts/header/_default.html.haml | 6 |
16 files changed, 239 insertions, 22 deletions
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue index 7be4c13f544..3f42f825866 100644 --- a/app/assets/javascripts/issues/show/components/description.vue +++ b/app/assets/javascripts/issues/show/components/description.vue @@ -1,18 +1,31 @@ <script> -import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; +import { + GlSafeHtmlDirective as SafeHtml, + GlModal, + GlModalDirective, + GlPopover, + GlButton, +} from '@gitlab/ui'; import $ from 'jquery'; import createFlash from '~/flash'; import { __, sprintf } from '~/locale'; import TaskList from '~/task_list'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import CreateWorkItem from '~/work_items/pages/create_work_item.vue'; import animateMixin from '../mixins/animate'; export default { directives: { SafeHtml, + GlModal: GlModalDirective, }, - - mixins: [animateMixin], - + components: { + GlModal, + GlPopover, + CreateWorkItem, + GlButton, + }, + mixins: [animateMixin, glFeatureFlagMixin()], props: { canUpdate: { type: Boolean, @@ -53,8 +66,15 @@ export default { preAnimation: false, pulseAnimation: false, initialUpdate: true, + taskButtons: [], + activeTask: {}, }; }, + computed: { + workItemsEnabled() { + return this.glFeatures.workItems; + }, + }, watch: { descriptionHtml(newDescription, oldDescription) { if (!this.initialUpdate && newDescription !== oldDescription) { @@ -74,6 +94,10 @@ export default { mounted() { this.renderGFM(); this.updateTaskStatusText(); + + if (this.workItemsEnabled) { + this.renderTaskActions(); + } }, methods: { renderGFM() { @@ -132,6 +156,55 @@ export default { $tasksShort.text(''); } }, + renderTaskActions() { + const taskListFields = this.$el.querySelectorAll('.task-list-item'); + taskListFields.forEach((item, index) => { + const button = document.createElement('button'); + button.classList.add( + 'btn', + 'btn-default', + 'btn-md', + 'gl-button', + 'btn-default-tertiary', + 'gl-left-0', + 'gl-p-0!', + 'gl-top-2', + 'gl-absolute', + 'js-add-task', + ); + button.id = `js-task-button-${index}`; + this.taskButtons.push(button.id); + button.innerHTML = + '<svg data-testid="ellipsis_v-icon" role="img" aria-hidden="true" class="dropdown-icon gl-icon s14"><use href="/assets/icons-7f1680a3670112fe4c8ef57b9dfb93f0f61b43a2a479d7abd6c83bcb724b9201.svg#ellipsis_v"></use></svg>'; + item.prepend(button); + }); + }, + openCreateTaskModal(id) { + this.activeTask = { id, title: this.$el.querySelector(`#${id}`).parentElement.innerText }; + this.$refs.modal.show(); + }, + closeCreateTaskModal() { + this.$refs.modal.hide(); + }, + handleCreateTask(title) { + const listItem = this.$el.querySelector(`#${this.activeTask.id}`).parentElement; + const taskBadge = document.createElement('span'); + taskBadge.innerHTML = ` + <svg data-testid="issue-open-m-icon" role="img" aria-hidden="true" class="gl-icon gl-fill-green-500 s12"> + <use href="/assets/icons-7f1680a3670112fe4c8ef57b9dfb93f0f61b43a2a479d7abd6c83bcb724b9201.svg#issue-open-m"></use> + </svg> + <span class="badge badge-info badge-pill gl-badge sm gl-mr-1"> + ${__('Task')} + </span> + <a href="#">${title}</a> + `; + listItem.insertBefore(taskBadge, listItem.lastChild); + listItem.removeChild(listItem.lastChild); + this.closeCreateTaskModal(); + }, + focusButton() { + this.$refs.convertButton[0].$el.focus(); + }, }, safeHtmlConfig: { ADD_TAGS: ['gl-emoji', 'copy-code'] }, }; @@ -142,12 +215,14 @@ export default { v-if="descriptionHtml" :class="{ 'js-task-list-container': canUpdate, + 'work-items-enabled': workItemsEnabled, }" class="description" > <div ref="gfm-content" v-safe-html:[$options.safeHtmlConfig]="descriptionHtml" + data-testid="gfm-content" :class="{ 'issue-realtime-pre-pulse': preAnimation, 'issue-realtime-trigger-pulse': pulseAnimation, @@ -157,13 +232,46 @@ export default { <!-- eslint-disable vue/no-mutating-props --> <textarea v-if="descriptionText" - ref="textarea" v-model="descriptionText" :data-update-url="updateUrl" class="hidden js-task-list-field" dir="auto" + data-testid="textarea" > </textarea> <!-- eslint-enable vue/no-mutating-props --> + <gl-modal + ref="modal" + modal-id="create-task-modal" + :title="s__('WorkItem|New Task')" + hide-footer + body-class="gl-py-0!" + > + <create-work-item + :is-modal="true" + :initial-title="activeTask.title" + @closeModal="closeCreateTaskModal" + @onCreate="handleCreateTask" + /> + </gl-modal> + <template v-if="workItemsEnabled"> + <gl-popover + v-for="item in taskButtons" + :key="item" + :target="item" + placement="top" + triggers="focus" + @shown="focusButton" + > + <gl-button + ref="convertButton" + variant="link" + data-testid="convert-to-task" + class="gl-text-gray-900! gl-text-decoration-none! gl-outline-0!" + @click="openCreateTaskModal(item)" + >{{ s__('WorkItem|Convert to work item') }}</gl-button + > + </gl-popover> + </template> </div> </template> diff --git a/app/assets/javascripts/sidebar/graphql.js b/app/assets/javascripts/sidebar/graphql.js index 5b2ce3fe446..c5d94cfa5e8 100644 --- a/app/assets/javascripts/sidebar/graphql.js +++ b/app/assets/javascripts/sidebar/graphql.js @@ -2,6 +2,7 @@ import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; import produce from 'immer'; import VueApollo from 'vue-apollo'; import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql'; +import { resolvers as workItemResolvers } from '~/work_items/graphql/resolvers'; import createDefaultClient from '~/lib/graphql'; import introspectionQueryResultData from './fragmentTypes.json'; @@ -10,6 +11,7 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({ }); const resolvers = { + ...workItemResolvers, Mutation: { updateIssueState: (_, { issueType = undefined, isDirty = false }, { cache }) => { const sourceData = cache.readQuery({ query: getIssueStateQuery }); @@ -18,6 +20,7 @@ const resolvers = { }); cache.writeQuery({ query: getIssueStateQuery, data }); }, + ...workItemResolvers.Mutation, }, }; diff --git a/app/assets/javascripts/work_items/pages/create_work_item.vue b/app/assets/javascripts/work_items/pages/create_work_item.vue index 12bad5606d4..2b9db3e3db5 100644 --- a/app/assets/javascripts/work_items/pages/create_work_item.vue +++ b/app/assets/javascripts/work_items/pages/create_work_item.vue @@ -10,9 +10,21 @@ export default { GlAlert, ItemTitle, }, + props: { + isModal: { + type: Boolean, + required: false, + default: false, + }, + initialTitle: { + type: String, + required: false, + default: '', + }, + }, data() { return { - title: '', + title: this.initialTitle, error: false, }; }, @@ -35,7 +47,11 @@ export default { }, }, } = response; - this.$router.push({ name: 'workItem', params: { id } }); + if (!this.isModal) { + this.$router.push({ name: 'workItem', params: { id } }); + } else { + this.$emit('onCreate', this.title); + } } catch { this.error = true; } @@ -43,6 +59,13 @@ export default { handleTitleInput(title) { this.title = title; }, + handleCancelClick() { + if (!this.isModal) { + this.$router.go(-1); + return; + } + this.$emit('closeModal'); + }, }, }; </script> @@ -52,18 +75,27 @@ export default { <gl-alert v-if="error" variant="danger" @dismiss="error = false">{{ __('Something went wrong when creating a work item. Please try again') }}</gl-alert> - <item-title data-testid="title-input" @title-input="handleTitleInput" /> - <div class="gl-bg-gray-10 gl-py-5 gl-px-6"> + <item-title :initial-title="title" data-testid="title-input" @title-input="handleTitleInput" /> + <div + class="gl-bg-gray-10 gl-py-5 gl-px-6" + :class="{ 'gl-display-flex gl-justify-content-end': isModal }" + > <gl-button variant="confirm" :disabled="title.length === 0" - class="gl-mr-3" + :class="{ 'gl-mr-3': !isModal }" data-testid="create-button" type="submit" > - {{ __('Create') }} + {{ s__('WorkItem|Create work item') }} </gl-button> - <gl-button type="button" data-testid="cancel-button" @click="$router.go(-1)"> + <gl-button + type="button" + data-testid="cancel-button" + class="gl-order-n1" + :class="{ 'gl-mr-3': isModal }" + @click="handleCancelClick" + > {{ __('Cancel') }} </gl-button> </div> diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index d77c8a40a79..6411d5e1000 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -305,3 +305,32 @@ ul.related-merge-requests > li gl-emoji { .issuable-header-slide-leave-to { transform: translateY(-100%); } + +.description.work-items-enabled { + ul.task-list { + > li.task-list-item { + padding-inline-start: 2.25rem; + + .js-add-task { + svg { + visibility: hidden; + } + + &:focus svg { + visibility: visible; + } + } + + > input.task-list-item-checkbox { + left: 0.875rem; + } + + &:hover, + &:focus-within { + .js-add-task svg { + visibility: visible; + } + } + } + } +} diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 785fbdaa611..8b5e9fa8bb9 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -54,6 +54,7 @@ class Projects::IssuesController < Projects::ApplicationController push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml) push_frontend_feature_flag(:paginated_issue_discussions, @project, default_enabled: :yaml) push_frontend_feature_flag(:fix_comment_scroll, @project, default_enabled: :yaml) + push_frontend_feature_flag(:work_items, project, default_enabled: :yaml) end around_action :allow_gitaly_ref_name_caching, only: [:discussions] diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index c1765d367d1..7b688c0ccc2 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -35,6 +35,7 @@ class RegistrationsController < Devise::RegistrationsController persist_accepted_terms_if_required(new_user) set_role_required(new_user) + track_experiment_event(new_user) if pending_approval? NotificationService.new.new_instance_access_request(new_user) @@ -223,6 +224,14 @@ class RegistrationsController < Devise::RegistrationsController def context_user current_user end + + def track_experiment_event(new_user) + # Track signed up event to relate it with click "Sign up" button events from + # the experimental logged out header with marketing links. This allows us to + # have a funnel of visitors clicking on the header and those visitors + # signing up and becoming users + experiment(:logged_out_marketing_header, actor: new_user).track(:signed_up) if new_user.persisted? + end end RegistrationsController.prepend_mod_with('RegistrationsController') diff --git a/app/graphql/types/ci/runner_status_enum.rb b/app/graphql/types/ci/runner_status_enum.rb index dd056191ceb..2e65e2d4e1e 100644 --- a/app/graphql/types/ci/runner_status_enum.rb +++ b/app/graphql/types/ci/runner_status_enum.rb @@ -7,12 +7,20 @@ module Types value 'ACTIVE', description: 'Runner that is not paused.', - deprecated: { reason: 'Use CiRunnerType.active instead', milestone: '14.6' }, + deprecated: { + reason: :renamed, + replacement: 'CiRunner.paused', + milestone: '14.6' + }, value: :active value 'PAUSED', description: 'Runner that is paused.', - deprecated: { reason: 'Use CiRunnerType.active instead', milestone: '14.6' }, + deprecated: { + reason: :renamed, + replacement: 'CiRunner.paused', + milestone: '14.6' + }, value: :paused value 'ONLINE', diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb index f4a3379c5ca..e3f04ec5814 100644 --- a/app/graphql/types/ci/runner_type.rb +++ b/app/graphql/types/ci/runner_type.rb @@ -29,7 +29,10 @@ module Types field :access_level, ::Types::Ci::RunnerAccessLevelEnum, null: false, description: 'Access level of the runner.' field :active, GraphQL::Types::Boolean, null: false, - description: 'Indicates the runner is allowed to receive jobs.' + description: 'Indicates the runner is allowed to receive jobs.', + deprecated: { reason: 'Use paused', milestone: '14.8' } + field :paused, GraphQL::Types::Boolean, null: false, + description: 'Indicates the runner is paused and not available to run jobs.' field :status, Types::Ci::RunnerStatusEnum, null: false, diff --git a/app/models/ci/namespace_mirror.rb b/app/models/ci/namespace_mirror.rb index ce3faf3546b..d5cbbb96134 100644 --- a/app/models/ci/namespace_mirror.rb +++ b/app/models/ci/namespace_mirror.rb @@ -6,7 +6,7 @@ module Ci class NamespaceMirror < ApplicationRecord belongs_to :namespace - scope :contains_namespace, -> (id) do + scope :by_group_and_descendants, -> (id) do where('traversal_ids @> ARRAY[?]::int[]', id) end @@ -32,7 +32,7 @@ module Ci private def sync_children_namespaces!(namespace_id, traversal_ids) - contains_namespace(namespace_id) + by_group_and_descendants(namespace_id) .where.not(namespace_id: namespace_id) .update_all( "traversal_ids = ARRAY[#{sanitize_sql(traversal_ids.join(','))}]::int[] || traversal_ids[array_position(traversal_ids, #{sanitize_sql(namespace_id)}) + 1:]" diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 0f09b1b8996..032959c385e 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -1163,7 +1163,11 @@ module Ci end def merge_request? - merge_request_id.present? + if Feature.enabled?(:ci_pipeline_merge_request_presence_check, default_enabled: :yaml) + merge_request_id.present? && merge_request + else + merge_request_id.present? + end end def external_pull_request? diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index e7bc6bffcaf..fc13ebec7c2 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -101,7 +101,7 @@ module Ci } scope :belonging_to_group_or_project_descendants, -> (group_id) { - group_ids = Ci::NamespaceMirror.contains_namespace(group_id).select(:namespace_id) + group_ids = Ci::NamespaceMirror.by_group_and_descendants(group_id).select(:namespace_id) project_ids = Ci::ProjectMirror.by_namespace_id(group_ids).select(:project_id) group_runners = joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: group_ids }) diff --git a/app/models/project.rb b/app/models/project.rb index 1070bb6db4f..27ec475cc8b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1519,6 +1519,10 @@ class Project < ApplicationRecord group || namespace.try(:owner) end + def owners + Array.wrap(owner) + end + def first_owner obj = owner diff --git a/app/presenters/ci/runner_presenter.rb b/app/presenters/ci/runner_presenter.rb index ffd826fab64..482534f27b9 100644 --- a/app/presenters/ci/runner_presenter.rb +++ b/app/presenters/ci/runner_presenter.rb @@ -15,5 +15,9 @@ module Ci def executor_name Ci::Runner::EXECUTOR_TYPE_TO_NAMES[executor_type&.to_sym] end + + def paused + !active + end end end diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index da3cebc2e6d..67cbbaf84f6 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -61,6 +61,8 @@ module Groups delay = Namespaces::InviteTeamEmailService::DELIVERY_DELAY_IN_MINUTES Namespaces::InviteTeamEmailWorker.perform_in(delay, group.id, current_user.id) end + + track_experiment_event end def remove_unallowed_params @@ -112,6 +114,15 @@ module Groups @group.shared_runners_enabled = @group.parent.shared_runners_enabled @group.allow_descendants_override_disabled_shared_runners = @group.parent.allow_descendants_override_disabled_shared_runners end + + def track_experiment_event + return unless group.persisted? + + # Track namespace created events to relate them with signed up events for + # the same experiment. This will let us associate created namespaces to + # users that signed up from the experimental logged out header. + experiment(:logged_out_marketing_header, actor: current_user).track(:namespace_created, namespace: group) + end end end diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index d6557772241..b7299df1bc1 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -16,6 +16,7 @@ = render "shared/service_ping_consent" = render_two_factor_auth_recovery_settings_check = render_if_exists "layouts/header/ee_subscribable_banner" + = render_if_exists "layouts/header/seats_count_alert" = render_if_exists "shared/namespace_storage_limit_alert" = render_if_exists "shared/namespace_user_cap_reached_alert" = render_if_exists "shared/new_user_signups_cap_reached_alert" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index d0a06c7d5bf..246a31f86c9 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -139,15 +139,15 @@ - experiment(:logged_out_marketing_header, actor: nil) do |e| - e.candidate do %li.nav-item.gl-display-none.gl-sm-display-block - = link_to _('Sign up now'), new_user_registration_path, class: 'gl-button btn btn-default btn-sign-in' + = link_to _('Sign up now'), new_user_registration_path, class: 'gl-button btn btn-default btn-sign-in', data: { track_action: 'click_button', track_experiment: e.name, track_label: 'sign_up_now' } %li.nav-item.gl-display-none.gl-sm-display-block = link_to _('Login'), new_session_path(:user, redirect_to_referer: 'yes') = render 'layouts/header/sign_in_register_button', class: 'gl-sm-display-none' - e.try(:trial_focused) do %li.nav-item.gl-display-none.gl-sm-display-block - = link_to _('Get a free trial'), 'https://about.gitlab.com/free-trial/', class: 'gl-button btn btn-default btn-sign-in' + = link_to _('Get a free trial'), 'https://about.gitlab.com/free-trial/', class: 'gl-button btn btn-default btn-sign-in', data: { track_action: 'click_button', track_experiment: e.name, track_label: 'get_a_free_trial' } %li.nav-item.gl-display-none.gl-sm-display-block - = link_to _('Sign up'), new_user_registration_path + = link_to _('Sign up'), new_user_registration_path, data: { track_action: 'click_button', track_experiment: e.name, track_label: 'sign_up' } %li.nav-item.gl-display-none.gl-sm-display-block = link_to _('Login'), new_session_path(:user, redirect_to_referer: 'yes') = render 'layouts/header/sign_in_register_button', class: 'gl-sm-display-none' |