Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-01-25 18:12:32 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-01-25 18:12:32 +0300
commit7d8d5a3dab415672a41ab29c3bfa9581f275dc50 (patch)
tree7b9249d8ca8c12ad899b4e6d968193d58e63f458 /app
parent868c8c35fbddd439f4df76a5954e2a1caa2af3cc (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue118
-rw-r--r--app/assets/javascripts/sidebar/graphql.js3
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue46
-rw-r--r--app/assets/stylesheets/pages/issues.scss29
-rw-r--r--app/controllers/projects/issues_controller.rb1
-rw-r--r--app/controllers/registrations_controller.rb9
-rw-r--r--app/graphql/types/ci/runner_status_enum.rb12
-rw-r--r--app/graphql/types/ci/runner_type.rb5
-rw-r--r--app/models/ci/namespace_mirror.rb4
-rw-r--r--app/models/ci/pipeline.rb6
-rw-r--r--app/models/ci/runner.rb2
-rw-r--r--app/models/project.rb4
-rw-r--r--app/presenters/ci/runner_presenter.rb4
-rw-r--r--app/services/groups/create_service.rb11
-rw-r--r--app/views/layouts/_page.html.haml1
-rw-r--r--app/views/layouts/header/_default.html.haml6
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'