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
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue4
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue (renamed from app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue)39
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form_old.vue494
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue81
-rw-r--r--app/assets/javascripts/alerts_settings/services/index.js21
-rw-r--r--app/assets/javascripts/behaviors/select2.js33
-rw-r--r--app/assets/javascripts/blob/file_template_selector.js15
-rw-r--r--app/assets/javascripts/blob/template_selector.js11
-rw-r--r--app/assets/javascripts/boards/boards_util.js36
-rw-r--r--app/assets/javascripts/boards/components/board_assignee_dropdown.vue99
-rw-r--r--app/assets/javascripts/boards/components/board_column_new.vue1
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_list_header_new.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_list_new.vue101
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue16
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_editable_item.vue15
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue2
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue3
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue161
-rw-r--r--app/assets/javascripts/boards/queries/group_milestones.query.graphql17
-rw-r--r--app/assets/javascripts/boards/queries/issue.fragment.graphql4
-rw-r--r--app/assets/javascripts/boards/queries/issue_set_milestone.mutation.graphql12
-rw-r--r--app/assets/javascripts/boards/stores/actions.js31
-rw-r--r--app/assets/javascripts/clone_panel.js42
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js7
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue36
-rw-r--r--app/assets/javascripts/clusters/components/knative_domain_editor.vue9
-rw-r--r--app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue2
-rw-r--r--app/assets/javascripts/clusters/services/application_state_machine.js6
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js27
-rw-r--r--app/assets/javascripts/diffs/components/app.vue3
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue7
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue10
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue18
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue12
-rw-r--r--app/assets/javascripts/diffs/store/actions.js90
-rw-r--r--app/assets/javascripts/diffs/store/getters.js48
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js80
-rw-r--r--app/assets/javascripts/diffs/store/utils.js186
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue67
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue30
-rw-r--r--app/assets/javascripts/groups/index.js3
-rw-r--r--app/assets/javascripts/groups/members/components/app.vue4
-rw-r--r--app/assets/javascripts/groups/members/index.js8
-rw-r--r--app/assets/javascripts/groups/store/groups_store.js5
-rw-r--r--app/assets/javascripts/groups/store/utils.js27
-rw-r--r--app/assets/javascripts/groups_select.js178
-rw-r--r--app/assets/javascripts/ide/components/editor_mode_dropdown.vue71
-rw-r--r--app/assets/javascripts/ide/components/ide.vue19
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue16
-rw-r--r--app/assets/javascripts/ide/components/nav_dropdown.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue20
-rw-r--r--app/assets/javascripts/ide/components/terminal/session.vue18
-rw-r--r--app/assets/javascripts/ide/ide_router.js16
-rw-r--r--app/assets/javascripts/ide/index.js5
-rw-r--r--app/assets/javascripts/ide/stores/actions.js21
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js23
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js21
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js6
-rw-r--r--app/assets/javascripts/issuable/auto_width_dropdown_select.js14
-rw-r--r--app/assets/javascripts/issuable_context.js14
-rw-r--r--app/assets/javascripts/issuable_form.js64
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_item.vue180
-rw-r--r--app/assets/javascripts/issue_show/utils/parse_data.js2
-rw-r--r--app/assets/javascripts/jira_connect/components/app.vue4
-rw-r--r--app/assets/javascripts/jobs/utils.js14
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js45
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js8
-rw-r--r--app/assets/javascripts/lib/utils/scroll_utils.js2
-rw-r--r--app/assets/javascripts/logs/utils.js2
-rw-r--r--app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/access_request_action_buttons.vue)0
-rw-r--r--app/assets/javascripts/members/components/action_buttons/action_button_group.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/action_button_group.vue)0
-rw-r--r--app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/approve_access_request_button.vue)0
-rw-r--r--app/assets/javascripts/members/components/action_buttons/group_action_buttons.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/group_action_buttons.vue)0
-rw-r--r--app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/invite_action_buttons.vue)0
-rw-r--r--app/assets/javascripts/members/components/action_buttons/leave_button.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/leave_button.vue)2
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/remove_group_link_button.vue)0
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_member_button.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/remove_member_button.vue)0
-rw-r--r--app/assets/javascripts/members/components/action_buttons/resend_invite_button.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/resend_invite_button.vue)0
-rw-r--r--app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/user_action_buttons.vue)2
-rw-r--r--app/assets/javascripts/members/components/avatars/group_avatar.vue (renamed from app/assets/javascripts/vue_shared/components/members/avatars/group_avatar.vue)2
-rw-r--r--app/assets/javascripts/members/components/avatars/invite_avatar.vue (renamed from app/assets/javascripts/vue_shared/components/members/avatars/invite_avatar.vue)2
-rw-r--r--app/assets/javascripts/members/components/avatars/user_avatar.vue (renamed from app/assets/javascripts/vue_shared/components/members/avatars/user_avatar.vue)4
-rw-r--r--app/assets/javascripts/members/components/modals/leave_modal.vue (renamed from app/assets/javascripts/vue_shared/components/members/modals/leave_modal.vue)2
-rw-r--r--app/assets/javascripts/members/components/modals/remove_group_link_modal.vue (renamed from app/assets/javascripts/vue_shared/components/members/modals/remove_group_link_modal.vue)2
-rw-r--r--app/assets/javascripts/members/components/table/created_at.vue (renamed from app/assets/javascripts/vue_shared/components/members/table/created_at.vue)0
-rw-r--r--app/assets/javascripts/members/components/table/expiration_datepicker.vue (renamed from app/assets/javascripts/vue_shared/components/members/table/expiration_datepicker.vue)0
-rw-r--r--app/assets/javascripts/members/components/table/expires_at.vue (renamed from app/assets/javascripts/vue_shared/components/members/table/expires_at.vue)2
-rw-r--r--app/assets/javascripts/members/components/table/member_action_buttons.vue (renamed from app/assets/javascripts/vue_shared/components/members/table/member_action_buttons.vue)2
-rw-r--r--app/assets/javascripts/members/components/table/member_avatar.vue (renamed from app/assets/javascripts/vue_shared/components/members/table/member_avatar.vue)0
-rw-r--r--app/assets/javascripts/members/components/table/member_source.vue (renamed from app/assets/javascripts/vue_shared/components/members/table/member_source.vue)0
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue (renamed from app/assets/javascripts/vue_shared/components/members/table/members_table.vue)15
-rw-r--r--app/assets/javascripts/members/components/table/members_table_cell.vue (renamed from app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue)11
-rw-r--r--app/assets/javascripts/members/components/table/role_dropdown.vue (renamed from app/assets/javascripts/vue_shared/components/members/table/role_dropdown.vue)3
-rw-r--r--app/assets/javascripts/members/constants.js (renamed from app/assets/javascripts/vue_shared/components/members/constants.js)0
-rw-r--r--app/assets/javascripts/members/store/actions.js (renamed from app/assets/javascripts/vuex_shared/modules/members/actions.js)0
-rw-r--r--app/assets/javascripts/members/store/index.js9
-rw-r--r--app/assets/javascripts/members/store/mutation_types.js (renamed from app/assets/javascripts/vuex_shared/modules/members/mutation_types.js)0
-rw-r--r--app/assets/javascripts/members/store/mutations.js (renamed from app/assets/javascripts/vuex_shared/modules/members/mutations.js)0
-rw-r--r--app/assets/javascripts/members/store/state.js (renamed from app/assets/javascripts/vuex_shared/modules/members/state.js)0
-rw-r--r--app/assets/javascripts/members/store/utils.js (renamed from app/assets/javascripts/vuex_shared/modules/members/utils.js)0
-rw-r--r--app/assets/javascripts/members/utils.js (renamed from app/assets/javascripts/vue_shared/components/members/utils.js)0
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js1
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/variable_mapping.js2
-rw-r--r--app/assets/javascripts/monitoring/utils.js2
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue9
-rw-r--r--app/assets/javascripts/notes/mixins/discussion_navigation.js9
-rw-r--r--app/assets/javascripts/notes/stores/actions.js19
-rw-r--r--app/assets/javascripts/notes/stores/collapse_utils.js3
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js4
-rw-r--r--app/assets/javascripts/packages/list/constants.js4
-rw-r--r--app/assets/javascripts/packages/shared/constants.js1
-rw-r--r--app/assets/javascripts/packages/shared/utils.js3
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js28
-rw-r--r--app/assets/javascripts/pages/projects/commit/pipelines/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js52
-rw-r--r--app/assets/javascripts/pages/projects/commits/show/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/new/index.js52
-rw-r--r--app/assets/javascripts/pages/projects/project.js40
-rw-r--r--app/assets/javascripts/pages/projects/settings/access_tokens/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js2
-rw-r--r--app/assets/javascripts/performance/constants.js21
-rw-r--r--app/assets/javascripts/persistent_user_callouts.js1
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue139
-rw-r--r--app/assets/javascripts/pipeline_editor/components/text_editor.vue14
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql26
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js12
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue201
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue26
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/actions.js1
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/getters.js7
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js1
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/mutations.js8
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/state.js4
-rw-r--r--app/assets/javascripts/project_select.js196
-rw-r--r--app/assets/javascripts/project_select_combo_button.js12
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue6
-rw-r--r--app/assets/javascripts/registry/explorer/pages/index.vue4
-rw-r--r--app/assets/javascripts/registry/settings/components/expiration_dropdown.vue50
-rw-r--r--app/assets/javascripts/registry/settings/components/expiration_run_text.vue31
-rw-r--r--app/assets/javascripts/registry/settings/components/expiration_textarea.vue109
-rw-r--r--app/assets/javascripts/registry/settings/components/expiration_toggle.vue55
-rw-r--r--app/assets/javascripts/registry/settings/constants.js45
-rw-r--r--app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql1
-rw-r--r--app/assets/javascripts/registry/settings/registry_settings_bundle.js13
-rw-r--r--app/assets/javascripts/reports/components/report_section.vue8
-rw-r--r--app/assets/javascripts/reports/constants.js14
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue14
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue14
-rw-r--r--app/assets/javascripts/users_select/index.js172
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue20
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue166
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue20
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/awards_list.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue90
-rw-r--r--app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js142
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_mentions.vue238
-rw-r--r--app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/pikaday.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/select2_select.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue3
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/components/constants.js8
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue59
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue150
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/constants.js7
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/getters.js66
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/index.js16
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/messages.js4
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/state.js5
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/utils.js78
-rw-r--r--app/assets/javascripts/vuex_shared/modules/members/index.js10
-rw-r--r--app/assets/javascripts/vulnerabilities/constants.js15
192 files changed, 3265 insertions, 2211 deletions
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
index 12c0409629f..cf16750dbf8 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
@@ -11,7 +11,6 @@ import {
GlSprintf,
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import Tracking from '~/tracking';
import {
trackAlertIntegrationsViewsOptions,
@@ -54,7 +53,6 @@ export default {
GlTooltip: GlTooltipDirective,
GlModal: GlModalDirective,
},
- mixins: [glFeatureFlagsMixin()],
props: {
integrations: {
type: Array,
@@ -170,7 +168,7 @@ export default {
</template>
<template #cell(actions)="{ item }">
- <gl-button-group v-if="glFeatures.httpIntegrationsList" class="gl-ml-3">
+ <gl-button-group class="gl-ml-3">
<gl-button icon="pencil" @click="$emit('edit-integration', { id: item.id })" />
<gl-button
v-gl-modal.deleteIntegration
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
index 3656fc4d7ec..bf2874b6cc7 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -216,8 +216,12 @@ export default {
return {
name: this.currentIntegration?.name || '',
active: this.currentIntegration?.active || false,
- token: this.currentIntegration?.token || this.selectedIntegrationType.token,
- url: this.currentIntegration?.url || this.selectedIntegrationType.url,
+ token:
+ this.currentIntegration?.token ||
+ (this.selectedIntegrationType !== this.generic ? this.selectedIntegrationType.token : ''),
+ url:
+ this.currentIntegration?.url ||
+ (this.selectedIntegrationType !== this.generic ? this.selectedIntegrationType.url : ''),
apiUrl: this.currentIntegration?.apiUrl || '',
};
},
@@ -246,8 +250,20 @@ export default {
canEditPayload() {
return this.hasSamplePayload && !this.resetSamplePayloadConfirmed;
},
+ isResetAuthKeyDisabled() {
+ return !this.active && !this.integrationForm.token !== '';
+ },
isPayloadEditDisabled() {
- return !this.active || this.canEditPayload;
+ return this.glFeatures.multipleHttpIntegrationsCustomMapping
+ ? !this.active || this.canEditPayload
+ : !this.active;
+ },
+ isSubmitTestPayloadDisabled() {
+ return (
+ !this.active ||
+ Boolean(this.integrationTestPayload.error) ||
+ this.integrationTestPayload.json === ''
+ );
},
},
watch: {
@@ -257,7 +273,7 @@ export default {
}
this.selectedIntegration = val.type;
this.active = val.active;
- if (val.type === typeSet.http) this.getIntegrationMapping(val.id);
+ if (val.type === typeSet.http && this.showMappingBuilder) this.getIntegrationMapping(val.id);
return this.integrationTypeSelect();
},
},
@@ -297,14 +313,8 @@ export default {
});
},
submitWithTestPayload() {
- return service
- .updateTestAlert(this.testAlertPayload)
- .then(() => {
- this.submit();
- })
- .catch(() => {
- this.$emit('test-payload-failure');
- });
+ this.$emit('set-test-alert-payload', this.testAlertPayload);
+ this.submit();
},
submit() {
// TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657
@@ -323,6 +333,7 @@ export default {
return this.$emit('update-integration', integrationPayload);
}
+ this.reset();
return this.$emit('create-new-integration', integrationPayload);
},
reset() {
@@ -539,7 +550,7 @@ export default {
</template>
</gl-form-input-group>
- <gl-button v-gl-modal.authKeyModal :disabled="!active">
+ <gl-button v-gl-modal.authKeyModal :disabled="isResetAuthKeyDisabled">
{{ $options.i18n.integrationFormSteps.step3.reset }}
</gl-button>
<gl-modal
@@ -642,7 +653,7 @@ export default {
<gl-button
v-if="!isManagingOpsgenie"
data-testid="integration-test-and-submit"
- :disabled="Boolean(integrationTestPayload.error)"
+ :disabled="isSubmitTestPayloadDisabled"
category="secondary"
variant="success"
class="gl-mx-3 js-no-auto-disable"
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_old.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_old.vue
deleted file mode 100644
index 0246315bdc5..00000000000
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_old.vue
+++ /dev/null
@@ -1,494 +0,0 @@
-<script>
-import {
- GlAlert,
- GlButton,
- GlForm,
- GlFormGroup,
- GlFormInput,
- GlFormInputGroup,
- GlFormTextarea,
- GlLink,
- GlModal,
- GlModalDirective,
- GlSprintf,
- GlFormSelect,
-} from '@gitlab/ui';
-import { debounce } from 'lodash';
-import { doesHashExistInUrl } from '~/lib/utils/url_utility';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import ToggleButton from '~/vue_shared/components/toggle_button.vue';
-import csrf from '~/lib/utils/csrf';
-import service from '../services';
-import {
- i18n,
- integrationTypes,
- JSON_VALIDATE_DELAY,
- targetPrometheusUrlPlaceholder,
- targetOpsgenieUrlPlaceholder,
- sectionHash,
-} from '../constants';
-import createFlash, { FLASH_TYPES } from '~/flash';
-
-export default {
- i18n,
- csrf,
- targetOpsgenieUrlPlaceholder,
- targetPrometheusUrlPlaceholder,
- components: {
- GlAlert,
- GlButton,
- GlForm,
- GlFormGroup,
- GlFormInput,
- GlFormInputGroup,
- GlFormSelect,
- GlFormTextarea,
- GlLink,
- GlModal,
- GlSprintf,
- ClipboardButton,
- ToggleButton,
- },
- directives: {
- 'gl-modal': GlModalDirective,
- },
- inject: ['prometheus', 'generic', 'opsgenie'],
- data() {
- return {
- loading: false,
- selectedIntegration: integrationTypes[0].value,
- options: integrationTypes,
- active: false,
- token: '',
- targetUrl: '',
- feedback: {
- variant: 'danger',
- feedbackMessage: '',
- isFeedbackDismissed: false,
- },
- testAlert: {
- json: null,
- error: null,
- },
- canSaveForm: false,
- serverError: null,
- };
- },
- computed: {
- sections() {
- return [
- {
- text: this.$options.i18n.usageSection,
- url: this.generic.alertsUsageUrl,
- },
- {
- text: this.$options.i18n.setupSection,
- url: this.generic.alertsSetupUrl,
- },
- ];
- },
- isPrometheus() {
- return this.selectedIntegration === 'PROMETHEUS';
- },
- isOpsgenie() {
- return this.selectedIntegration === 'OPSGENIE';
- },
- selectedIntegrationType() {
- switch (this.selectedIntegration) {
- case 'HTTP': {
- return {
- url: this.generic.url,
- token: this.generic.token,
- active: this.generic.active,
- resetKey: this.resetKey.bind(this),
- };
- }
- case 'PROMETHEUS': {
- return {
- url: this.prometheus.url,
- token: this.prometheus.token,
- active: this.prometheus.active,
- resetKey: this.resetKey.bind(this, 'PROMETHEUS'),
- targetUrl: this.prometheus.prometheusApiUrl,
- };
- }
- case 'OPSGENIE': {
- return {
- targetUrl: this.opsgenie.opsgenieMvcTargetUrl,
- active: this.opsgenie.active,
- };
- }
- default: {
- return {};
- }
- }
- },
- showFeedbackMsg() {
- return this.feedback.feedbackMessage && !this.isFeedbackDismissed;
- },
- showAlertSave() {
- return (
- this.feedback.feedbackMessage === this.$options.i18n.testAlertFailed &&
- !this.isFeedbackDismissed
- );
- },
- prometheusInfo() {
- return this.isPrometheus ? this.$options.i18n.prometheusInfo : '';
- },
- jsonIsValid() {
- return this.testAlert.error === null;
- },
- canTestAlert() {
- return this.active && this.testAlert.json !== null;
- },
- canSaveConfig() {
- return !this.loading && this.canSaveForm;
- },
- baseUrlPlaceholder() {
- return this.isOpsgenie
- ? this.$options.targetOpsgenieUrlPlaceholder
- : this.$options.targetPrometheusUrlPlaceholder;
- },
- },
- watch: {
- 'testAlert.json': debounce(function debouncedJsonValidate() {
- this.validateJson();
- }, JSON_VALIDATE_DELAY),
- targetUrl(oldVal, newVal) {
- if (newVal && oldVal !== this.selectedIntegrationType.targetUrl) {
- this.canSaveForm = true;
- }
- },
- },
- mounted() {
- if (this.prometheus.active || this.generic.active || !this.opsgenie.opsgenieMvcIsAvailable) {
- this.removeOpsGenieOption();
- } else if (this.opsgenie.active) {
- this.setOpsgenieAsDefault();
- }
- this.active = this.selectedIntegrationType.active;
- this.token = this.selectedIntegrationType.token ?? '';
- },
- methods: {
- createUserErrorMessage(errors = {}) {
- const error = Object.entries(errors)?.[0];
- if (error) {
- const [field, [msg]] = error;
- this.serverError = `${field} ${msg}`;
- }
- },
- setOpsgenieAsDefault() {
- this.options = this.options.map(el => {
- if (el.value !== 'OPSGENIE') {
- return { ...el, disabled: true };
- }
- return { ...el, disabled: false };
- });
- this.selectedIntegration = this.options.find(({ value }) => value === 'OPSGENIE').value;
- if (this.targetUrl === null) {
- this.targetUrl = this.selectedIntegrationType.targetUrl;
- }
- },
- removeOpsGenieOption() {
- this.options = this.options.map(el => {
- if (el.value !== 'OPSGENIE') {
- return { ...el, disabled: false };
- }
- return { ...el, disabled: true };
- });
- },
- resetFormValues() {
- this.testAlert.json = null;
- this.targetUrl = this.selectedIntegrationType.targetUrl;
- this.active = this.selectedIntegrationType.active;
- },
- dismissFeedback() {
- this.serverError = null;
- this.feedback = { ...this.feedback, feedbackMessage: null };
- this.isFeedbackDismissed = false;
- },
- resetKey(key) {
- const fn = key === 'PROMETHEUS' ? this.resetPrometheusKey() : this.resetGenericKey();
-
- return fn
- .then(({ data: { token } }) => {
- this.token = token;
- this.setFeedback({ feedbackMessage: this.$options.i18n.tokenRest, variant: 'success' });
- })
- .catch(() => {
- this.setFeedback({ feedbackMessage: this.$options.i18n.errorKeyMsg, variant: 'danger' });
- });
- },
- resetGenericKey() {
- this.dismissFeedback();
- return service.updateGenericKey({
- endpoint: this.generic.formPath,
- params: { service: { token: '' } },
- });
- },
- resetPrometheusKey() {
- return service.updatePrometheusKey({ endpoint: this.prometheus.prometheusResetKeyPath });
- },
- toggleService(value) {
- this.canSaveForm = true;
- this.active = value;
- },
- toggle(value) {
- return this.isPrometheus ? this.togglePrometheusActive(value) : this.toggleActivated(value);
- },
- toggleActivated(value) {
- this.loading = true;
- const path = this.isOpsgenie ? this.opsgenie.formPath : this.generic.formPath;
- return service
- .updateGenericActive({
- endpoint: path,
- params: this.isOpsgenie
- ? { service: { opsgenie_mvc_target_url: this.targetUrl, opsgenie_mvc_enabled: value } }
- : { service: { active: value } },
- })
- .then(() => this.notifySuccessAndReload())
- .catch(({ response: { data: { errors } = {} } = {} }) => {
- this.createUserErrorMessage(errors);
- this.setFeedback({
- feedbackMessage: this.$options.i18n.errorMsg,
- variant: 'danger',
- });
- })
- .finally(() => {
- this.loading = false;
- this.canSaveForm = false;
- });
- },
- reload() {
- if (!doesHashExistInUrl(sectionHash)) {
- window.location.hash = sectionHash;
- }
- window.location.reload();
- },
- togglePrometheusActive(value) {
- this.loading = true;
- return service
- .updatePrometheusActive({
- endpoint: this.prometheus.prometheusFormPath,
- params: {
- token: this.$options.csrf.token,
- config: value,
- url: this.targetUrl,
- redirect: window.location,
- },
- })
- .then(() => this.notifySuccessAndReload())
- .catch(({ response: { data: { errors } = {} } = {} }) => {
- this.createUserErrorMessage(errors);
- this.setFeedback({
- feedbackMessage: this.$options.i18n.errorMsg,
- variant: 'danger',
- });
- })
- .finally(() => {
- this.loading = false;
- this.canSaveForm = false;
- });
- },
- notifySuccessAndReload() {
- createFlash({ message: this.$options.i18n.changesSaved, type: FLASH_TYPES.NOTICE });
- setTimeout(() => this.reload(), 1000);
- },
- setFeedback({ feedbackMessage, variant }) {
- this.feedback = { feedbackMessage, variant };
- },
- validateJson() {
- this.testAlert.error = null;
- try {
- JSON.parse(this.testAlert.json);
- } catch (e) {
- this.testAlert.error = JSON.stringify(e.message);
- }
- },
- validateTestAlert() {
- this.loading = true;
- this.dismissFeedback();
- this.validateJson();
- return service
- .updateTestAlert({
- endpoint: this.selectedIntegrationType.url,
- data: this.testAlert.json,
- token: this.selectedIntegrationType.token,
- })
- .then(() => {
- this.setFeedback({
- feedbackMessage: this.$options.i18n.testAlertSuccess,
- variant: 'success',
- });
- })
- .catch(() => {
- this.setFeedback({
- feedbackMessage: this.$options.i18n.testAlertFailed,
- variant: 'danger',
- });
- })
- .finally(() => {
- this.loading = false;
- });
- },
- onSubmit() {
- this.dismissFeedback();
- this.toggle(this.active);
- },
- onReset() {
- this.testAlert.json = null;
- this.dismissFeedback();
- this.targetUrl = this.selectedIntegrationType.targetUrl;
-
- if (this.canSaveForm) {
- this.canSaveForm = false;
- this.active = this.selectedIntegrationType.active;
- }
- },
- },
-};
-</script>
-
-<template>
- <gl-form @submit.prevent="onSubmit" @reset.prevent="onReset">
- <h5 class="gl-font-lg gl-my-5">{{ $options.i18n.integrationsLabel }}</h5>
-
- <gl-alert v-if="showFeedbackMsg" :variant="feedback.variant" @dismiss="dismissFeedback">
- {{ feedback.feedbackMessage }}
- <br />
- <i v-if="serverError">{{ __('Error message:') }} {{ serverError }}</i>
- <gl-button
- v-if="showAlertSave"
- variant="danger"
- category="primary"
- class="gl-display-block gl-mt-3"
- @click="toggle(active)"
- >
- {{ __('Save anyway') }}
- </gl-button>
- </gl-alert>
-
- <div data-testid="alert-settings-description">
- <p v-for="section in sections" :key="section.text">
- <gl-sprintf :message="section.text">
- <template #link="{ content }">
- <gl-link :href="section.url" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
-
- <gl-form-group label-for="integration-type" :label="$options.i18n.integration">
- <gl-form-select
- id="integration-type"
- v-model="selectedIntegration"
- :options="options"
- data-testid="alert-settings-select"
- @change="resetFormValues"
- />
- <span class="gl-text-gray-500">
- <gl-sprintf :message="$options.i18n.integrationsInfo">
- <template #link="{ content }">
- <gl-link
- class="gl-display-inline-block"
- href="https://gitlab.com/groups/gitlab-org/-/epics/4390"
- target="_blank"
- >{{ content }}</gl-link
- >
- </template>
- </gl-sprintf>
- </span>
- </gl-form-group>
- <gl-form-group :label="$options.i18n.activeLabel" label-for="active">
- <toggle-button
- id="active"
- :disabled-input="loading"
- :is-loading="loading"
- :value="active"
- @change="toggleService"
- />
- </gl-form-group>
- <gl-form-group
- v-if="isOpsgenie || isPrometheus"
- :label="$options.i18n.apiBaseUrlLabel"
- label-for="api-url"
- >
- <gl-form-input
- id="api-url"
- v-model="targetUrl"
- type="url"
- :placeholder="baseUrlPlaceholder"
- :disabled="!active"
- />
- <span class="gl-text-gray-500">
- {{ $options.i18n.apiBaseUrlHelpText }}
- </span>
- </gl-form-group>
- <template v-if="!isOpsgenie">
- <gl-form-group :label="$options.i18n.urlLabel" label-for="url">
- <gl-form-input-group id="url" readonly :value="selectedIntegrationType.url">
- <template #append>
- <clipboard-button
- :text="selectedIntegrationType.url"
- :title="$options.i18n.copyToClipboard"
- class="gl-m-0!"
- />
- </template>
- </gl-form-input-group>
- <span class="gl-text-gray-500">
- {{ prometheusInfo }}
- </span>
- </gl-form-group>
- <gl-form-group :label="$options.i18n.tokenLabel" label-for="authorization-key">
- <gl-form-input-group id="authorization-key" class="gl-mb-2" readonly :value="token">
- <template #append>
- <clipboard-button
- :text="token"
- :title="$options.i18n.copyToClipboard"
- class="gl-m-0!"
- />
- </template>
- </gl-form-input-group>
- <gl-button v-gl-modal.tokenModal :disabled="!active" class="gl-mt-3">{{
- $options.i18n.resetKey
- }}</gl-button>
- <gl-modal
- modal-id="tokenModal"
- :title="$options.i18n.resetKey"
- :ok-title="$options.i18n.resetKey"
- ok-variant="danger"
- @ok="selectedIntegrationType.resetKey"
- >
- {{ $options.i18n.restKeyInfo }}
- </gl-modal>
- </gl-form-group>
- <gl-form-group
- :label="$options.i18n.alertJson"
- label-for="alert-json"
- :invalid-feedback="testAlert.error"
- >
- <gl-form-textarea
- id="alert-json"
- v-model.trim="testAlert.json"
- :disabled="!active"
- :state="jsonIsValid"
- :placeholder="$options.i18n.alertJsonPlaceholder"
- rows="6"
- max-rows="10"
- />
- </gl-form-group>
-
- <gl-button :disabled="!canTestAlert" @click="validateTestAlert">{{
- $options.i18n.testAlertInfo
- }}</gl-button>
- </template>
- <div class="footer-block row-content-block gl-display-flex gl-justify-content-space-between">
- <gl-button variant="success" category="primary" :disabled="!canSaveConfig" @click="onSubmit">
- {{ __('Save changes') }}
- </gl-button>
- <gl-button category="primary" :disabled="!canSaveConfig" @click="onReset">
- {{ __('Cancel') }}
- </gl-button>
- </div>
- </gl-form>
-</template>
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
index 1ffc2f80148..1ac63fd04a1 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
@@ -1,7 +1,6 @@
<script>
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { fetchPolicies } from '~/lib/graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
import getIntegrationsQuery from '../graphql/queries/get_integrations.query.graphql';
@@ -15,8 +14,8 @@ import resetHttpTokenMutation from '../graphql/mutations/reset_http_token.mutati
import resetPrometheusTokenMutation from '../graphql/mutations/reset_prometheus_token.mutation.graphql';
import updateCurrentIntergrationMutation from '../graphql/mutations/update_current_intergration.mutation.graphql';
import IntegrationsList from './alerts_integrations_list.vue';
-import SettingsFormOld from './alerts_settings_form_old.vue';
-import SettingsFormNew from './alerts_settings_form_new.vue';
+import AlertSettingsForm from './alerts_settings_form.vue';
+import service from '../services';
import { typeSet } from '../constants';
import {
updateStoreAfterIntegrationDelete,
@@ -37,6 +36,9 @@ export default {
'AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list.',
),
integrationRemoved: s__('AlertsIntegrations|The integration has been successfully removed.'),
+ alertSent: s__(
+ 'AlertsIntegrations|The test alert has been successfully sent, and should now be visible on your alerts list.',
+ ),
},
components: {
// TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657
@@ -44,10 +46,8 @@ export default {
GlLink,
GlSprintf,
IntegrationsList,
- SettingsFormOld,
- SettingsFormNew,
+ AlertSettingsForm,
},
- mixins: [glFeatureFlagsMixin()],
inject: {
generic: {
default: {},
@@ -93,6 +93,7 @@ export default {
data() {
return {
isUpdating: false,
+ testAlertPayload: null,
integrations: {},
currentIntegration: null,
};
@@ -101,20 +102,6 @@ export default {
loading() {
return this.$apollo.queries.integrations.loading;
},
- integrationsOptionsOld() {
- return [
- {
- name: s__('AlertSettings|HTTP endpoint'),
- type: s__('AlertsIntegrations|HTTP endpoint'),
- active: this.generic.active,
- },
- {
- name: s__('AlertSettings|External Prometheus'),
- type: s__('AlertsIntegrations|Prometheus'),
- active: this.prometheus.active,
- },
- ];
- },
canAddIntegration() {
return this.multiIntegrations || this.integrations?.list?.length < 2;
},
@@ -149,6 +136,19 @@ export default {
if (error) {
return createFlash({ message: error });
}
+
+ if (this.testAlertPayload) {
+ const integration =
+ httpIntegrationCreate?.integration || prometheusIntegrationCreate?.integration;
+
+ const payload = {
+ ...this.testAlertPayload,
+ endpoint: integration.url,
+ token: integration.token,
+ };
+ return this.validateAlertPayload(payload);
+ }
+
return createFlash({
message: this.$options.i18n.changesSaved,
type: FLASH_TYPES.SUCCESS,
@@ -179,6 +179,13 @@ export default {
if (error) {
return createFlash({ message: error });
}
+
+ if (this.testAlertPayload) {
+ return this.validateAlertPayload();
+ }
+
+ this.clearCurrentIntegration();
+
return createFlash({
message: this.$options.i18n.changesSaved,
type: FLASH_TYPES.SUCCESS,
@@ -189,6 +196,7 @@ export default {
})
.finally(() => {
this.isUpdating = false;
+ this.testAlertPayload = null;
});
},
resetToken({ type, variables }) {
@@ -212,7 +220,13 @@ export default {
const integration =
httpIntegrationResetToken?.integration ||
prometheusIntegrationResetToken?.integration;
- this.currentIntegration = integration;
+
+ this.$apollo.mutate({
+ mutation: updateCurrentIntergrationMutation,
+ variables: {
+ ...integration,
+ },
+ });
return createFlash({
message: this.$options.i18n.changesSaved,
@@ -280,8 +294,21 @@ export default {
variables: {},
});
},
- testPayloadFailure() {
- createFlash({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
+ setTestAlertPayload(payload) {
+ this.testAlertPayload = payload;
+ },
+ validateAlertPayload(payload) {
+ return service
+ .updateTestAlert(payload ?? this.testAlertPayload)
+ .then(() => {
+ return createFlash({
+ message: this.$options.i18n.alertSent,
+ type: FLASH_TYPES.SUCCESS,
+ });
+ })
+ .catch(() => {
+ createFlash({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
+ });
},
},
};
@@ -310,13 +337,12 @@ export default {
</gl-alert>
<integrations-list
v-else
- :integrations="glFeatures.httpIntegrationsList ? integrations.list : integrationsOptionsOld"
+ :integrations="integrations.list"
:loading="loading"
@edit-integration="editIntegration"
@delete-integration="deleteIntegration"
/>
- <settings-form-new
- v-if="glFeatures.httpIntegrationsList"
+ <alert-settings-form
:loading="isUpdating"
:can-add-integration="canAddIntegration"
:can-manage-opsgenie="canManageOpsgenie"
@@ -324,8 +350,7 @@ export default {
@update-integration="updateIntegration"
@reset-token="resetToken"
@clear-current-integration="clearCurrentIntegration"
- @test-payload-failure="testPayloadFailure"
+ @set-test-alert-payload="setTestAlertPayload"
/>
- <settings-form-old v-else />
</div>
</template>
diff --git a/app/assets/javascripts/alerts_settings/services/index.js b/app/assets/javascripts/alerts_settings/services/index.js
index 1835d6b46aa..e45ea772ddd 100644
--- a/app/assets/javascripts/alerts_settings/services/index.js
+++ b/app/assets/javascripts/alerts_settings/services/index.js
@@ -2,30 +2,9 @@
import axios from '~/lib/utils/axios_utils';
export default {
- // TODO: All this code save updateTestAlert will be deleted as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/255501
- updateGenericKey({ endpoint, params }) {
- return axios.put(endpoint, params);
- },
- updatePrometheusKey({ endpoint }) {
- return axios.post(endpoint);
- },
updateGenericActive({ endpoint, params }) {
return axios.put(endpoint, params);
},
- updatePrometheusActive({ endpoint, params: { token, config, url, redirect } }) {
- const data = new FormData();
- data.set('_method', 'put');
- data.set('authenticity_token', token);
- data.set('service[manual_configuration]', config);
- data.set('service[api_url]', url);
- data.set('redirect_to', redirect);
-
- return axios.post(endpoint, data, {
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- });
- },
updateTestAlert({ endpoint, data, token }) {
return axios.post(endpoint, data, {
headers: {
diff --git a/app/assets/javascripts/behaviors/select2.js b/app/assets/javascripts/behaviors/select2.js
index 37b75bb5e56..1f222d8c1f6 100644
--- a/app/assets/javascripts/behaviors/select2.js
+++ b/app/assets/javascripts/behaviors/select2.js
@@ -1,22 +1,29 @@
import $ from 'jquery';
+import { loadCSSFile } from '../lib/utils/css_utils';
export default () => {
- if ($('select.select2').length) {
+ const $select2Elements = $('select.select2');
+ if ($select2Elements.length) {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
- $('select.select2').select2({
- width: 'resolve',
- minimumResultsForSearch: 10,
- dropdownAutoWidth: true,
- });
+ // eslint-disable-next-line promise/no-nesting
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ $select2Elements.select2({
+ width: 'resolve',
+ minimumResultsForSearch: 10,
+ dropdownAutoWidth: true,
+ });
- // Close select2 on escape
- $('.js-select2').on('select2-close', () => {
- setTimeout(() => {
- $('.select2-container-active').removeClass('select2-container-active');
- $(':focus').blur();
- }, 1);
- });
+ // Close select2 on escape
+ $('.js-select2').on('select2-close', () => {
+ requestAnimationFrame(() => {
+ $('.select2-container-active').removeClass('select2-container-active');
+ $(':focus').blur();
+ });
+ });
+ })
+ .catch(() => {});
})
.catch(() => {});
}
diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js
index bd39aa2e16f..2532aeea989 100644
--- a/app/assets/javascripts/blob/file_template_selector.js
+++ b/app/assets/javascripts/blob/file_template_selector.js
@@ -12,7 +12,10 @@ export default class FileTemplateSelector {
this.$dropdown = $(cfg.dropdown);
this.$wrapper = $(cfg.wrapper);
- this.$loadingIcon = this.$wrapper.find('.fa-chevron-down');
+ this.$dropdownIcon = this.$wrapper.find('.dropdown-menu-toggle-icon');
+ this.$loadingIcon = $(
+ '<div class="gl-spinner gl-spinner-orange gl-spinner-sm gl-absolute gl-top-3 gl-right-3 gl-display-none"></div>',
+ ).insertAfter(this.$dropdownIcon);
this.$dropdownToggleText = this.$wrapper.find('.dropdown-toggle-text');
this.initDropdown();
@@ -45,15 +48,13 @@ export default class FileTemplateSelector {
}
renderLoading() {
- this.$loadingIcon
- .addClass('gl-spinner gl-spinner-orange gl-spinner-sm')
- .removeClass('fa-chevron-down');
+ this.$loadingIcon.removeClass('gl-display-none');
+ this.$dropdownIcon.addClass('gl-display-none');
}
renderLoaded() {
- this.$loadingIcon
- .addClass('fa-chevron-down')
- .removeClass('gl-spinner gl-spinner-orange gl-spinner-sm');
+ this.$loadingIcon.addClass('gl-display-none');
+ this.$dropdownIcon.removeClass('gl-display-none');
}
reportSelection(options) {
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
index 257458138dc..ae9bb3455f0 100644
--- a/app/assets/javascripts/blob/template_selector.js
+++ b/app/assets/javascripts/blob/template_selector.js
@@ -10,7 +10,10 @@ export default class TemplateSelector {
this.dropdown = dropdown;
this.$dropdownContainer = wrapper;
this.$filenameInput = $input || $('#file_name');
- this.$dropdownIcon = $('.fa-chevron-down', dropdown);
+ this.$dropdownIcon = $('.dropdown-menu-toggle-icon', dropdown);
+ this.$loadingIcon = $(
+ '<div class="gl-spinner gl-spinner-orange gl-spinner-sm gl-absolute gl-top-3 gl-right-3 gl-display-none"></div>',
+ ).insertAfter(this.$dropdownIcon);
this.initDropdown(dropdown, data);
this.listenForFilenameInput();
@@ -92,10 +95,12 @@ export default class TemplateSelector {
}
startLoadingSpinner() {
- this.$dropdownIcon.addClass('spinner').removeClass('fa-chevron-down');
+ this.$loadingIcon.removeClass('gl-display-none');
+ this.$dropdownIcon.addClass('gl-display-none');
}
stopLoadingSpinner() {
- this.$dropdownIcon.addClass('fa-chevron-down').removeClass('spinner');
+ this.$loadingIcon.addClass('gl-display-none');
+ this.$dropdownIcon.removeClass('gl-display-none');
}
}
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index 6b7b0c2e28d..0142c95e773 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -1,5 +1,4 @@
import { sortBy } from 'lodash';
-import ListIssue from 'ee_else_ce/boards/models/issue';
import { ListType } from './constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import boardsStore from '~/boards/stores/boards_store';
@@ -21,11 +20,11 @@ export function formatBoardLists(lists) {
}
export function formatIssue(issue) {
- return new ListIssue({
+ return {
...issue,
labels: issue.labels?.nodes || [],
assignees: issue.assignees?.nodes || [],
- });
+ };
}
export function formatListIssues(listIssues) {
@@ -44,12 +43,12 @@ export function formatListIssues(listIssues) {
[list.id]: sortedIssues.map(i => {
const id = getIdFromGraphQLId(i.id);
- const listIssue = new ListIssue({
+ const listIssue = {
...i,
id,
labels: i.labels?.nodes || [],
assignees: i.assignees?.nodes || [],
- });
+ };
issues[id] = listIssue;
@@ -83,21 +82,30 @@ export function fullLabelId(label) {
}
export function moveIssueListHelper(issue, fromList, toList) {
- if (toList.type === ListType.label) {
- issue.addLabel(toList.label);
+ const updatedIssue = issue;
+ if (
+ toList.type === ListType.label &&
+ !updatedIssue.labels.find(label => label.id === toList.label.id)
+ ) {
+ updatedIssue.labels.push(toList.label);
}
- if (fromList && fromList.type === ListType.label) {
- issue.removeLabel(fromList.label);
+ if (fromList?.label && fromList.type === ListType.label) {
+ updatedIssue.labels = updatedIssue.labels.filter(label => fromList.label.id !== label.id);
}
- if (toList.type === ListType.assignee) {
- issue.addAssignee(toList.assignee);
+ if (
+ toList.type === ListType.assignee &&
+ !updatedIssue.assignees.find(assignee => assignee.id === toList.assignee.id)
+ ) {
+ updatedIssue.assignees.push(toList.assignee);
}
- if (fromList && fromList.type === ListType.assignee) {
- issue.removeAssignee(fromList.assignee);
+ if (fromList?.assignee && fromList.type === ListType.assignee) {
+ updatedIssue.assignees = updatedIssue.assignees.filter(
+ assignee => assignee.id !== fromList.assignee.id,
+ );
}
- return issue;
+ return updatedIssue;
}
export default {
diff --git a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
index c81f171af2b..231cde0d24f 100644
--- a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
+++ b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
@@ -1,11 +1,13 @@
<script>
import { mapActions, mapGetters } from 'vuex';
+import { cloneDeep } from 'lodash';
import {
GlDropdownItem,
GlDropdownDivider,
GlAvatarLabeled,
GlAvatarLink,
GlSearchBoxByType,
+ GlLoadingIcon,
} from '@gitlab/ui';
import { __, n__ } from '~/locale';
import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
@@ -32,12 +34,13 @@ export default {
GlAvatarLabeled,
GlAvatarLink,
GlSearchBoxByType,
+ GlLoadingIcon,
},
data() {
return {
search: '',
participants: [],
- selected: this.$store.getters.activeIssue.assignees,
+ selected: [],
};
},
apollo: {
@@ -89,9 +92,20 @@ export default {
isSearchEmpty() {
return this.search === '';
},
+ currentUser() {
+ return gon?.current_username;
+ },
+ },
+ created() {
+ this.selected = cloneDeep(this.activeIssue.assignees);
},
methods: {
...mapActions(['setAssignees']),
+ async assignSelf() {
+ const [currentUserObject] = await this.setAssignees(this.currentUser);
+
+ this.selectAssignee(currentUserObject);
+ },
clearSelected() {
this.selected = [];
},
@@ -119,7 +133,7 @@ export default {
<template>
<board-editable-item :title="assigneeText" @close="saveAssignees">
<template #collapsed>
- <issuable-assignees :users="activeIssue.assignees" />
+ <issuable-assignees :users="selected" @assign-self="assignSelf" />
</template>
<template #default>
@@ -132,45 +146,48 @@ export default {
<gl-search-box-by-type v-model.trim="search" />
</template>
<template #items>
- <gl-dropdown-item
- :is-checked="selectedIsEmpty"
- data-testid="unassign"
- class="mt-2"
- @click="selectAssignee()"
- >{{ $options.i18n.unassigned }}</gl-dropdown-item
- >
- <gl-dropdown-divider data-testid="unassign-divider" />
- <gl-dropdown-item
- v-for="item in selected"
- :key="item.id"
- :is-checked="isChecked(item.username)"
- @click="unselect(item.username)"
- >
- <gl-avatar-link>
- <gl-avatar-labeled
- :size="32"
- :label="item.name"
- :sub-label="item.username"
- :src="item.avatarUrl || item.avatar"
- />
- </gl-avatar-link>
- </gl-dropdown-item>
- <gl-dropdown-divider v-if="!selectedIsEmpty" data-testid="selected-user-divider" />
- <gl-dropdown-item
- v-for="unselectedUser in unSelectedFiltered"
- :key="unselectedUser.id"
- :data-testid="`item_${unselectedUser.name}`"
- @click="selectAssignee(unselectedUser)"
- >
- <gl-avatar-link>
- <gl-avatar-labeled
- :size="32"
- :label="unselectedUser.name"
- :sub-label="unselectedUser.username"
- :src="unselectedUser.avatarUrl || unselectedUser.avatar"
- />
- </gl-avatar-link>
- </gl-dropdown-item>
+ <gl-loading-icon v-if="$apollo.queries.participants.loading" size="lg" />
+ <template v-else>
+ <gl-dropdown-item
+ :is-checked="selectedIsEmpty"
+ data-testid="unassign"
+ class="mt-2"
+ @click="selectAssignee()"
+ >{{ $options.i18n.unassigned }}</gl-dropdown-item
+ >
+ <gl-dropdown-divider data-testid="unassign-divider" />
+ <gl-dropdown-item
+ v-for="item in selected"
+ :key="item.id"
+ :is-checked="isChecked(item.username)"
+ @click="unselect(item.username)"
+ >
+ <gl-avatar-link>
+ <gl-avatar-labeled
+ :size="32"
+ :label="item.name"
+ :sub-label="item.username"
+ :src="item.avatarUrl || item.avatar"
+ />
+ </gl-avatar-link>
+ </gl-dropdown-item>
+ <gl-dropdown-divider v-if="!selectedIsEmpty" data-testid="selected-user-divider" />
+ <gl-dropdown-item
+ v-for="unselectedUser in unSelectedFiltered"
+ :key="unselectedUser.id"
+ :data-testid="`item_${unselectedUser.name}`"
+ @click="selectAssignee(unselectedUser)"
+ >
+ <gl-avatar-link>
+ <gl-avatar-labeled
+ :size="32"
+ :label="unselectedUser.name"
+ :sub-label="unselectedUser.username"
+ :src="unselectedUser.avatarUrl || unselectedUser.avatar"
+ />
+ </gl-avatar-link>
+ </gl-dropdown-item>
+ </template>
</template>
</multi-select-dropdown>
</template>
diff --git a/app/assets/javascripts/boards/components/board_column_new.vue b/app/assets/javascripts/boards/components/board_column_new.vue
index 8a59355eb83..7162423846b 100644
--- a/app/assets/javascripts/boards/components/board_column_new.vue
+++ b/app/assets/javascripts/boards/components/board_column_new.vue
@@ -85,6 +85,7 @@ export default {
:disabled="disabled"
:issues="listIssues"
:list="list"
+ :can-admin-list="canAdminList"
/>
<!-- Will be only available in EE -->
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 53989e2d9de..1f87b563e73 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -6,7 +6,6 @@ import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store';
import { sprintf, __ } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
getBoardSortableDefaultOptions,
@@ -25,7 +24,6 @@ export default {
boardNewIssue,
GlLoadingIcon,
},
- mixins: [glFeatureFlagMixin()],
props: {
disabled: {
type: Boolean,
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index d85ba2038a7..722bd20f227 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -190,7 +190,8 @@ export default {
:title="chevronTooltip"
:icon="chevronIcon"
class="board-title-caret no-drag gl-cursor-pointer"
- variant="link"
+ category="tertiary"
+ size="small"
@click="toggleExpanded"
/>
<!-- The following is only true in EE and if it is a milestone -->
diff --git a/app/assets/javascripts/boards/components/board_list_header_new.vue b/app/assets/javascripts/boards/components/board_list_header_new.vue
index 99347a4cd4d..447fef4b49c 100644
--- a/app/assets/javascripts/boards/components/board_list_header_new.vue
+++ b/app/assets/javascripts/boards/components/board_list_header_new.vue
@@ -198,7 +198,8 @@ export default {
:title="chevronTooltip"
:icon="chevronIcon"
class="board-title-caret no-drag gl-cursor-pointer"
- variant="link"
+ category="tertiary"
+ size="small"
@click="toggleExpanded"
/>
<!-- EE start -->
diff --git a/app/assets/javascripts/boards/components/board_list_new.vue b/app/assets/javascripts/boards/components/board_list_new.vue
index 396aedcc557..291abc9849b 100644
--- a/app/assets/javascripts/boards/components/board_list_new.vue
+++ b/app/assets/javascripts/boards/components/board_list_new.vue
@@ -1,12 +1,14 @@
<script>
+import Draggable from 'vuedraggable';
import { mapActions, mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
+import defaultSortableConfig from '~/sortable/sortable_config';
+import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_options';
import BoardNewIssue from './board_new_issue_new.vue';
import BoardCard from './board_card.vue';
import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store';
import { sprintf, __ } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'BoardList',
@@ -15,7 +17,6 @@ export default {
BoardNewIssue,
GlLoadingIcon,
},
- mixins: [glFeatureFlagMixin()],
props: {
disabled: {
type: Boolean,
@@ -29,6 +30,11 @@ export default {
type: Array,
required: true,
},
+ canAdminList: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -55,12 +61,32 @@ export default {
loading() {
return this.listsFlags[this.list.id]?.isLoading;
},
+ listRef() {
+ // When list is draggable, the reference to the list needs to be accessed differently
+ return this.canAdminList ? this.$refs.list.$el : this.$refs.list;
+ },
+ treeRootWrapper() {
+ return this.canAdminList ? Draggable : 'ul';
+ },
+ treeRootOptions() {
+ const options = {
+ ...defaultSortableConfig,
+ fallbackOnBody: false,
+ group: 'boards-list',
+ tag: 'ul',
+ 'ghost-class': 'board-card-drag-active',
+ 'data-list-id': this.list.id,
+ value: this.issues,
+ };
+
+ return this.canAdminList ? options : {};
+ },
},
watch: {
filters: {
handler() {
this.list.loadingMore = false;
- this.$refs.list.scrollTop = 0;
+ this.listRef.scrollTop = 0;
},
deep: true,
},
@@ -76,26 +102,26 @@ export default {
},
mounted() {
// Scroll event on list to load more
- this.$refs.list.addEventListener('scroll', this.onScroll);
+ this.listRef.addEventListener('scroll', this.onScroll);
},
beforeDestroy() {
eventHub.$off(`toggle-issue-form-${this.list.id}`, this.toggleForm);
eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop);
- this.$refs.list.removeEventListener('scroll', this.onScroll);
+ this.listRef.removeEventListener('scroll', this.onScroll);
},
methods: {
- ...mapActions(['fetchIssuesForList']),
+ ...mapActions(['fetchIssuesForList', 'moveIssue']),
listHeight() {
- return this.$refs.list.getBoundingClientRect().height;
+ return this.listRef.getBoundingClientRect().height;
},
scrollHeight() {
- return this.$refs.list.scrollHeight;
+ return this.listRef.scrollHeight;
},
scrollTop() {
- return this.$refs.list.scrollTop + this.listHeight();
+ return this.listRef.scrollTop + this.listHeight();
},
scrollToTop() {
- this.$refs.list.scrollTop = 0;
+ this.listRef.scrollTop = 0;
},
loadNextPage() {
const loadingDone = () => {
@@ -120,6 +146,52 @@ export default {
}
});
},
+ handleDragOnStart() {
+ sortableStart();
+ },
+ handleDragOnEnd(params) {
+ sortableEnd();
+ const { newIndex, oldIndex, from, to, item } = params;
+ const { issueId, issueIid, issuePath } = item.dataset;
+ const { children } = to;
+ let moveBeforeId;
+ let moveAfterId;
+
+ const getIssueId = el => Number(el.dataset.issueId);
+
+ // If issue is being moved within the same list
+ if (from === to) {
+ if (newIndex > oldIndex && children.length > 1) {
+ // If issue is being moved down we look for the issue that ends up before
+ moveBeforeId = getIssueId(children[newIndex]);
+ } else if (newIndex < oldIndex && children.length > 1) {
+ // If issue is being moved up we look for the issue that ends up after
+ moveAfterId = getIssueId(children[newIndex]);
+ } else {
+ // If issue remains in the same list at the same position we do nothing
+ return;
+ }
+ } else {
+ // We look for the issue that ends up before the moved issue if it exists
+ if (children[newIndex - 1]) {
+ moveBeforeId = getIssueId(children[newIndex - 1]);
+ }
+ // We look for the issue that ends up after the moved issue if it exists
+ if (children[newIndex]) {
+ moveAfterId = getIssueId(children[newIndex]);
+ }
+ }
+
+ this.moveIssue({
+ issueId,
+ issueIid,
+ issuePath,
+ fromListId: from.dataset.listId,
+ toListId: to.dataset.listId,
+ moveBeforeId,
+ moveAfterId,
+ });
+ },
},
};
</script>
@@ -139,13 +211,18 @@ export default {
<gl-loading-icon />
</div>
<board-new-issue v-if="list.type !== 'closed' && showIssueForm" :list="list" />
- <ul
+ <component
+ :is="treeRootWrapper"
v-show="!loading"
ref="list"
+ v-bind="treeRootOptions"
:data-board="list.id"
:data-board-type="list.type"
:class="{ 'bg-danger-100': issuesSizeExceedsMax }"
class="board-list gl-w-full gl-h-full gl-list-style-none gl-mb-0 gl-p-2 js-board-list"
+ data-testid="tree-root-wrapper"
+ @start="handleDragOnStart"
+ @end="handleDragOnEnd"
>
<board-card
v-for="(issue, index) in issues"
@@ -161,6 +238,6 @@ export default {
<span v-if="issues.length === list.issuesSize">{{ __('Showing all issues') }}</span>
<span v-else>{{ paginatedIssueText }}</span>
</li>
- </ul>
+ </component>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index 45ce1e51489..67e8a32dbe2 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -158,9 +158,13 @@ export default {
class="confidential-icon gl-mr-2"
:aria-label="__('Confidential')"
/>
- <a :href="issue.path" :title="issue.title" class="js-no-trigger" @mousemove.stop>{{
- issue.title
- }}</a>
+ <a
+ :href="issue.path || issue.webUrl || ''"
+ :title="issue.title"
+ class="js-no-trigger"
+ @mousemove.stop
+ >{{ issue.title }}</a
+ >
</h4>
</div>
<div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap">
@@ -196,7 +200,11 @@ export default {
#{{ issue.iid }}
</span>
<span class="board-info-items gl-mt-3 gl-display-inline-block">
- <issue-due-date v-if="issue.dueDate" :date="issue.dueDate" :closed="issue.closed" />
+ <issue-due-date
+ v-if="issue.dueDate"
+ :date="issue.dueDate"
+ :closed="issue.closed || Boolean(issue.closedAt)"
+ />
<issue-time-estimate v-if="issue.timeEstimate" :estimate="issue.timeEstimate" />
<issue-card-weight
v-if="validIssueWeight"
diff --git a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
index 5fb7a9b210c..ce267be6d45 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
@@ -50,6 +50,13 @@ export default {
}
window.removeEventListener('click', this.collapseWhenOffClick);
},
+ toggle({ emitEvent = true } = {}) {
+ if (this.edit) {
+ this.collapse({ emitEvent });
+ } else {
+ this.expand();
+ }
+ },
},
};
</script>
@@ -64,18 +71,18 @@ export default {
<gl-button
v-if="canUpdate"
variant="link"
- class="gl-text-gray-900!"
+ class="gl-text-gray-900! js-sidebar-dropdown-toggle"
data-testid="edit-button"
- @click="expand()"
+ @click="toggle"
>
{{ __('Edit') }}
</gl-button>
</div>
- <div v-show="!edit" class="gl-text-gray-400" data-testid="collapsed-content">
+ <div v-show="!edit" class="gl-text-gray-500" data-testid="collapsed-content">
<slot name="collapsed">{{ __('None') }}</slot>
</div>
<div v-show="edit" data-testid="expanded-content">
- <slot></slot>
+ <slot :edit="edit"></slot>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
index 6935ead2706..904ceaed1b3 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
@@ -79,7 +79,7 @@ export default {
<span class="gl-mx-2">-</span>
<gl-button
variant="link"
- class="gl-text-gray-400!"
+ class="gl-text-gray-500!"
data-testid="reset-button"
:disabled="loading"
@click="setDueDate(null)"
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
index 9d537a4ef2c..6a407bd6ba6 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
@@ -92,7 +92,7 @@ export default {
@close="removeLabel(label.id)"
/>
</template>
- <template>
+ <template #default="{ edit }">
<labels-select
ref="labelsSelect"
:allow-label-edit="false"
@@ -105,6 +105,7 @@ export default {
:labels-filter-base-path="labelsFilterBasePath"
:labels-list-title="__('Select label')"
:dropdown-button-text="__('Choose labels')"
+ :is-editing="edit"
variant="embedded"
class="gl-display-block labels gl-w-full"
@updateSelectedLabels="setLabels"
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
new file mode 100644
index 00000000000..f7b7fd3f61f
--- /dev/null
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
@@ -0,0 +1,161 @@
+<script>
+import { mapGetters, mapActions } from 'vuex';
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlSearchBoxByType,
+ GlDropdownDivider,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import { fetchPolicies } from '~/lib/graphql';
+import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
+import groupMilestones from '../../queries/group_milestones.query.graphql';
+import createFlash from '~/flash';
+import { __, s__ } from '~/locale';
+
+export default {
+ components: {
+ BoardEditableItem,
+ GlDropdown,
+ GlLoadingIcon,
+ GlDropdownItem,
+ GlDropdownText,
+ GlSearchBoxByType,
+ GlDropdownDivider,
+ },
+ data() {
+ return {
+ milestones: [],
+ searchTitle: '',
+ loading: false,
+ edit: false,
+ };
+ },
+ apollo: {
+ milestones: {
+ fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ query: groupMilestones,
+ debounce: 250,
+ skip() {
+ return !this.edit;
+ },
+ variables() {
+ return {
+ fullPath: this.groupFullPath,
+ searchTitle: this.searchTitle,
+ state: 'active',
+ includeDescendants: true,
+ };
+ },
+ update(data) {
+ const edges = data?.group?.milestones?.edges ?? [];
+ return edges.map(item => item.node);
+ },
+ error() {
+ createFlash({ message: this.$options.i18n.fetchMilestonesError });
+ },
+ },
+ },
+ computed: {
+ ...mapGetters({ issue: 'activeIssue' }),
+ hasMilestone() {
+ return this.issue.milestone !== null;
+ },
+ groupFullPath() {
+ const { referencePath = '' } = this.issue;
+ return referencePath.slice(0, referencePath.indexOf('/'));
+ },
+ projectPath() {
+ const { referencePath = '' } = this.issue;
+ return referencePath.slice(0, referencePath.indexOf('#'));
+ },
+ dropdownText() {
+ return this.issue.milestone?.title ?? this.$options.i18n.noMilestone;
+ },
+ },
+ mounted() {
+ this.$root.$on('bv::dropdown::hide', () => {
+ this.$refs.sidebarItem.collapse();
+ });
+ },
+ methods: {
+ ...mapActions(['setActiveIssueMilestone']),
+ handleOpen() {
+ this.edit = true;
+ this.$refs.dropdown.show();
+ },
+ async setMilestone(milestoneId) {
+ this.loading = true;
+ this.searchTitle = '';
+ this.$refs.sidebarItem.collapse();
+
+ try {
+ const input = { milestoneId, projectPath: this.projectPath };
+ await this.setActiveIssueMilestone(input);
+ } catch (e) {
+ createFlash({ message: this.$options.i18n.updateMilestoneError });
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ i18n: {
+ milestone: __('Milestone'),
+ noMilestone: __('No milestone'),
+ assignMilestone: __('Assign milestone'),
+ noMilestonesFound: s__('Milestones|No milestones found'),
+ fetchMilestonesError: __('There was a problem fetching milestones.'),
+ updateMilestoneError: __('An error occurred while updating the milestone.'),
+ },
+};
+</script>
+
+<template>
+ <board-editable-item
+ ref="sidebarItem"
+ :title="$options.i18n.milestone"
+ :loading="loading"
+ @open="handleOpen()"
+ @close="edit = false"
+ >
+ <template v-if="hasMilestone" #collapsed>
+ <strong class="gl-text-gray-900">{{ issue.milestone.title }}</strong>
+ </template>
+ <template>
+ <gl-dropdown
+ ref="dropdown"
+ :text="dropdownText"
+ :header-text="$options.i18n.assignMilestone"
+ block
+ >
+ <gl-search-box-by-type ref="search" v-model.trim="searchTitle" class="gl-m-3" />
+ <gl-dropdown-item
+ data-testid="no-milestone-item"
+ :is-check-item="true"
+ :is-checked="!issue.milestone"
+ @click="setMilestone(null)"
+ >
+ {{ $options.i18n.noMilestone }}
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ <gl-loading-icon v-if="$apollo.loading" class="gl-py-4" />
+ <template v-else-if="milestones.length > 0">
+ <gl-dropdown-item
+ v-for="milestone in milestones"
+ :key="milestone.id"
+ :is-check-item="true"
+ :is-checked="issue.milestone && milestone.id === issue.milestone.id"
+ data-testid="milestone-item"
+ @click="setMilestone(milestone.id)"
+ >
+ {{ milestone.title }}
+ </gl-dropdown-item>
+ </template>
+ <gl-dropdown-text v-else data-testid="no-milestones-found">
+ {{ $options.i18n.noMilestonesFound }}
+ </gl-dropdown-text>
+ </gl-dropdown>
+ </template>
+ </board-editable-item>
+</template>
diff --git a/app/assets/javascripts/boards/queries/group_milestones.query.graphql b/app/assets/javascripts/boards/queries/group_milestones.query.graphql
new file mode 100644
index 00000000000..f2ab12ef4a7
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/group_milestones.query.graphql
@@ -0,0 +1,17 @@
+query groupMilestones(
+ $fullPath: ID!
+ $state: MilestoneStateEnum
+ $includeDescendants: Boolean
+ $searchTitle: String
+) {
+ group(fullPath: $fullPath) {
+ milestones(state: $state, includeDescendants: $includeDescendants, searchTitle: $searchTitle) {
+ edges {
+ node {
+ id
+ title
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/issue.fragment.graphql b/app/assets/javascripts/boards/queries/issue.fragment.graphql
index 4b429f875a6..1395bef39ed 100644
--- a/app/assets/javascripts/boards/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/boards/queries/issue.fragment.graphql
@@ -11,6 +11,10 @@ fragment IssueNode on Issue {
webUrl
subscribed
relativePosition
+ milestone {
+ id
+ title
+ }
assignees {
nodes {
...User
diff --git a/app/assets/javascripts/boards/queries/issue_set_milestone.mutation.graphql b/app/assets/javascripts/boards/queries/issue_set_milestone.mutation.graphql
new file mode 100644
index 00000000000..5dc78a03a06
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/issue_set_milestone.mutation.graphql
@@ -0,0 +1,12 @@
+mutation issueSetMilestone($input: UpdateIssueInput!) {
+ updateIssue(input: $input) {
+ issue {
+ milestone {
+ id
+ title
+ description
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index dd950a45076..00c64bff74e 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -25,6 +25,7 @@ import issueCreateMutation from '../queries/issue_create.mutation.graphql';
import issueSetLabels from '../queries/issue_set_labels.mutation.graphql';
import issueSetDueDate from '../queries/issue_set_due_date.mutation.graphql';
import issueSetSubscriptionMutation from '../graphql/mutations/issue_set_subscription.mutation.graphql';
+import issueSetMilestone from '../queries/issue_set_milestone.mutation.graphql';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
@@ -325,14 +326,42 @@ export default {
},
})
.then(({ data }) => {
+ const { nodes } = data.issueSetAssignees?.issue?.assignees || [];
+
commit('UPDATE_ISSUE_BY_ID', {
issueId: getters.activeIssue.id,
prop: 'assignees',
- value: data.issueSetAssignees.issue.assignees.nodes,
+ value: nodes,
});
+
+ return nodes;
});
},
+ setActiveIssueMilestone: async ({ commit, getters }, input) => {
+ const { activeIssue } = getters;
+ const { data } = await gqlClient.mutate({
+ mutation: issueSetMilestone,
+ variables: {
+ input: {
+ iid: String(activeIssue.iid),
+ milestoneId: getIdFromGraphQLId(input.milestoneId),
+ projectPath: input.projectPath,
+ },
+ },
+ });
+
+ if (data.updateIssue.errors?.length > 0) {
+ throw new Error(data.updateIssue.errors);
+ }
+
+ commit(types.UPDATE_ISSUE_BY_ID, {
+ issueId: activeIssue.id,
+ prop: 'milestone',
+ value: data.updateIssue.issue.milestone,
+ });
+ },
+
createNewIssue: ({ commit, state }, issueInput) => {
const input = issueInput;
const { boardType, endpoints } = state;
diff --git a/app/assets/javascripts/clone_panel.js b/app/assets/javascripts/clone_panel.js
new file mode 100644
index 00000000000..362e6c5c5ce
--- /dev/null
+++ b/app/assets/javascripts/clone_panel.js
@@ -0,0 +1,42 @@
+import $ from 'jquery';
+
+export default function initClonePanel() {
+ const $cloneOptions = $('ul.clone-options-dropdown');
+ if ($cloneOptions.length) {
+ const $cloneUrlField = $('#clone_url');
+ const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label');
+ const mobileCloneField = document.querySelector(
+ '.js-mobile-git-clone .js-clone-dropdown-label',
+ );
+
+ const selectedCloneOption = $cloneBtnLabel.text().trim();
+ if (selectedCloneOption.length > 0) {
+ $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
+ }
+
+ $('a', $cloneOptions).on('click', e => {
+ e.preventDefault();
+ const $this = $(e.currentTarget);
+ const url = $this.attr('href');
+ const cloneType = $this.data('cloneType');
+
+ $('.is-active', $cloneOptions).removeClass('is-active');
+ $(`a[data-clone-type="${cloneType}"]`).each(function switchProtocol() {
+ const $el = $(this);
+ const activeText = $el.find('.dropdown-menu-inner-title').text();
+ const $container = $el.closest('.js-git-clone-holder, .js-mobile-git-clone');
+ const $label = $container.find('.js-clone-dropdown-label');
+
+ $el.toggleClass('is-active');
+ $label.text(activeText);
+ });
+
+ if (mobileCloneField) {
+ mobileCloneField.dataset.clipboardText = url;
+ } else {
+ $cloneUrlField.val(url);
+ }
+ $('.js-git-empty .js-clone').text(url);
+ });
+ }
+}
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index a75646db162..ba005e98d53 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -52,6 +52,7 @@ export default class Clusters {
clusterStatus,
clusterStatusReason,
helpPath,
+ helmHelpPath,
ingressHelpPath,
ingressDnsHelpPath,
ingressModSecurityHelpPath,
@@ -68,8 +69,9 @@ export default class Clusters {
this.clusterBannerDismissedKey = `cluster_${this.clusterId}_banner_dismissed`;
this.store = new ClustersStore();
- this.store.setHelpPaths(
+ this.store.setHelpPaths({
helpPath,
+ helmHelpPath,
ingressHelpPath,
ingressDnsHelpPath,
ingressModSecurityHelpPath,
@@ -78,7 +80,7 @@ export default class Clusters {
deployBoardsHelpPath,
cloudRunHelpPath,
ciliumHelpPath,
- );
+ });
this.store.setManagePrometheusPath(managePrometheusPath);
this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason);
@@ -162,6 +164,7 @@ export default class Clusters {
type,
applications: this.state.applications,
helpPath: this.state.helpPath,
+ helmHelpPath: this.state.helmHelpPath,
ingressHelpPath: this.state.ingressHelpPath,
managePrometheusPath: this.state.managePrometheusPath,
ingressDnsHelpPath: this.state.ingressDnsHelpPath,
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 271d862afab..412082b648f 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -1,6 +1,7 @@
<script>
import { GlLoadingIcon, GlSprintf, GlLink } from '@gitlab/ui';
import gitlabLogo from 'images/cluster_app_logos/gitlab.png';
+import helmLogo from 'images/cluster_app_logos/helm.png';
import jupyterhubLogo from 'images/cluster_app_logos/jupyterhub.png';
import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png';
import certManagerLogo from 'images/cluster_app_logos/cert_manager.png';
@@ -46,6 +47,11 @@ export default {
required: false,
default: '',
},
+ helmHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
ingressHelpPath: {
type: String,
required: false,
@@ -150,6 +156,7 @@ export default {
},
logos: {
gitlabLogo,
+ helmLogo,
jupyterhubLogo,
kubernetesLogo,
certManagerLogo,
@@ -173,6 +180,35 @@ export default {
<div class="cluster-application-list gl-mt-3">
<application-row
+ v-if="applications.helm.installed || applications.helm.uninstalling"
+ id="helm"
+ :logo-url="$options.logos.helmLogo"
+ :title="applications.helm.title"
+ :status="applications.helm.status"
+ :status-reason="applications.helm.statusReason"
+ :request-status="applications.helm.requestStatus"
+ :request-reason="applications.helm.requestReason"
+ :installed="applications.helm.installed"
+ :install-failed="applications.helm.installFailed"
+ :uninstallable="applications.helm.uninstallable"
+ :uninstall-successful="applications.helm.uninstallSuccessful"
+ :uninstall-failed="applications.helm.uninstallFailed"
+ title-link="https://v2.helm.sh/"
+ >
+ <template #description>
+ <p>
+ {{
+ s__(`ClusterIntegration|Can be safely removed. Prior to GitLab
+ 13.2, GitLab used a remote Tiller server to manage the
+ applications. GitLab no longer uses this server.
+ Uninstalling this server will not affect your other
+ applications. This row will disappear afterwards.`)
+ }}
+ <gl-link :href="helmHelpPath">{{ __('More information') }}</gl-link>
+ </p>
+ </template>
+ </application-row>
+ <application-row
:id="ingressId"
:logo-url="$options.logos.kubernetesLogo"
:title="applications.ingress.title"
diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
index cb415d902e8..d80bd6f5b42 100644
--- a/app/assets/javascripts/clusters/components/knative_domain_editor.vue
+++ b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
@@ -7,6 +7,7 @@ import {
GlSearchBoxByType,
GlSprintf,
GlButton,
+ GlAlert,
} from '@gitlab/ui';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { __, s__ } from '~/locale';
@@ -25,6 +26,7 @@ export default {
GlDropdownItem,
GlSearchBoxByType,
GlSprintf,
+ GlAlert,
},
props: {
knative: {
@@ -106,12 +108,13 @@ export default {
<template>
<div class="row">
- <div
+ <gl-alert
v-if="knative.updateFailed"
- class="bs-callout bs-callout-danger cluster-application-banner col-12 mt-2 mb-2 js-cluster-knative-domain-name-failure-message"
+ class="gl-mb-5 col-12 js-cluster-knative-domain-name-failure-message"
+ variant="danger"
>
{{ s__('ClusterIntegration|Something went wrong while updating Knative domain name.') }}
- </div>
+ </gl-alert>
<div
:class="{ 'col-md-6': knativeInstalled, 'col-12': !knativeInstalled }"
diff --git a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
index 477dd13db4f..2a197e40b60 100644
--- a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
+++ b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
@@ -16,7 +16,7 @@ import {
const CUSTOM_APP_WARNING_TEXT = {
[HELM]: sprintf(
s__(
- 'ClusterIntegration|The associated Tiller pod, the %{gitlabManagedAppsNamespace} namespace, and all of its resources will be deleted and cannot be restored.',
+ 'ClusterIntegration|The associated Tiller pod will be deleted and cannot be restored. Your other applications will remain unaffected.',
),
{
gitlabManagedAppsNamespace: '<code>gitlab-managed-apps</code>',
diff --git a/app/assets/javascripts/clusters/services/application_state_machine.js b/app/assets/javascripts/clusters/services/application_state_machine.js
index 683b0e18534..1dd815ae44d 100644
--- a/app/assets/javascripts/clusters/services/application_state_machine.js
+++ b/app/assets/javascripts/clusters/services/application_state_machine.js
@@ -193,6 +193,12 @@ const applicationStateMachine = {
uninstallSuccessful: true,
},
},
+ [NOT_INSTALLABLE]: {
+ target: NOT_INSTALLABLE,
+ effects: {
+ uninstallSuccessful: true,
+ },
+ },
[UNINSTALL_ERRORED]: {
target: INSTALLED,
effects: {
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 53868b7c02d..88505eac3a9 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -36,6 +36,7 @@ export default class ClusterStore {
constructor() {
this.state = {
helpPath: null,
+ helmHelpPath: null,
ingressHelpPath: null,
environmentsHelpPath: null,
clustersHelpPath: null,
@@ -49,7 +50,7 @@ export default class ClusterStore {
applications: {
helm: {
...applicationInitialState,
- title: s__('ClusterIntegration|Helm Tiller'),
+ title: s__('ClusterIntegration|Legacy Helm Tiller server'),
},
ingress: {
...applicationInitialState,
@@ -126,26 +127,10 @@ export default class ClusterStore {
};
}
- setHelpPaths(
- helpPath,
- ingressHelpPath,
- ingressDnsHelpPath,
- ingressModSecurityHelpPath,
- environmentsHelpPath,
- clustersHelpPath,
- deployBoardsHelpPath,
- cloudRunHelpPath,
- ciliumHelpPath,
- ) {
- this.state.helpPath = helpPath;
- this.state.ingressHelpPath = ingressHelpPath;
- this.state.ingressDnsHelpPath = ingressDnsHelpPath;
- this.state.ingressModSecurityHelpPath = ingressModSecurityHelpPath;
- this.state.environmentsHelpPath = environmentsHelpPath;
- this.state.clustersHelpPath = clustersHelpPath;
- this.state.deployBoardsHelpPath = deployBoardsHelpPath;
- this.state.cloudRunHelpPath = cloudRunHelpPath;
- this.state.ciliumHelpPath = ciliumHelpPath;
+ setHelpPaths(helpPaths) {
+ Object.assign(this.state, {
+ ...helpPaths,
+ });
}
setManagePrometheusPath(managePrometheusPath) {
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 9d8d184a3f6..09baf16ade9 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -230,9 +230,6 @@ export default {
}
},
diffViewType() {
- if (!this.glFeatures.unifiedDiffLines && (this.needsReload() || this.needsFirstLoad())) {
- this.refetchDiffData();
- }
this.adjustView();
},
shouldShow() {
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 1b747fb7f20..a548354f257 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -136,7 +136,12 @@ export default {
class="d-inline-flex mb-2"
/>
<gl-button-group class="gl-ml-4 gl-mb-4" data-testid="commit-sha-group">
- <gl-button label class="gl-font-monospace" v-text="commit.short_id" />
+ <gl-button
+ label
+ class="gl-font-monospace"
+ data-testid="commit-sha-short-id"
+ v-text="commit.short_id"
+ />
<clipboard-button
:text="commit.id"
:title="__('Copy commit SHA')"
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index 401064fb18f..1454728288e 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -87,7 +87,7 @@ export default {
return this.getUserData;
},
mappedLines() {
- if (this.glFeatures.unifiedDiffLines && this.glFeatures.unifiedDiffComponents) {
+ if (this.glFeatures.unifiedDiffComponents) {
return this.diffLines(this.diffFile, true).map(mapParallel(this)) || [];
}
@@ -95,9 +95,7 @@ export default {
if (this.isInlineView) {
return this.diffFile.highlighted_diff_lines.map(mapInline(this));
}
- return this.glFeatures.unifiedDiffLines
- ? this.diffLines(this.diffFile).map(mapParallel(this))
- : this.diffFile.parallel_diff_lines.map(mapParallel(this)) || [];
+ return this.diffLines(this.diffFile).map(mapParallel(this));
},
},
updated() {
@@ -129,9 +127,7 @@ export default {
<template>
<div class="diff-content">
<div class="diff-viewer">
- <template
- v-if="isTextFile && glFeatures.unifiedDiffLines && glFeatures.unifiedDiffComponents"
- >
+ <template v-if="isTextFile && glFeatures.unifiedDiffComponents">
<diff-view
:diff-file="diffFile"
:diff-lines="mappedLines"
diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
index 4c49dfb5de9..ca0edb448b6 100644
--- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
@@ -4,7 +4,7 @@ import { GlIcon } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { UNFOLD_COUNT, INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
+import { UNFOLD_COUNT, INLINE_DIFF_VIEW_TYPE, INLINE_DIFF_LINES_KEY } from '../constants';
import * as utils from '../store/utils';
const EXPAND_ALL = 0;
@@ -14,7 +14,6 @@ const EXPAND_DOWN = 2;
const lineNumberByViewType = (viewType, diffLine) => {
const numberGetters = {
[INLINE_DIFF_VIEW_TYPE]: line => line?.new_line,
- [PARALLEL_DIFF_VIEW_TYPE]: line => (line?.right || line?.left)?.new_line,
};
const numberGetter = numberGetters[viewType];
return numberGetter && numberGetter(diffLine);
@@ -57,9 +56,6 @@ export default {
},
computed: {
...mapState({
- diffViewType(state) {
- return this.glFeatures.unifiedDiffLines ? INLINE_DIFF_VIEW_TYPE : state.diffs.diffViewType;
- },
diffFiles: state => state.diffs.diffFiles,
}),
canExpandUp() {
@@ -77,16 +73,14 @@ export default {
...mapActions('diffs', ['loadMoreLines']),
getPrevLineNumber(oldLineNumber, newLineNumber) {
const diffFile = utils.findDiffFile(this.diffFiles, this.fileHash);
- const lines = {
- [INLINE_DIFF_VIEW_TYPE]: diffFile.highlighted_diff_lines,
- [PARALLEL_DIFF_VIEW_TYPE]: diffFile.parallel_diff_lines,
- };
- const index = utils.getPreviousLineIndex(this.diffViewType, diffFile, {
+ const index = utils.getPreviousLineIndex(INLINE_DIFF_VIEW_TYPE, diffFile, {
oldLineNumber,
newLineNumber,
});
- return lineNumberByViewType(this.diffViewType, lines[this.diffViewType][index - 2]) || 0;
+ return (
+ lineNumberByViewType(INLINE_DIFF_VIEW_TYPE, diffFile[INLINE_DIFF_LINES_KEY][index - 2]) || 0
+ );
},
callLoadMoreLines(
endpoint,
@@ -113,7 +107,7 @@ export default {
this.isRequesting = true;
const endpoint = this.contextLinesPath;
const { fileHash } = this;
- const view = this.diffViewType;
+ const view = INLINE_DIFF_VIEW_TYPE;
const oldLineNumber = this.line.meta_data.old_pos || 0;
const newLineNumber = this.line.meta_data.new_pos || 0;
const offset = newLineNumber - oldLineNumber;
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index 55f5a736cdf..172a2bdde7d 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -7,7 +7,7 @@ import noteForm from '../../notes/components/note_form.vue';
import MultilineCommentForm from '../../notes/components/multiline_comment_form.vue';
import autosave from '../../notes/mixins/autosave';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import { DIFF_NOTE_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
+import { DIFF_NOTE_TYPE, INLINE_DIFF_LINES_KEY, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
import {
commentLineOptions,
formatLineRange,
@@ -102,13 +102,13 @@ export default {
};
const getDiffLines = () => {
if (this.diffViewType === PARALLEL_DIFF_VIEW_TYPE) {
- return (this.glFeatures.unifiedDiffLines
- ? this.diffLines(this.diffFile)
- : this.diffFile.parallel_diff_lines
- ).reduce(combineSides, []);
+ return this.diffLines(this.diffFile, this.glFeatures.unifiedDiffComponents).reduce(
+ combineSides,
+ [],
+ );
}
- return this.diffFile.highlighted_diff_lines;
+ return this.diffFile[INLINE_DIFF_LINES_KEY];
};
const side = this.line.type === 'new' ? 'right' : 'left';
const lines = getDiffLines();
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 91c4c51487f..b3cdf138ac9 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -30,13 +30,11 @@ import {
OLD_LINE_KEY,
NEW_LINE_KEY,
TYPE_KEY,
- LEFT_LINE_KEY,
MAX_RENDERING_DIFF_LINES,
MAX_RENDERING_BULK_ROWS,
MIN_RENDERING_MS,
START_RENDERING_INDEX,
INLINE_DIFF_LINES_KEY,
- PARALLEL_DIFF_LINES_KEY,
DIFFS_PER_PAGE,
DIFF_WHITESPACE_COOKIE_NAME,
SHOW_WHITESPACE,
@@ -77,7 +75,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
const urlParams = {
per_page: DIFFS_PER_PAGE,
w: state.showWhitespace ? '0' : '1',
- view: window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType,
+ view: 'inline',
};
commit(types.SET_BATCH_LOADING, true);
@@ -140,7 +138,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
export const fetchDiffFilesMeta = ({ commit, state }) => {
const worker = new TreeWorker();
const urlParams = {
- view: window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType,
+ view: 'inline',
};
commit(types.SET_LOADING, true);
@@ -401,15 +399,10 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
export const toggleFileDiscussionWrappers = ({ commit }, diff) => {
const discussionWrappersExpanded = allDiscussionWrappersExpanded(diff);
const lineCodesWithDiscussions = new Set();
- const { parallel_diff_lines: parallelLines, highlighted_diff_lines: inlineLines } = diff;
- const allLines = inlineLines.concat(
- parallelLines.map(line => line.left),
- parallelLines.map(line => line.right),
- );
const lineHasDiscussion = line => Boolean(line?.discussions.length);
const registerDiscussionLine = line => lineCodesWithDiscussions.add(line.line_code);
- allLines.filter(lineHasDiscussion).forEach(registerDiscussionLine);
+ diff[INLINE_DIFF_LINES_KEY].filter(lineHasDiscussion).forEach(registerDiscussionLine);
if (lineCodesWithDiscussions.size) {
Array.from(lineCodesWithDiscussions).forEach(lineCode => {
@@ -508,61 +501,26 @@ export const receiveFullDiffError = ({ commit }, filePath) => {
createFlash(s__('MergeRequest|Error loading full diff. Please try again.'));
};
-export const setExpandedDiffLines = ({ commit, state }, { file, data }) => {
- const expandedDiffLines = {
- highlighted_diff_lines: convertExpandLines({
- diffLines: file.highlighted_diff_lines,
- typeKey: TYPE_KEY,
- oldLineKey: OLD_LINE_KEY,
- newLineKey: NEW_LINE_KEY,
- data,
- mapLine: ({ line, oldLine, newLine }) =>
- Object.assign(line, {
- old_line: oldLine,
- new_line: newLine,
- line_code: `${file.file_hash}_${oldLine}_${newLine}`,
- }),
- }),
- parallel_diff_lines: convertExpandLines({
- diffLines: file.parallel_diff_lines,
- typeKey: [LEFT_LINE_KEY, TYPE_KEY],
- oldLineKey: [LEFT_LINE_KEY, OLD_LINE_KEY],
- newLineKey: [LEFT_LINE_KEY, NEW_LINE_KEY],
- data,
- mapLine: ({ line, oldLine, newLine }) => ({
- left: {
- ...line,
- old_line: oldLine,
- line_code: `${file.file_hash}_${oldLine}_${newLine}`,
- },
- right: {
- ...line,
- new_line: newLine,
- line_code: `${file.file_hash}_${newLine}_${oldLine}`,
- },
+export const setExpandedDiffLines = ({ commit }, { file, data }) => {
+ const expandedDiffLines = convertExpandLines({
+ diffLines: file[INLINE_DIFF_LINES_KEY],
+ typeKey: TYPE_KEY,
+ oldLineKey: OLD_LINE_KEY,
+ newLineKey: NEW_LINE_KEY,
+ data,
+ mapLine: ({ line, oldLine, newLine }) =>
+ Object.assign(line, {
+ old_line: oldLine,
+ new_line: newLine,
+ line_code: `${file.file_hash}_${oldLine}_${newLine}`,
}),
- }),
- };
- const unifiedDiffLinesEnabled = window.gon?.features?.unifiedDiffLines;
- const currentDiffLinesKey =
- state.diffViewType === INLINE_DIFF_VIEW_TYPE || unifiedDiffLinesEnabled
- ? INLINE_DIFF_LINES_KEY
- : PARALLEL_DIFF_LINES_KEY;
- const hiddenDiffLinesKey =
- state.diffViewType === INLINE_DIFF_VIEW_TYPE ? PARALLEL_DIFF_LINES_KEY : INLINE_DIFF_LINES_KEY;
-
- if (!unifiedDiffLinesEnabled) {
- commit(types.SET_HIDDEN_VIEW_DIFF_FILE_LINES, {
- filePath: file.file_path,
- lines: expandedDiffLines[hiddenDiffLinesKey],
- });
- }
+ });
- if (expandedDiffLines[currentDiffLinesKey].length > MAX_RENDERING_DIFF_LINES) {
+ if (expandedDiffLines.length > MAX_RENDERING_DIFF_LINES) {
let index = START_RENDERING_INDEX;
commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, {
filePath: file.file_path,
- lines: expandedDiffLines[currentDiffLinesKey].slice(0, index),
+ lines: expandedDiffLines.slice(0, index),
});
commit(types.TOGGLE_DIFF_FILE_RENDERING_MORE, file.file_path);
@@ -571,10 +529,10 @@ export const setExpandedDiffLines = ({ commit, state }, { file, data }) => {
while (
t.timeRemaining() >= MIN_RENDERING_MS &&
- index !== expandedDiffLines[currentDiffLinesKey].length &&
+ index !== expandedDiffLines.length &&
index - startIndex !== MAX_RENDERING_BULK_ROWS
) {
- const line = expandedDiffLines[currentDiffLinesKey][index];
+ const line = expandedDiffLines[index];
if (line) {
commit(types.ADD_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: file.file_path, line });
@@ -582,7 +540,7 @@ export const setExpandedDiffLines = ({ commit, state }, { file, data }) => {
}
}
- if (index !== expandedDiffLines[currentDiffLinesKey].length) {
+ if (index !== expandedDiffLines.length) {
idleCallback(idleCb);
} else {
commit(types.TOGGLE_DIFF_FILE_RENDERING_MORE, file.file_path);
@@ -593,7 +551,7 @@ export const setExpandedDiffLines = ({ commit, state }, { file, data }) => {
} else {
commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, {
filePath: file.file_path,
- lines: expandedDiffLines[currentDiffLinesKey],
+ lines: expandedDiffLines,
});
}
};
@@ -627,7 +585,7 @@ export const toggleFullDiff = ({ dispatch, commit, getters, state }, filePath) =
}
};
-export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { diffFile }) {
+export function switchToFullDiffFromRenamedFile({ commit, dispatch }, { diffFile }) {
return axios
.get(diffFile.context_lines_path, {
params: {
@@ -638,7 +596,7 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d
.then(({ data }) => {
const lines = data.map((line, index) =>
prepareLineForRenamedFile({
- diffViewType: window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType,
+ diffViewType: 'inline',
line,
diffFile,
index,
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index 9ee73998177..baf54188932 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -1,6 +1,10 @@
import { __, n__ } from '~/locale';
import { parallelizeDiffLines } from './utils';
-import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
+import {
+ PARALLEL_DIFF_VIEW_TYPE,
+ INLINE_DIFF_VIEW_TYPE,
+ INLINE_DIFF_LINES_KEY,
+} from '../constants';
export * from './getters_versions_dropdowns';
@@ -54,24 +58,10 @@ export const diffHasAllCollapsedDiscussions = (state, getters) => diff => {
* @param {Object} diff
* @returns {Boolean}
*/
-export const diffHasExpandedDiscussions = state => diff => {
- const lines = {
- [INLINE_DIFF_VIEW_TYPE]: diff.highlighted_diff_lines || [],
- [PARALLEL_DIFF_VIEW_TYPE]: (diff.parallel_diff_lines || []).reduce((acc, line) => {
- if (line.left) {
- acc.push(line.left);
- }
-
- if (line.right) {
- acc.push(line.right);
- }
-
- return acc;
- }, []),
- };
- return lines[window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType]
- .filter(l => l.discussions.length >= 1)
- .some(l => l.discussionsExpanded);
+export const diffHasExpandedDiscussions = () => diff => {
+ return diff[INLINE_DIFF_LINES_KEY].filter(l => l.discussions.length >= 1).some(
+ l => l.discussionsExpanded,
+ );
};
/**
@@ -79,24 +69,8 @@ export const diffHasExpandedDiscussions = state => diff => {
* @param {Boolean} diff
* @returns {Boolean}
*/
-export const diffHasDiscussions = state => diff => {
- const lines = {
- [INLINE_DIFF_VIEW_TYPE]: diff.highlighted_diff_lines || [],
- [PARALLEL_DIFF_VIEW_TYPE]: (diff.parallel_diff_lines || []).reduce((acc, line) => {
- if (line.left) {
- acc.push(line.left);
- }
-
- if (line.right) {
- acc.push(line.right);
- }
-
- return acc;
- }, []),
- };
- return lines[window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType].some(
- l => l.discussions.length >= 1,
- );
+export const diffHasDiscussions = () => diff => {
+ return diff[INLINE_DIFF_LINES_KEY].some(l => l.discussions.length >= 1);
};
/**
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 19a9e65edc9..3223d61e48b 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -35,7 +35,6 @@ export const RECEIVE_FULL_DIFF_SUCCESS = 'RECEIVE_FULL_DIFF_SUCCESS';
export const RECEIVE_FULL_DIFF_ERROR = 'RECEIVE_FULL_DIFF_ERROR';
export const SET_FILE_COLLAPSED = 'SET_FILE_COLLAPSED';
-export const SET_HIDDEN_VIEW_DIFF_FILE_LINES = 'SET_HIDDEN_VIEW_DIFF_FILE_LINES';
export const SET_CURRENT_VIEW_DIFF_FILE_LINES = 'SET_CURRENT_VIEW_DIFF_FILE_LINES';
export const ADD_CURRENT_VIEW_DIFF_FILE_LINES = 'ADD_CURRENT_VIEW_DIFF_FILE_LINES';
export const TOGGLE_DIFF_FILE_RENDERING_MORE = 'TOGGLE_DIFF_FILE_RENDERING_MORE';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 096c4f69439..c5bb2b40163 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -1,11 +1,6 @@
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import {
- DIFF_FILE_MANUAL_COLLAPSE,
- DIFF_FILE_AUTOMATIC_COLLAPSE,
- INLINE_DIFF_VIEW_TYPE,
-} from '../constants';
-import {
findDiffFile,
addLineReferences,
removeMatchLine,
@@ -14,6 +9,11 @@ import {
isDiscussionApplicableToLine,
updateLineInFile,
} from './utils';
+import {
+ DIFF_FILE_MANUAL_COLLAPSE,
+ DIFF_FILE_AUTOMATIC_COLLAPSE,
+ INLINE_DIFF_LINES_KEY,
+} from '../constants';
import * as types from './mutation_types';
function updateDiffFilesInState(state, files) {
@@ -109,25 +109,7 @@ export default {
if (!diffFile) return;
- if (diffFile.highlighted_diff_lines.length) {
- diffFile.highlighted_diff_lines.find(l => l.line_code === lineCode).hasForm = hasForm;
- }
-
- if (diffFile.parallel_diff_lines.length) {
- const line = diffFile.parallel_diff_lines.find(l => {
- const { left, right } = l;
-
- return (left && left.line_code === lineCode) || (right && right.line_code === lineCode);
- });
-
- if (line.left && line.left.line_code === lineCode) {
- line.left.hasForm = hasForm;
- }
-
- if (line.right && line.right.line_code === lineCode) {
- line.right.hasForm = hasForm;
- }
- }
+ diffFile[INLINE_DIFF_LINES_KEY].find(l => l.line_code === lineCode).hasForm = hasForm;
},
[types.ADD_CONTEXT_LINES](state, options) {
@@ -157,11 +139,7 @@ export default {
});
addContextLines({
- inlineLines: diffFile.highlighted_diff_lines,
- parallelLines: diffFile.parallel_diff_lines,
- diffViewType: window.gon?.features?.unifiedDiffLines
- ? INLINE_DIFF_VIEW_TYPE
- : state.diffViewType,
+ inlineLines: diffFile[INLINE_DIFF_LINES_KEY],
contextLines: lines,
bottom,
lineNumbers,
@@ -219,8 +197,8 @@ export default {
state.diffFiles.forEach(file => {
if (file.file_hash === fileHash) {
- if (file.highlighted_diff_lines.length) {
- file.highlighted_diff_lines.forEach(line => {
+ if (file[INLINE_DIFF_LINES_KEY].length) {
+ file[INLINE_DIFF_LINES_KEY].forEach(line => {
Object.assign(
line,
setDiscussionsExpanded(lineCheck(line) ? mapDiscussions(line) : line),
@@ -228,25 +206,7 @@ export default {
});
}
- if (file.parallel_diff_lines.length) {
- file.parallel_diff_lines.forEach(line => {
- const left = line.left && lineCheck(line.left);
- const right = line.right && lineCheck(line.right);
-
- if (left || right) {
- Object.assign(line, {
- left: line.left ? setDiscussionsExpanded(mapDiscussions(line.left)) : null,
- right: line.right
- ? setDiscussionsExpanded(mapDiscussions(line.right, () => !left))
- : null,
- });
- }
-
- return line;
- });
- }
-
- if (!file.parallel_diff_lines.length || !file.highlighted_diff_lines.length) {
+ if (!file[INLINE_DIFF_LINES_KEY].length) {
const newDiscussions = (file.discussions || [])
.filter(d => d.id !== discussion.id)
.concat(discussion);
@@ -369,31 +329,15 @@ export default {
renderFile(file);
}
},
- [types.SET_HIDDEN_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) {
- const file = state.diffFiles.find(f => f.file_path === filePath);
- const hiddenDiffLinesKey =
- state.diffViewType === 'inline' ? 'parallel_diff_lines' : 'highlighted_diff_lines';
-
- file[hiddenDiffLinesKey] = lines;
- },
[types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) {
const file = state.diffFiles.find(f => f.file_path === filePath);
- let currentDiffLinesKey;
-
- if (window.gon?.features?.unifiedDiffLines || state.diffViewType === 'inline') {
- currentDiffLinesKey = 'highlighted_diff_lines';
- } else {
- currentDiffLinesKey = 'parallel_diff_lines';
- }
- file[currentDiffLinesKey] = lines;
+ file[INLINE_DIFF_LINES_KEY] = lines;
},
[types.ADD_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, line }) {
const file = state.diffFiles.find(f => f.file_path === filePath);
- const currentDiffLinesKey =
- state.diffViewType === 'inline' ? 'highlighted_diff_lines' : 'parallel_diff_lines';
- file[currentDiffLinesKey].push(line);
+ file[INLINE_DIFF_LINES_KEY].push(line);
},
[types.TOGGLE_DIFF_FILE_RENDERING_MORE](state, filePath) {
const file = state.diffFiles.find(f => f.file_path === filePath);
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index f87f57c32c3..509a89d52f6 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -12,8 +12,7 @@ import {
MATCH_LINE_TYPE,
LINES_TO_BE_RENDERED_DIRECTLY,
TREE_TYPE,
- INLINE_DIFF_VIEW_TYPE,
- PARALLEL_DIFF_VIEW_TYPE,
+ INLINE_DIFF_LINES_KEY,
SHOW_WHITESPACE,
NO_SHOW_WHITESPACE,
} from '../constants';
@@ -178,43 +177,16 @@ export const findIndexInInlineLines = (lines, lineNumbers) => {
);
};
-export const findIndexInParallelLines = (lines, lineNumbers) => {
- const { oldLineNumber, newLineNumber } = lineNumbers;
-
- return lines.findIndex(
- line =>
- line.left &&
- line.right &&
- line.left.old_line === oldLineNumber &&
- line.right.new_line === newLineNumber,
- );
-};
-
-const indexGettersByViewType = {
- [INLINE_DIFF_VIEW_TYPE]: findIndexInInlineLines,
- [PARALLEL_DIFF_VIEW_TYPE]: findIndexInParallelLines,
-};
-
export const getPreviousLineIndex = (diffViewType, file, lineNumbers) => {
- const findIndex = indexGettersByViewType[diffViewType];
- const lines = {
- [INLINE_DIFF_VIEW_TYPE]: file.highlighted_diff_lines,
- [PARALLEL_DIFF_VIEW_TYPE]: file.parallel_diff_lines,
- };
-
- return findIndex && findIndex(lines[diffViewType], lineNumbers);
+ return findIndexInInlineLines(file[INLINE_DIFF_LINES_KEY], lineNumbers);
};
export function removeMatchLine(diffFile, lineNumbers, bottom) {
- const indexForInline = findIndexInInlineLines(diffFile.highlighted_diff_lines, lineNumbers);
- const indexForParallel = findIndexInParallelLines(diffFile.parallel_diff_lines, lineNumbers);
+ const indexForInline = findIndexInInlineLines(diffFile[INLINE_DIFF_LINES_KEY], lineNumbers);
const factor = bottom ? 1 : -1;
if (indexForInline > -1) {
- diffFile.highlighted_diff_lines.splice(indexForInline + factor, 1);
- }
- if (indexForParallel > -1) {
- diffFile.parallel_diff_lines.splice(indexForParallel + factor, 1);
+ diffFile[INLINE_DIFF_LINES_KEY].splice(indexForInline + factor, 1);
}
}
@@ -257,24 +229,6 @@ export function addLineReferences(lines, lineNumbers, bottom, isExpandDown, next
return linesWithNumbers;
}
-function addParallelContextLines(options) {
- const { parallelLines, contextLines, lineNumbers, isExpandDown } = options;
- const normalizedParallelLines = contextLines.map(line => ({
- left: line,
- right: line,
- line_code: line.line_code,
- }));
- const factor = isExpandDown ? 1 : 0;
-
- if (!isExpandDown && options.bottom) {
- parallelLines.push(...normalizedParallelLines);
- } else {
- const parallelIndex = findIndexInParallelLines(parallelLines, lineNumbers);
-
- parallelLines.splice(parallelIndex + factor, 0, ...normalizedParallelLines);
- }
-}
-
function addInlineContextLines(options) {
const { inlineLines, contextLines, lineNumbers, isExpandDown } = options;
const factor = isExpandDown ? 1 : 0;
@@ -289,16 +243,7 @@ function addInlineContextLines(options) {
}
export function addContextLines(options) {
- const { diffViewType } = options;
- const contextLineHandlers = {
- [INLINE_DIFF_VIEW_TYPE]: addInlineContextLines,
- [PARALLEL_DIFF_VIEW_TYPE]: addParallelContextLines,
- };
- const contextLineHandler = contextLineHandlers[diffViewType];
-
- if (contextLineHandler) {
- contextLineHandler(options);
- }
+ addInlineContextLines(options);
}
/**
@@ -324,41 +269,29 @@ export function trimFirstCharOfLineContent(line = {}) {
return parsedLine;
}
-function getLineCode({ left, right }, index) {
- if (left && left.line_code) {
- return left.line_code;
- } else if (right && right.line_code) {
- return right.line_code;
- }
- return index;
-}
-
function diffFileUniqueId(file) {
return `${file.content_sha}-${file.file_hash}`;
}
function mergeTwoFiles(target, source) {
- const originalInline = target.highlighted_diff_lines;
- const originalParallel = target.parallel_diff_lines;
+ const originalInline = target[INLINE_DIFF_LINES_KEY];
const missingInline = !originalInline.length;
- const missingParallel = !originalParallel.length;
return {
...target,
- highlighted_diff_lines: missingInline ? source.highlighted_diff_lines : originalInline,
- parallel_diff_lines: missingParallel ? source.parallel_diff_lines : originalParallel,
+ [INLINE_DIFF_LINES_KEY]: missingInline ? source[INLINE_DIFF_LINES_KEY] : originalInline,
+ parallel_diff_lines: null,
renderIt: source.renderIt,
collapsed: source.collapsed,
};
}
function ensureBasicDiffFileLines(file) {
- const missingInline = !file.highlighted_diff_lines;
- const missingParallel = !file.parallel_diff_lines || window.gon?.features?.unifiedDiffLines;
+ const missingInline = !file[INLINE_DIFF_LINES_KEY];
Object.assign(file, {
- highlighted_diff_lines: missingInline ? [] : file.highlighted_diff_lines,
- parallel_diff_lines: missingParallel ? [] : file.parallel_diff_lines,
+ [INLINE_DIFF_LINES_KEY]: missingInline ? [] : file[INLINE_DIFF_LINES_KEY],
+ parallel_diff_lines: null,
});
return file;
@@ -382,7 +315,7 @@ function prepareLine(line, file) {
}
}
-export function prepareLineForRenamedFile({ line, diffViewType, diffFile, index = 0 }) {
+export function prepareLineForRenamedFile({ line, diffFile, index = 0 }) {
/*
Renamed files are a little different than other diffs, which
is why this is distinct from `prepareDiffFileLines` below.
@@ -407,48 +340,23 @@ export function prepareLineForRenamedFile({ line, diffViewType, diffFile, index
prepareLine(cleanLine, diffFile); // WARNING: In-Place Mutations!
- if (diffViewType === PARALLEL_DIFF_VIEW_TYPE) {
- return {
- left: { ...cleanLine },
- right: { ...cleanLine },
- line_code: cleanLine.line_code,
- };
- }
-
return cleanLine;
}
function prepareDiffFileLines(file) {
- const inlineLines = file.highlighted_diff_lines;
- const parallelLines = file.parallel_diff_lines;
- let parallelLinesCount = 0;
+ const inlineLines = file[INLINE_DIFF_LINES_KEY];
inlineLines.forEach(line => prepareLine(line, file)); // WARNING: In-Place Mutations!
- parallelLines.forEach((line, index) => {
- Object.assign(line, { line_code: getLineCode(line, index) });
-
- if (line.left) {
- parallelLinesCount += 1;
- prepareLine(line.left, file); // WARNING: In-Place Mutations!
- }
-
- if (line.right) {
- parallelLinesCount += 1;
- prepareLine(line.right, file); // WARNING: In-Place Mutations!
- }
- });
-
Object.assign(file, {
inlineLinesCount: inlineLines.length,
- parallelLinesCount,
});
return file;
}
function getVisibleDiffLines(file) {
- return Math.max(file.inlineLinesCount, file.parallelLinesCount);
+ return file.inlineLinesCount;
}
function finalizeDiffFile(file) {
@@ -490,43 +398,14 @@ export function prepareDiffData(diff, priorFiles = []) {
export function getDiffPositionByLineCode(diffFiles) {
let lines = [];
- const hasInlineDiffs = diffFiles.some(file => file.highlighted_diff_lines.length > 0);
-
- if (hasInlineDiffs) {
- // In either of these cases, we can use `highlighted_diff_lines` because
- // that will include all of the parallel diff lines, too
-
- lines = diffFiles.reduce((acc, diffFile) => {
- diffFile.highlighted_diff_lines.forEach(line => {
- acc.push({ file: diffFile, line });
- });
-
- return acc;
- }, []);
- } else {
- // If we're in single diff view mode and the inline lines haven't been
- // loaded yet, we need to parse the parallel lines
-
- lines = diffFiles.reduce((acc, diffFile) => {
- diffFile.parallel_diff_lines.forEach(pair => {
- // It's possible for a parallel line to have an opposite line that doesn't exist
- // For example: *deleted* lines will have `null` right lines, while
- // *added* lines will have `null` left lines.
- // So we have to check each line before we push it onto the array so we're not
- // pushing null line diffs
-
- if (pair.left) {
- acc.push({ file: diffFile, line: pair.left });
- }
- if (pair.right) {
- acc.push({ file: diffFile, line: pair.right });
- }
- });
+ lines = diffFiles.reduce((acc, diffFile) => {
+ diffFile[INLINE_DIFF_LINES_KEY].forEach(line => {
+ acc.push({ file: diffFile, line });
+ });
- return acc;
- }, []);
- }
+ return acc;
+ }, []);
return lines.reduce((acc, { file, line }) => {
if (line.line_code) {
@@ -739,24 +618,10 @@ export const convertExpandLines = ({
export const idleCallback = cb => requestIdleCallback(cb);
function getLinesFromFileByLineCode(file, lineCode) {
- const parallelLines = file.parallel_diff_lines;
- const inlineLines = file.highlighted_diff_lines;
+ const inlineLines = file[INLINE_DIFF_LINES_KEY];
const matchesCode = line => line.line_code === lineCode;
- return [
- ...parallelLines.reduce((acc, line) => {
- if (line.left) {
- acc.push(line.left);
- }
-
- if (line.right) {
- acc.push(line.right);
- }
-
- return acc;
- }, []),
- ...inlineLines,
- ].filter(matchesCode);
+ return inlineLines.filter(matchesCode);
}
export const updateLineInFile = (selectedFile, lineCode, updateFn) => {
@@ -771,12 +636,7 @@ export const allDiscussionWrappersExpanded = diff => {
}
};
- diff.parallel_diff_lines.forEach(line => {
- changeExpandedResult(line.left);
- changeExpandedResult(line.right);
- });
-
- diff.highlighted_diff_lines.forEach(line => {
+ diff[INLINE_DIFF_LINES_KEY].forEach(line => {
changeExpandedResult(line);
});
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index bc35a07fe4a..2192d456861 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { formatTime } from '~/lib/utils/datetime_utility';
import eventHub from '../event_hub';
@@ -9,7 +9,8 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- GlButton,
+ GlDropdown,
+ GlDropdownItem,
GlIcon,
GlLoadingIcon,
},
@@ -35,7 +36,7 @@ export default {
if (action.scheduledAt) {
const confirmationMessage = sprintf(
s__(
- "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.",
+ 'DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after its timer finishes.',
),
{ jobName: action.name },
);
@@ -67,40 +68,32 @@ export default {
};
</script>
<template>
- <div class="btn-group" role="group">
- <gl-button
- v-gl-tooltip
- :title="title"
- :aria-label="title"
- :disabled="isLoading"
- class="dropdown dropdown-new js-environment-actions-dropdown"
- data-container="body"
- data-toggle="dropdown"
- data-testid="environment-actions-button"
+ <gl-dropdown
+ v-gl-tooltip
+ :title="title"
+ :aria-label="title"
+ :disabled="isLoading"
+ right
+ data-container="body"
+ data-testid="environment-actions-button"
+ >
+ <template #button-content>
+ <gl-icon name="play" />
+ <gl-icon name="chevron-down" />
+ <gl-loading-icon v-if="isLoading" />
+ </template>
+ <gl-dropdown-item
+ v-for="(action, i) in actions"
+ :key="i"
+ :disabled="isActionDisabled(action)"
+ data-testid="manual-action-link"
+ @click="onClickAction(action)"
>
- <span>
- <gl-icon name="play" />
- <gl-icon name="chevron-down" />
- <gl-loading-icon v-if="isLoading" />
+ <span class="gl-flex-fill-1">{{ action.name }}</span>
+ <span v-if="action.scheduledAt" class="gl-text-gray-500 float-right">
+ <gl-icon name="clock" />
+ {{ remainingTime(action) }}
</span>
- </gl-button>
-
- <ul class="dropdown-menu dropdown-menu-right">
- <li v-for="(action, i) in actions" :key="i" class="gl-display-flex">
- <gl-button
- :class="{ disabled: isActionDisabled(action) }"
- :disabled="isActionDisabled(action)"
- variant="link"
- class="js-manual-action-link gl-flex-fill-1"
- @click="onClickAction(action)"
- >
- <span class="gl-flex-fill-1">{{ action.name }}</span>
- <span v-if="action.scheduledAt" class="text-secondary float-right">
- <gl-icon name="clock" />
- {{ remainingTime(action) }}
- </span>
- </gl-button>
- </li>
- </ul>
- </div>
+ </gl-dropdown-item>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 6e99b6ad4fa..ef58b93c049 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -74,6 +74,9 @@ export default {
visibilityTooltip() {
return GROUP_VISIBILITY_TYPE[this.group.visibility];
},
+ microdata() {
+ return this.group.microdata || {};
+ },
},
mounted() {
if (this.group.name === 'Learn GitLab') {
@@ -99,7 +102,15 @@ export default {
</script>
<template>
- <li :id="groupDomId" :class="rowClass" class="group-row" @click.stop="onClickRowGroup">
+ <li
+ :id="groupDomId"
+ :class="rowClass"
+ class="group-row"
+ :itemprop="microdata.itemprop"
+ :itemtype="microdata.itemtype"
+ :itemscope="microdata.itemscope"
+ @click.stop="onClickRowGroup"
+ >
<div
:class="{ 'project-row-contents': !isGroup }"
class="group-row-contents d-flex align-items-center py-2 pr-3"
@@ -118,7 +129,13 @@ export default {
class="avatar-container rect-avatar s32 d-none flex-grow-0 flex-shrink-0 "
>
<a :href="group.relativePath" class="no-expand">
- <img v-if="hasAvatar" :src="group.avatarUrl" class="avatar s40" />
+ <img
+ v-if="hasAvatar"
+ :src="group.avatarUrl"
+ data-testid="group-avatar"
+ class="avatar s40"
+ :itemprop="microdata.imageItemprop"
+ />
<identicon v-else :entity-id="group.id" :entity-name="group.name" size-class="s40" />
</a>
</div>
@@ -127,9 +144,11 @@ export default {
<div class="d-flex align-items-center flex-wrap title namespace-title gl-mr-3">
<a
v-gl-tooltip.bottom
+ data-testid="group-name"
:href="group.relativePath"
:title="group.fullName"
class="no-expand gl-mt-3 gl-mr-3 gl-text-gray-900!"
+ :itemprop="microdata.nameItemprop"
>{{
// ending bracket must be by closing tag to prevent
// link hover text-decoration from over-extending
@@ -146,7 +165,12 @@ export default {
</span>
</div>
<div v-if="group.description" class="description">
- <span v-html="group.description"> </span>
+ <span
+ :itemprop="microdata.descriptionItemprop"
+ data-testid="group-description"
+ v-html="group.description"
+ >
+ </span>
</div>
</div>
<div v-if="isGroupPendingRemoval">
diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js
index 522f1d16df2..e11c3aaf984 100644
--- a/app/assets/javascripts/groups/index.js
+++ b/app/assets/javascripts/groups/index.js
@@ -47,8 +47,9 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
data() {
const { dataset } = dataEl || this.$options.el;
const hideProjects = parseBoolean(dataset.hideProjects);
+ const showSchemaMarkup = parseBoolean(dataset.showSchemaMarkup);
const service = new GroupsService(endpoint || dataset.endpoint);
- const store = new GroupsStore(hideProjects);
+ const store = new GroupsStore({ hideProjects, showSchemaMarkup });
return {
action,
diff --git a/app/assets/javascripts/groups/members/components/app.vue b/app/assets/javascripts/groups/members/components/app.vue
index 2e6dd4a0bad..8f1bb6e8094 100644
--- a/app/assets/javascripts/groups/members/components/app.vue
+++ b/app/assets/javascripts/groups/members/components/app.vue
@@ -1,9 +1,9 @@
<script>
import { mapState, mapMutations } from 'vuex';
import { GlAlert } from '@gitlab/ui';
-import MembersTable from '~/vue_shared/components/members/table/members_table.vue';
+import MembersTable from '~/members/components/table/members_table.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
-import { HIDE_ERROR } from '~/vuex_shared/modules/members/mutation_types';
+import { HIDE_ERROR } from '~/members/store/mutation_types';
export default {
name: 'GroupMembersApp',
diff --git a/app/assets/javascripts/groups/members/index.js b/app/assets/javascripts/groups/members/index.js
index cb28fb057c9..68caf6628f6 100644
--- a/app/assets/javascripts/groups/members/index.js
+++ b/app/assets/javascripts/groups/members/index.js
@@ -3,7 +3,7 @@ import Vuex from 'vuex';
import { GlToast } from '@gitlab/ui';
import { parseDataAttributes } from 'ee_else_ce/groups/members/utils';
import App from './components/app.vue';
-import membersModule from '~/vuex_shared/modules/members';
+import membersStore from '~/members/store';
export const initGroupMembersApp = (el, tableFields, tableAttrs, requestFormatter) => {
if (!el) {
@@ -13,15 +13,15 @@ export const initGroupMembersApp = (el, tableFields, tableAttrs, requestFormatte
Vue.use(Vuex);
Vue.use(GlToast);
- const store = new Vuex.Store({
- ...membersModule({
+ const store = new Vuex.Store(
+ membersStore({
...parseDataAttributes(el),
currentUserId: gon.current_user_id || null,
tableFields,
tableAttrs,
requestFormatter,
}),
- });
+ );
return new Vue({
el,
diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js
index 6a1197fa163..b6cea38e87f 100644
--- a/app/assets/javascripts/groups/store/groups_store.js
+++ b/app/assets/javascripts/groups/store/groups_store.js
@@ -1,11 +1,13 @@
import { normalizeHeaders, parseIntPagination } from '../../lib/utils/common_utils';
+import { getGroupItemMicrodata } from './utils';
export default class GroupsStore {
- constructor(hideProjects) {
+ constructor({ hideProjects = false, showSchemaMarkup = false } = {}) {
this.state = {};
this.state.groups = [];
this.state.pageInfo = {};
this.hideProjects = hideProjects;
+ this.showSchemaMarkup = showSchemaMarkup;
}
setGroups(rawGroups) {
@@ -94,6 +96,7 @@ export default class GroupsStore {
starCount: rawGroupItem.star_count,
updatedAt: rawGroupItem.updated_at,
pendingRemoval: rawGroupItem.marked_for_deletion,
+ microdata: this.showSchemaMarkup ? getGroupItemMicrodata(rawGroupItem) : {},
};
}
diff --git a/app/assets/javascripts/groups/store/utils.js b/app/assets/javascripts/groups/store/utils.js
new file mode 100644
index 00000000000..371b3aa9d52
--- /dev/null
+++ b/app/assets/javascripts/groups/store/utils.js
@@ -0,0 +1,27 @@
+export const getGroupItemMicrodata = ({ type }) => {
+ const defaultMicrodata = {
+ itemscope: true,
+ itemtype: 'https://schema.org/Thing',
+ itemprop: 'owns',
+ imageItemprop: 'image',
+ nameItemprop: 'name',
+ descriptionItemprop: 'description',
+ };
+
+ switch (type) {
+ case 'group':
+ return {
+ ...defaultMicrodata,
+ itemtype: 'https://schema.org/Organization',
+ itemprop: 'subOrganization',
+ imageItemprop: 'logo',
+ };
+ case 'project':
+ return {
+ ...defaultMicrodata,
+ itemtype: 'https://schema.org/SoftwareSourceCode',
+ };
+ default:
+ return defaultMicrodata;
+ }
+};
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index aac23db8fd6..29af8c77d25 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -4,97 +4,107 @@ import axios from './lib/utils/axios_utils';
import Api from './api';
import { normalizeHeaders } from './lib/utils/common_utils';
import { __ } from '~/locale';
+import { loadCSSFile } from './lib/utils/css_utils';
+
+const fetchGroups = params => {
+ axios[params.type.toLowerCase()](params.url, {
+ params: params.data,
+ })
+ .then(res => {
+ const results = res.data || [];
+ const headers = normalizeHeaders(res.headers);
+ const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
+ const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
+ const more = currentPage < totalPages;
+
+ params.success({
+ results,
+ pagination: {
+ more,
+ },
+ });
+ })
+ .catch(params.error);
+};
const groupsSelect = () => {
- // Needs to be accessible in rspec
- window.GROUP_SELECT_PER_PAGE = 20;
- $('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
- const $select = $(this);
- const allAvailable = $select.data('allAvailable');
- const skipGroups = $select.data('skipGroups') || [];
- const parentGroupID = $select.data('parentId');
- const groupsPath = parentGroupID
- ? Api.subgroupsPath.replace(':id', parentGroupID)
- : Api.groupsPath;
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ // Needs to be accessible in rspec
+ window.GROUP_SELECT_PER_PAGE = 20;
- $select.select2({
- placeholder: __('Search for a group'),
- allowClear: $select.hasClass('allowClear'),
- multiple: $select.hasClass('multiselect'),
- minimumInputLength: 0,
- ajax: {
- url: Api.buildUrl(groupsPath),
- dataType: 'json',
- quietMillis: 250,
- transport(params) {
- axios[params.type.toLowerCase()](params.url, {
- params: params.data,
- })
- .then(res => {
- const results = res.data || [];
- const headers = normalizeHeaders(res.headers);
- const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
- const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
- const more = currentPage < totalPages;
+ $('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
+ const $select = $(this);
+ const allAvailable = $select.data('allAvailable');
+ const skipGroups = $select.data('skipGroups') || [];
+ const parentGroupID = $select.data('parentId');
+ const groupsPath = parentGroupID
+ ? Api.subgroupsPath.replace(':id', parentGroupID)
+ : Api.groupsPath;
- params.success({
- results,
- pagination: {
- more,
- },
- });
- })
- .catch(params.error);
- },
- data(search, page) {
- return {
- search,
- page,
- per_page: window.GROUP_SELECT_PER_PAGE,
- all_available: allAvailable,
- };
- },
- results(data, page) {
- if (data.length) return { results: [] };
+ $select.select2({
+ placeholder: __('Search for a group'),
+ allowClear: $select.hasClass('allowClear'),
+ multiple: $select.hasClass('multiselect'),
+ minimumInputLength: 0,
+ ajax: {
+ url: Api.buildUrl(groupsPath),
+ dataType: 'json',
+ quietMillis: 250,
+ transport(params) {
+ fetchGroups(params);
+ },
+ data(search, page) {
+ return {
+ search,
+ page,
+ per_page: window.GROUP_SELECT_PER_PAGE,
+ all_available: allAvailable,
+ };
+ },
+ results(data, page) {
+ if (data.length) return { results: [] };
- const groups = data.length ? data : data.results || [];
- const more = data.pagination ? data.pagination.more : false;
- const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
+ const groups = data.length ? data : data.results || [];
+ const more = data.pagination ? data.pagination.more : false;
+ const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
- return {
- results,
- page,
- more,
- };
- },
- },
- // eslint-disable-next-line consistent-return
- initSelection(element, callback) {
- const id = $(element).val();
- if (id !== '') {
- return Api.group(id, callback);
- }
- },
- formatResult(object) {
- return `<div class='group-result'> <div class='group-name'>${escape(
- object.full_name,
- )}</div> <div class='group-path'>${object.full_path}</div> </div>`;
- },
- formatSelection(object) {
- return escape(object.full_name);
- },
- dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup(m) {
- return m;
- },
- });
+ return {
+ results,
+ page,
+ more,
+ };
+ },
+ },
+ // eslint-disable-next-line consistent-return
+ initSelection(element, callback) {
+ const id = $(element).val();
+ if (id !== '') {
+ return Api.group(id, callback);
+ }
+ },
+ formatResult(object) {
+ return `<div class='group-result'> <div class='group-name'>${escape(
+ object.full_name,
+ )}</div> <div class='group-path'>${object.full_path}</div> </div>`;
+ },
+ formatSelection(object) {
+ return escape(object.full_name);
+ },
+ dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup(m) {
+ return m;
+ },
+ });
- $select.on('select2-loaded', () => {
- const dropdown = document.querySelector('.select2-infinite .select2-results');
- dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
- });
- });
+ $select.on('select2-loaded', () => {
+ const dropdown = document.querySelector('.select2-infinite .select2-results');
+ dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
+ });
+ });
+ })
+ .catch(() => {});
};
export default () => {
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
index dec8aa61838..52593aabfea 100644
--- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -1,11 +1,12 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { viewerTypes } from '../constants';
export default {
components: {
- GlButton,
+ GlDropdown,
+ GlDropdownItem,
},
props: {
viewer: {
@@ -18,10 +19,21 @@ export default {
},
},
computed: {
- mergeReviewLine() {
- return sprintf(__('Reviewing (merge request !%{mergeRequestId})'), {
- mergeRequestId: this.mergeRequestId,
- });
+ modeDropdownItems() {
+ return [
+ {
+ viewerType: this.$options.viewerTypes.mr,
+ title: sprintf(__('Reviewing (merge request !%{mergeRequestId})'), {
+ mergeRequestId: this.mergeRequestId,
+ }),
+ content: __('Compare changes with the merge request target branch'),
+ },
+ {
+ viewerType: this.$options.viewerTypes.diff,
+ title: __('Reviewing'),
+ content: __('Compare changes with the last commit'),
+ },
+ ];
},
},
methods: {
@@ -34,39 +46,16 @@ export default {
</script>
<template>
- <div class="dropdown">
- <gl-button variant="link" data-toggle="dropdown">{{ __('Edit') }}</gl-button>
- <div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
- <ul>
- <li>
- <a
- :class="{
- 'is-active': viewer === $options.viewerTypes.mr,
- }"
- href="#"
- @click.prevent="changeMode($options.viewerTypes.mr)"
- >
- <strong class="dropdown-menu-inner-title"> {{ mergeReviewLine }} </strong>
- <span class="dropdown-menu-inner-content">
- {{ __('Compare changes with the merge request target branch') }}
- </span>
- </a>
- </li>
- <li>
- <a
- :class="{
- 'is-active': viewer === $options.viewerTypes.diff,
- }"
- href="#"
- @click.prevent="changeMode($options.viewerTypes.diff)"
- >
- <strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
- <span class="dropdown-menu-inner-content">
- {{ __('Compare changes with the last commit') }}
- </span>
- </a>
- </li>
- </ul>
- </div>
- </div>
+ <gl-dropdown :text="__('Edit')" size="small">
+ <gl-dropdown-item
+ v-for="mode in modeDropdownItems"
+ :key="mode.viewerType"
+ :is-check-item="true"
+ :is-checked="viewer === mode.viewerType"
+ @click="changeMode(mode.viewerType)"
+ >
+ <strong class="dropdown-menu-inner-title"> {{ mode.title }} </strong>
+ <span class="dropdown-menu-inner-content"> {{ mode.content }} </span>
+ </gl-dropdown-item>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index e1d2895831a..f1dc855362b 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -5,10 +5,8 @@ import {
WEBIDE_MARK_APP_START,
WEBIDE_MARK_FILE_FINISH,
WEBIDE_MARK_FILE_CLICKED,
- WEBIDE_MARK_TREE_FINISH,
- WEBIDE_MEASURE_TREE_FROM_REQUEST,
- WEBIDE_MEASURE_FILE_FROM_REQUEST,
WEBIDE_MEASURE_FILE_AFTER_INTERACTION,
+ WEBIDE_MEASURE_BEFORE_VUE,
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
import { modalTypes } from '../constants';
@@ -19,12 +17,6 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { measurePerformance } from '../utils';
-eventHub.$on(WEBIDE_MEASURE_TREE_FROM_REQUEST, () =>
- measurePerformance(WEBIDE_MARK_TREE_FINISH, WEBIDE_MEASURE_TREE_FROM_REQUEST),
-);
-eventHub.$on(WEBIDE_MEASURE_FILE_FROM_REQUEST, () =>
- measurePerformance(WEBIDE_MARK_FILE_FINISH, WEBIDE_MEASURE_FILE_FROM_REQUEST),
-);
eventHub.$on(WEBIDE_MEASURE_FILE_AFTER_INTERACTION, () =>
measurePerformance(
WEBIDE_MARK_FILE_FINISH,
@@ -84,7 +76,14 @@ export default {
document.querySelector('.navbar-gitlab').classList.add(`theme-${this.themeName}`);
},
beforeCreate() {
- performanceMarkAndMeasure({ mark: WEBIDE_MARK_APP_START });
+ performanceMarkAndMeasure({
+ mark: WEBIDE_MARK_APP_START,
+ measures: [
+ {
+ name: WEBIDE_MEASURE_BEFORE_VUE,
+ },
+ ],
+ });
},
methods: {
...mapActions(['toggleFileFinder']),
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index e7e94f5b5da..b67881b14f4 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -2,17 +2,13 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import FileTree from '~/vue_shared/components/file_tree.vue';
-import {
- WEBIDE_MARK_TREE_START,
- WEBIDE_MEASURE_TREE_FROM_REQUEST,
- WEBIDE_MARK_FILE_CLICKED,
-} from '~/performance/constants';
+import { WEBIDE_MARK_FILE_CLICKED } from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
-import eventHub from '../eventhub';
import IdeFileRow from './ide_file_row.vue';
import NavDropdown from './nav_dropdown.vue';
export default {
+ name: 'IdeTreeList',
components: {
GlSkeletonLoading,
NavDropdown,
@@ -39,14 +35,6 @@ export default {
}
},
},
- beforeCreate() {
- performanceMarkAndMeasure({ mark: WEBIDE_MARK_TREE_START });
- },
- updated() {
- if (this.currentTree?.tree?.length) {
- eventHub.$emit(WEBIDE_MEASURE_TREE_FROM_REQUEST);
- }
- },
methods: {
...mapActions(['toggleTreeOpen']),
clickedFile() {
diff --git a/app/assets/javascripts/ide/components/nav_dropdown.vue b/app/assets/javascripts/ide/components/nav_dropdown.vue
index 2307efd1d24..a2338c6dec5 100644
--- a/app/assets/javascripts/ide/components/nav_dropdown.vue
+++ b/app/assets/javascripts/ide/components/nav_dropdown.vue
@@ -45,7 +45,7 @@ export default {
</script>
<template>
- <div ref="dropdown" class="btn-group ide-nav-dropdown dropdown">
+ <div ref="dropdown" class="btn-group ide-nav-dropdown dropdown" data-testid="ide-nav-dropdown">
<nav-dropdown-button :show-merge-requests="canReadMergeRequests" />
<div class="dropdown-menu dropdown-menu-left p-0">
<nav-form v-if="isVisibleDropdown" :show-merge-requests="canReadMergeRequests" />
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index c8a825065f1..1f029612c29 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -6,9 +6,10 @@ import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import {
WEBIDE_MARK_FILE_CLICKED,
- WEBIDE_MARK_FILE_START,
+ WEBIDE_MARK_REPO_EDITOR_START,
+ WEBIDE_MARK_REPO_EDITOR_FINISH,
+ WEBIDE_MEASURE_REPO_EDITOR,
WEBIDE_MEASURE_FILE_AFTER_INTERACTION,
- WEBIDE_MEASURE_FILE_FROM_REQUEST,
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
import eventHub from '../eventhub';
@@ -28,6 +29,7 @@ import { getRulesWithTraversal } from '../lib/editorconfig/parser';
import mapRulesToMonaco from '../lib/editorconfig/rules_mapper';
export default {
+ name: 'RepoEditor',
components: {
ContentViewer,
DiffViewer,
@@ -175,9 +177,6 @@ export default {
}
},
},
- beforeCreate() {
- performanceMarkAndMeasure({ mark: WEBIDE_MARK_FILE_START });
- },
beforeDestroy() {
this.editor.dispose();
},
@@ -204,6 +203,7 @@ export default {
]),
...mapActions('editor', ['updateFileEditor']),
initEditor() {
+ performanceMarkAndMeasure({ mark: WEBIDE_MARK_REPO_EDITOR_START });
if (this.shouldHideEditor && (this.file.content || this.file.raw)) {
return;
}
@@ -305,7 +305,15 @@ export default {
if (performance.getEntriesByName(WEBIDE_MARK_FILE_CLICKED).length) {
eventHub.$emit(WEBIDE_MEASURE_FILE_AFTER_INTERACTION);
} else {
- eventHub.$emit(WEBIDE_MEASURE_FILE_FROM_REQUEST);
+ performanceMarkAndMeasure({
+ mark: WEBIDE_MARK_REPO_EDITOR_FINISH,
+ measures: [
+ {
+ name: WEBIDE_MEASURE_REPO_EDITOR,
+ start: WEBIDE_MARK_REPO_EDITOR_START,
+ },
+ ],
+ });
}
},
refreshEditorDimensions() {
diff --git a/app/assets/javascripts/ide/components/terminal/session.vue b/app/assets/javascripts/ide/components/terminal/session.vue
index a8fe9ea6866..0e67a2ab45f 100644
--- a/app/assets/javascripts/ide/components/terminal/session.vue
+++ b/app/assets/javascripts/ide/components/terminal/session.vue
@@ -1,5 +1,6 @@
<script>
import { mapActions, mapState } from 'vuex';
+import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import Terminal from './terminal.vue';
import { isEndingStatus } from '../../stores/modules/terminal/utils';
@@ -7,6 +8,7 @@ import { isEndingStatus } from '../../stores/modules/terminal/utils';
export default {
components: {
Terminal,
+ GlButton,
},
computed: {
...mapState('terminal', ['session']),
@@ -14,15 +16,17 @@ export default {
if (isEndingStatus(this.session.status)) {
return {
action: () => this.restartSession(),
+ variant: 'info',
+ category: 'primary',
text: __('Restart Terminal'),
- class: 'btn-primary',
};
}
return {
action: () => this.stopSession(),
+ variant: 'danger',
+ category: 'secondary',
text: __('Stop Terminal'),
- class: 'btn-inverted btn-remove',
};
},
},
@@ -37,15 +41,13 @@ export default {
<header class="ide-job-header d-flex align-items-center">
<h5>{{ __('Web Terminal') }}</h5>
<div class="ml-auto align-self-center">
- <button
+ <gl-button
v-if="actionButton"
- type="button"
- class="btn btn-sm"
- :class="actionButton.class"
+ :variant="actionButton.variant"
+ :category="actionButton.category"
@click="actionButton.action"
+ >{{ actionButton.text }}</gl-button
>
- {{ actionButton.text }}
- </button>
</div>
</header>
<terminal :terminal-path="session.terminalPath" :status="session.status" />
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 396aedbfa10..b9ebacef7e1 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -3,6 +3,12 @@ import IdeRouter from '~/ide/ide_router_extension';
import { joinPaths } from '~/lib/utils/url_utility';
import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
+import { performanceMarkAndMeasure } from '~/performance/utils';
+import {
+ WEBIDE_MARK_FETCH_PROJECT_DATA_START,
+ WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH,
+ WEBIDE_MEASURE_FETCH_PROJECT_DATA,
+} from '~/performance/constants';
import { syncRouterAndStore } from './sync_router_and_store';
Vue.use(IdeRouter);
@@ -69,6 +75,7 @@ export const createRouter = store => {
router.beforeEach((to, from, next) => {
if (to.params.namespace && to.params.project) {
+ performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_PROJECT_DATA_START });
store
.dispatch('getProjectData', {
namespace: to.params.namespace,
@@ -81,6 +88,15 @@ export const createRouter = store => {
const mergeRequestId = to.params.mrid;
if (branchId) {
+ performanceMarkAndMeasure({
+ mark: WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH,
+ measures: [
+ {
+ name: WEBIDE_MEASURE_FETCH_PROJECT_DATA,
+ start: WEBIDE_MARK_FETCH_PROJECT_DATA_START,
+ },
+ ],
+ });
store.dispatch('openBranch', {
projectId,
branchId,
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index 56d48e87c18..62f49ba56b1 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import { mapActions } from 'vuex';
import { identity } from 'lodash';
import Translate from '~/vue_shared/translate';
+import PerformancePlugin from '~/performance/vue_performance_plugin';
import ide from './components/ide.vue';
import { createStore } from './stores';
import { createRouter } from './ide_router';
@@ -11,6 +12,10 @@ import { DEFAULT_THEME } from './lib/themes';
Vue.use(Translate);
+Vue.use(PerformancePlugin, {
+ components: ['FileTree'],
+});
+
/**
* Function that receives the default store and returns an extended one.
* @callback extendStoreCallback
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 1496170447d..710256b6377 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -3,6 +3,12 @@ import { escape } from 'lodash';
import { __, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import { deprecatedCreateFlash as flash } from '~/flash';
+import { performanceMarkAndMeasure } from '~/performance/utils';
+import {
+ WEBIDE_MARK_FETCH_BRANCH_DATA_START,
+ WEBIDE_MARK_FETCH_BRANCH_DATA_FINISH,
+ WEBIDE_MEASURE_FETCH_BRANCH_DATA,
+} from '~/performance/constants';
import * as types from './mutation_types';
import { decorateFiles } from '../lib/files';
import { stageKeys, commitActionTypes } from '../constants';
@@ -245,13 +251,23 @@ export const renameEntry = ({ dispatch, commit, state, getters }, { path, name,
dispatch('triggerFilesChange', { type: commitActionTypes.move, path, newPath });
};
-export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) =>
- new Promise((resolve, reject) => {
+export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) => {
+ return new Promise((resolve, reject) => {
+ performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_BRANCH_DATA_START });
const currentProject = state.projects[projectId];
if (!currentProject || !currentProject.branches[branchId] || force) {
service
.getBranchData(projectId, branchId)
.then(({ data }) => {
+ performanceMarkAndMeasure({
+ mark: WEBIDE_MARK_FETCH_BRANCH_DATA_FINISH,
+ measures: [
+ {
+ name: WEBIDE_MEASURE_FETCH_BRANCH_DATA,
+ start: WEBIDE_MARK_FETCH_BRANCH_DATA_START,
+ },
+ ],
+ });
const { id } = data.commit;
commit(types.SET_BRANCH, {
projectPath: projectId,
@@ -291,6 +307,7 @@ export const getBranchData = ({ commit, state }, { projectId, branchId, force =
resolve(currentProject.branches[branchId]);
}
});
+};
export * from './actions/tree';
export * from './actions/file';
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 4b9b958ddd6..8b43c7238fd 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -1,5 +1,11 @@
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import { performanceMarkAndMeasure } from '~/performance/utils';
+import {
+ WEBIDE_MARK_FETCH_FILE_DATA_START,
+ WEBIDE_MARK_FETCH_FILE_DATA_FINISH,
+ WEBIDE_MEASURE_FETCH_FILE_DATA,
+} from '~/performance/constants';
import eventHub from '../../eventhub';
import service from '../../services';
import * as types from '../mutation_types';
@@ -61,6 +67,7 @@ export const getFileData = (
{ state, commit, dispatch, getters },
{ path, makeFileActive = true, openFile = makeFileActive, toggleLoading = true },
) => {
+ performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_FILE_DATA_START });
const file = state.entries[path];
const fileDeletedAndReadded = getters.isFileDeletedAndReadded(path);
@@ -81,6 +88,15 @@ export const getFileData = (
return service
.getFileData(url)
.then(({ data }) => {
+ performanceMarkAndMeasure({
+ mark: WEBIDE_MARK_FETCH_FILE_DATA_FINISH,
+ measures: [
+ {
+ name: WEBIDE_MEASURE_FETCH_FILE_DATA,
+ start: WEBIDE_MARK_FETCH_FILE_DATA_START,
+ },
+ ],
+ });
if (data) commit(types.SET_FILE_DATA, { data, file });
if (openFile) commit(types.TOGGLE_FILE_OPEN, path);
@@ -150,6 +166,13 @@ export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) =
export const changeFileContent = ({ commit, state, getters }, { path, content }) => {
const file = state.entries[path];
+
+ // It's possible for monaco to hit a race condition where it tries to update renamed files.
+ // See issue https://gitlab.com/gitlab-org/gitlab/-/issues/284930
+ if (!file) {
+ return;
+ }
+
commit(types.UPDATE_FILE_CONTENT, {
path,
content,
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 3a7daf30cc4..23a5e26bc1c 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -1,4 +1,10 @@
import { defer } from 'lodash';
+import { performanceMarkAndMeasure } from '~/performance/utils';
+import {
+ WEBIDE_MARK_FETCH_FILES_FINISH,
+ WEBIDE_MEASURE_FETCH_FILES,
+ WEBIDE_MARK_FETCH_FILES_START,
+} from '~/performance/constants';
import { __ } from '../../../locale';
import service from '../../services';
import * as types from '../mutation_types';
@@ -46,8 +52,9 @@ export const setDirectoryData = ({ state, commit }, { projectId, branchId, treeL
});
};
-export const getFiles = ({ state, commit, dispatch }, payload = {}) =>
- new Promise((resolve, reject) => {
+export const getFiles = ({ state, commit, dispatch }, payload = {}) => {
+ performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_FILES_START });
+ return new Promise((resolve, reject) => {
const { projectId, branchId, ref = branchId } = payload;
if (
@@ -61,6 +68,15 @@ export const getFiles = ({ state, commit, dispatch }, payload = {}) =>
service
.getFiles(selectedProject.path_with_namespace, ref)
.then(({ data }) => {
+ performanceMarkAndMeasure({
+ mark: WEBIDE_MARK_FETCH_FILES_FINISH,
+ measures: [
+ {
+ name: WEBIDE_MEASURE_FETCH_FILES,
+ start: WEBIDE_MARK_FETCH_FILES_START,
+ },
+ ],
+ });
const { entries, treeList } = decorateFiles({ data });
commit(types.SET_ENTRIES, entries);
@@ -85,6 +101,7 @@ export const getFiles = ({ state, commit, dispatch }, payload = {}) =>
resolve();
}
});
+};
export const restoreTree = ({ dispatch, commit, state }, path) => {
const entry = state.entries[path];
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index 1d0814125e6..14d6f133d27 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -35,12 +35,14 @@ export default class IntegrationSettingsForm {
}
saveIntegration() {
- // Service was marked active so now we check;
+ // Save Service if not active and check the following if active;
// 1) If form contents are valid
// 2) If this service can be saved
// If both conditions are true, we override form submission
// and save the service using provided configuration.
- if (this.$form.get(0).checkValidity()) {
+ const formValid = this.$form.get(0).checkValidity() || this.formActive === false;
+
+ if (formValid) {
this.$form.submit();
} else {
eventHub.$emit('validateForm');
diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
index e0fb58ef195..12f03873958 100644
--- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js
+++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { loadCSSFile } from '../lib/utils/css_utils';
let instanceCount = 0;
@@ -13,10 +14,15 @@ class AutoWidthDropdownSelect {
const { dropdownClass } = this;
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
- this.$selectElement.select2({
- dropdownCssClass: dropdownClass,
- ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
- });
+ // eslint-disable-next-line promise/no-nesting
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ this.$selectElement.select2({
+ dropdownCssClass: dropdownClass,
+ ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
+ });
+ })
+ .catch(() => {});
})
.catch(() => {});
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 6f2bd2da078..2072e41514d 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import Cookies from 'js-cookie';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import UsersSelect from './users_select';
+import { loadCSSFile } from './lib/utils/css_utils';
export default class IssuableContext {
constructor(currentUser) {
@@ -10,10 +11,15 @@ export default class IssuableContext {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
- $('select.select2').select2({
- width: 'resolve',
- dropdownAutoWidth: true,
- });
+ // eslint-disable-next-line promise/no-nesting
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ $('select.select2').select2({
+ width: 'resolve',
+ dropdownAutoWidth: true,
+ });
+ })
+ .catch(() => {});
})
.catch(() => {});
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index ed34e2f5623..791b5fef699 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -7,6 +7,7 @@ import ZenMode from './zen_mode';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
import { queryToObject, objectToQuery } from './lib/utils/url_utility';
+import { loadCSSFile } from './lib/utils/css_utils';
const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
const MR_TARGET_BRANCH = 'merge_request[target_branch]';
@@ -184,36 +185,41 @@ export default class IssuableForm {
initTargetBranchDropdown() {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
- this.$targetBranchSelect.select2({
- ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'),
- ajax: {
- url: this.$targetBranchSelect.data('endpoint'),
- dataType: 'JSON',
- quietMillis: 250,
- data(search) {
- return {
- search,
- };
- },
- results(data) {
- return {
- // `data` keys are translated so we can't just access them with a string based key
- results: data[Object.keys(data)[0]].map(name => ({
- id: name,
- text: name,
- })),
- };
- },
- },
- initSelection(el, callback) {
- const val = el.val();
-
- callback({
- id: val,
- text: val,
+ // eslint-disable-next-line promise/no-nesting
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ this.$targetBranchSelect.select2({
+ ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'),
+ ajax: {
+ url: this.$targetBranchSelect.data('endpoint'),
+ dataType: 'JSON',
+ quietMillis: 250,
+ data(search) {
+ return {
+ search,
+ };
+ },
+ results(data) {
+ return {
+ // `data` keys are translated so we can't just access them with a string based key
+ results: data[Object.keys(data)[0]].map(name => ({
+ id: name,
+ text: name,
+ })),
+ };
+ },
+ },
+ initSelection(el, callback) {
+ const val = el.val();
+
+ callback({
+ id: val,
+ text: val,
+ });
+ },
});
- },
- });
+ })
+ .catch(() => {});
})
.catch(() => {});
}
diff --git a/app/assets/javascripts/issuable_list/components/issuable_item.vue b/app/assets/javascripts/issuable_list/components/issuable_item.vue
index 1ee794ab208..583e5cb703d 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_item.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_item.vue
@@ -128,7 +128,7 @@ export default {
<template>
<li class="issue gl-px-5!">
- <div class="issue-box">
+ <div class="issuable-info-container">
<div v-if="showCheckbox" class="issue-check">
<gl-form-checkbox
class="gl-mr-0"
@@ -136,101 +136,99 @@ export default {
@input="$emit('checked-input', $event)"
/>
</div>
- <div class="issuable-info-container">
- <div class="issuable-main-info">
- <div data-testid="issuable-title" class="issue-title title">
- <span class="issue-title-text" dir="auto">
- <gl-link :href="issuable.webUrl" v-bind="issuableTitleProps"
- >{{ issuable.title
- }}<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2"
- /></gl-link>
- </span>
- </div>
- <div class="issuable-info">
- <slot v-if="hasSlotContents('reference')" name="reference"></slot>
- <span v-else data-testid="issuable-reference" class="issuable-reference"
- >{{ issuableSymbol }}{{ issuable.iid }}</span
- >
- <span class="issuable-authored d-none d-sm-inline-block">
- &middot;
- <span
- v-gl-tooltip:tooltipcontainer.bottom
- data-testid="issuable-created-at"
- :title="tooltipTitle(issuable.createdAt)"
- >{{ createdAt }}</span
- >
- {{ __('by') }}
- <slot v-if="hasSlotContents('author')" name="author"></slot>
- <gl-link
- v-else
- :data-user-id="authorId"
- :data-username="author.username"
- :data-name="author.name"
- :data-avatar-url="author.avatarUrl"
- :href="author.webUrl"
- data-testid="issuable-author"
- class="author-link js-user-link"
- >
- <span class="author">{{ author.name }}</span>
- </gl-link>
- </span>
- <slot name="timeframe"></slot>
- &nbsp;
- <gl-label
- v-for="(label, index) in labels"
- :key="index"
- :background-color="label.color"
- :title="labelTitle(label)"
- :description="label.description"
- :scoped="scopedLabel(label)"
- :target="labelTarget(label)"
- :class="{ 'gl-ml-2': index }"
- size="sm"
- />
- </div>
+ <div class="issuable-main-info">
+ <div data-testid="issuable-title" class="issue-title title">
+ <span class="issue-title-text" dir="auto">
+ <gl-link :href="issuable.webUrl" v-bind="issuableTitleProps"
+ >{{ issuable.title
+ }}<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2"
+ /></gl-link>
+ </span>
</div>
- <div class="issuable-meta">
- <ul v-if="showIssuableMeta" class="controls">
- <li v-if="hasSlotContents('status')" class="issuable-status">
- <slot name="status"></slot>
- </li>
- <li
- v-if="showDiscussions"
- data-testid="issuable-discussions"
- class="issuable-comments gl-display-none gl-display-sm-block"
- >
- <gl-link
- v-gl-tooltip:tooltipcontainer.top
- :title="__('Comments')"
- :href="`${issuable.webUrl}#notes`"
- :class="{ 'no-comments': !issuable.userDiscussionsCount }"
- class="gl-reset-color!"
- >
- <gl-icon name="comments" />
- {{ issuable.userDiscussionsCount }}
- </gl-link>
- </li>
- <li v-if="assignees.length" class="gl-display-flex">
- <issuable-assignees
- :assignees="issuable.assignees"
- :icon-size="16"
- :max-visible="4"
- img-css-classes="gl-mr-2!"
- class="gl-align-items-center gl-display-flex gl-ml-3"
- />
- </li>
- </ul>
- <div
- data-testid="issuable-updated-at"
- class="float-right issuable-updated-at d-none d-sm-inline-block"
+ <div class="issuable-info">
+ <slot v-if="hasSlotContents('reference')" name="reference"></slot>
+ <span v-else data-testid="issuable-reference" class="issuable-reference"
+ >{{ issuableSymbol }}{{ issuable.iid }}</span
>
+ <span class="issuable-authored d-none d-sm-inline-block">
+ &middot;
<span
v-gl-tooltip:tooltipcontainer.bottom
- :title="tooltipTitle(issuable.updatedAt)"
- class="issuable-updated-at"
- >{{ updatedAt }}</span
+ data-testid="issuable-created-at"
+ :title="tooltipTitle(issuable.createdAt)"
+ >{{ createdAt }}</span
+ >
+ {{ __('by') }}
+ <slot v-if="hasSlotContents('author')" name="author"></slot>
+ <gl-link
+ v-else
+ :data-user-id="authorId"
+ :data-username="author.username"
+ :data-name="author.name"
+ :data-avatar-url="author.avatarUrl"
+ :href="author.webUrl"
+ data-testid="issuable-author"
+ class="author-link js-user-link"
+ >
+ <span class="author">{{ author.name }}</span>
+ </gl-link>
+ </span>
+ <slot name="timeframe"></slot>
+ &nbsp;
+ <gl-label
+ v-for="(label, index) in labels"
+ :key="index"
+ :background-color="label.color"
+ :title="labelTitle(label)"
+ :description="label.description"
+ :scoped="scopedLabel(label)"
+ :target="labelTarget(label)"
+ :class="{ 'gl-ml-2': index }"
+ size="sm"
+ />
+ </div>
+ </div>
+ <div class="issuable-meta">
+ <ul v-if="showIssuableMeta" class="controls">
+ <li v-if="hasSlotContents('status')" class="issuable-status">
+ <slot name="status"></slot>
+ </li>
+ <li
+ v-if="showDiscussions"
+ data-testid="issuable-discussions"
+ class="issuable-comments gl-display-none gl-display-sm-block"
+ >
+ <gl-link
+ v-gl-tooltip:tooltipcontainer.top
+ :title="__('Comments')"
+ :href="`${issuable.webUrl}#notes`"
+ :class="{ 'no-comments': !issuable.userDiscussionsCount }"
+ class="gl-reset-color!"
>
- </div>
+ <gl-icon name="comments" />
+ {{ issuable.userDiscussionsCount }}
+ </gl-link>
+ </li>
+ <li v-if="assignees.length" class="gl-display-flex">
+ <issuable-assignees
+ :assignees="issuable.assignees"
+ :icon-size="16"
+ :max-visible="4"
+ img-css-classes="gl-mr-2!"
+ class="gl-align-items-center gl-display-flex gl-ml-3"
+ />
+ </li>
+ </ul>
+ <div
+ data-testid="issuable-updated-at"
+ class="float-right issuable-updated-at d-none d-sm-inline-block"
+ >
+ <span
+ v-gl-tooltip:tooltipcontainer.bottom
+ :title="tooltipTitle(issuable.updatedAt)"
+ class="issuable-updated-at"
+ >{{ updatedAt }}</span
+ >
</div>
</div>
</div>
diff --git a/app/assets/javascripts/issue_show/utils/parse_data.js b/app/assets/javascripts/issue_show/utils/parse_data.js
index 620974901fb..aacbb6a9c6f 100644
--- a/app/assets/javascripts/issue_show/utils/parse_data.js
+++ b/app/assets/javascripts/issue_show/utils/parse_data.js
@@ -23,5 +23,3 @@ export const parseIssuableData = () => {
return {};
}
};
-
-export default {};
diff --git a/app/assets/javascripts/jira_connect/components/app.vue b/app/assets/javascripts/jira_connect/components/app.vue
index 6d32ba41eae..7b8b46cb048 100644
--- a/app/assets/javascripts/jira_connect/components/app.vue
+++ b/app/assets/javascripts/jira_connect/components/app.vue
@@ -1,7 +1,3 @@
-<script>
-export default {};
-</script>
-
<template>
<div></div>
</template>
diff --git a/app/assets/javascripts/jobs/utils.js b/app/assets/javascripts/jobs/utils.js
index 28a125b2b8f..122f23a5bb5 100644
--- a/app/assets/javascripts/jobs/utils.js
+++ b/app/assets/javascripts/jobs/utils.js
@@ -1,4 +1,12 @@
-// capture anything starting with http:// or https://
-// up until a disallowed character or whitespace
-export const linkRegex = /(https?:\/\/[^"<>\\^`{|}\s]+)/g;
+/**
+ * capture anything starting with http:// or https://
+ * https?:\/\/
+ *
+ * up until a disallowed character or whitespace
+ * [^"<>\\^`{|}\s]+
+ *
+ * and a disallowed character or whitespace, including non-ending chars .,:;!?
+ * [^"<>\\^`{|}\s.,:;!?]
+ */
+export const linkRegex = /(https?:\/\/[^"<>\\^`{|}\s]+[^"<>\\^`{|}\s.,:;!?])/g;
export default { linkRegex };
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 42a5de68cfa..ef25fd83db9 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -218,23 +218,36 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2;
export const contentTop = () => {
- const perfBar = $('#js-peek').outerHeight() || 0;
- const mrTabsHeight = $('.merge-request-tabs').outerHeight() || 0;
- const headerHeight = $('.navbar-gitlab').outerHeight() || 0;
- const diffFilesChanged = $('.js-diff-files-changed').outerHeight() || 0;
- const isDesktop = breakpointInstance.isDesktop();
- const diffFileTitleBar =
- (isDesktop && $('.diff-file .file-title-flex-parent:visible').outerHeight()) || 0;
- const compareVersionsHeaderHeight = (isDesktop && $('.mr-version-controls').outerHeight()) || 0;
+ const heightCalculators = [
+ () => $('#js-peek').outerHeight(),
+ () => $('.navbar-gitlab').outerHeight(),
+ () => $('.merge-request-tabs').outerHeight(),
+ () => $('.js-diff-files-changed').outerHeight(),
+ () => {
+ const isDesktop = breakpointInstance.isDesktop();
+ const diffsTabIsActive = window.mrTabs?.currentAction === 'diffs';
+ let size;
+
+ if (isDesktop && diffsTabIsActive) {
+ size = $('.diff-file .file-title-flex-parent:visible').outerHeight();
+ }
- return (
- perfBar +
- mrTabsHeight +
- headerHeight +
- diffFilesChanged +
- diffFileTitleBar +
- compareVersionsHeaderHeight
- );
+ return size;
+ },
+ () => {
+ let size;
+
+ if (breakpointInstance.isDesktop()) {
+ size = $('.mr-version-controls').outerHeight();
+ }
+
+ return size;
+ },
+ ];
+
+ return heightCalculators.reduce((totalHeight, calculator) => {
+ return totalHeight + (calculator() || 0);
+ }, 0);
};
export const scrollToElement = (element, options = {}) => {
diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js
index 7bba7ba2f45..2f19a0c9b26 100644
--- a/app/assets/javascripts/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/lib/utils/dom_utils.js
@@ -1,6 +1,14 @@
import { has } from 'lodash';
import { isInIssuePage, isInMRPage, isInEpicPage } from './common_utils';
+/**
+ * Checks whether an element's content exceeds the element's width.
+ *
+ * @param element DOM element to check
+ */
+export const hasHorizontalOverflow = element =>
+ Boolean(element && element.scrollWidth > element.offsetWidth);
+
export const addClassIfElementExists = (element, className) => {
if (element) {
element.classList.add(className);
diff --git a/app/assets/javascripts/lib/utils/scroll_utils.js b/app/assets/javascripts/lib/utils/scroll_utils.js
index b4da1e16f08..01e43fd3b93 100644
--- a/app/assets/javascripts/lib/utils/scroll_utils.js
+++ b/app/assets/javascripts/lib/utils/scroll_utils.js
@@ -49,5 +49,3 @@ export const toggleDisableButton = ($button, disable) => {
if (disable && $button.prop('disabled')) return;
$button.prop('disabled', disable);
};
-
-export default {};
diff --git a/app/assets/javascripts/logs/utils.js b/app/assets/javascripts/logs/utils.js
index 8e537a4025f..880f762e225 100644
--- a/app/assets/javascripts/logs/utils.js
+++ b/app/assets/javascripts/logs/utils.js
@@ -23,5 +23,3 @@ export const getTimeRange = (seconds = 0) => {
};
export const formatDate = timestamp => dateFormat(timestamp, dateFormatMask);
-
-export default {};
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/access_request_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue
index 10078d5cd64..10078d5cd64 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/access_request_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/action_button_group.vue b/app/assets/javascripts/members/components/action_buttons/action_button_group.vue
index 8356fdb60b1..8356fdb60b1 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/action_button_group.vue
+++ b/app/assets/javascripts/members/components/action_buttons/action_button_group.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/approve_access_request_button.vue b/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
index e8a53ff173d..e8a53ff173d 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/approve_access_request_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/group_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/group_action_buttons.vue
index 2aebfe80db5..2aebfe80db5 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/group_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/group_action_buttons.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/invite_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue
index 2b0a75640e2..2b0a75640e2 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/invite_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/leave_button.vue b/app/assets/javascripts/members/components/action_buttons/leave_button.vue
index d9976e7181c..443a962e0cf 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/leave_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/leave_button.vue
@@ -2,7 +2,7 @@
import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import LeaveModal from '../modals/leave_modal.vue';
-import { LEAVE_MODAL_ID } from '../constants';
+import { LEAVE_MODAL_ID } from '../../constants';
export default {
name: 'LeaveButton',
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/remove_group_link_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
index 9d89cb40676..9d89cb40676 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/remove_group_link_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/remove_member_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
index b0b7ff4ce9a..b0b7ff4ce9a 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/remove_member_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/resend_invite_button.vue b/app/assets/javascripts/members/components/action_buttons/resend_invite_button.vue
index 1cc3fd17e98..1cc3fd17e98 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/resend_invite_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/resend_invite_button.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/user_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
index 484dbb8fef5..f2bc9c7e876 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/user_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
@@ -11,7 +11,7 @@ export default {
RemoveMemberButton,
LeaveButton,
LdapOverrideButton: () =>
- import('ee_component/vue_shared/components/members/ldap/ldap_override_button.vue'),
+ import('ee_component/members/components/ldap/ldap_override_button.vue'),
},
props: {
member: {
diff --git a/app/assets/javascripts/vue_shared/components/members/avatars/group_avatar.vue b/app/assets/javascripts/members/components/avatars/group_avatar.vue
index 12b748f9ab6..3b176bf2b43 100644
--- a/app/assets/javascripts/vue_shared/components/members/avatars/group_avatar.vue
+++ b/app/assets/javascripts/members/components/avatars/group_avatar.vue
@@ -1,6 +1,6 @@
<script>
import { GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui';
-import { AVATAR_SIZE } from '../constants';
+import { AVATAR_SIZE } from '../../constants';
export default {
name: 'GroupAvatar',
diff --git a/app/assets/javascripts/vue_shared/components/members/avatars/invite_avatar.vue b/app/assets/javascripts/members/components/avatars/invite_avatar.vue
index 28654a60860..08e702007bb 100644
--- a/app/assets/javascripts/vue_shared/components/members/avatars/invite_avatar.vue
+++ b/app/assets/javascripts/members/components/avatars/invite_avatar.vue
@@ -1,6 +1,6 @@
<script>
import { GlAvatarLabeled } from '@gitlab/ui';
-import { AVATAR_SIZE } from '../constants';
+import { AVATAR_SIZE } from '../../constants';
export default {
name: 'InviteAvatar',
diff --git a/app/assets/javascripts/vue_shared/components/members/avatars/user_avatar.vue b/app/assets/javascripts/members/components/avatars/user_avatar.vue
index e5e7cdf149c..fe45ca769af 100644
--- a/app/assets/javascripts/vue_shared/components/members/avatars/user_avatar.vue
+++ b/app/assets/javascripts/members/components/avatars/user_avatar.vue
@@ -5,9 +5,9 @@ import {
GlBadge,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
-import { generateBadges } from 'ee_else_ce/vue_shared/components/members/utils';
+import { generateBadges } from 'ee_else_ce/members/utils';
import { __ } from '~/locale';
-import { AVATAR_SIZE } from '../constants';
+import { AVATAR_SIZE } from '../../constants';
import { glEmojiTag } from '~/emoji';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/members/modals/leave_modal.vue b/app/assets/javascripts/members/components/modals/leave_modal.vue
index 9a2ce0d4931..57a5da774e3 100644
--- a/app/assets/javascripts/vue_shared/components/members/modals/leave_modal.vue
+++ b/app/assets/javascripts/members/components/modals/leave_modal.vue
@@ -3,7 +3,7 @@ import { mapState } from 'vuex';
import { GlModal, GlForm, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { __, s__, sprintf } from '~/locale';
-import { LEAVE_MODAL_ID } from '../constants';
+import { LEAVE_MODAL_ID } from '../../constants';
export default {
name: 'LeaveModal',
diff --git a/app/assets/javascripts/vue_shared/components/members/modals/remove_group_link_modal.vue b/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
index e8890717724..231d014a4ec 100644
--- a/app/assets/javascripts/vue_shared/components/members/modals/remove_group_link_modal.vue
+++ b/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
@@ -3,7 +3,7 @@ import { mapState, mapActions } from 'vuex';
import { GlModal, GlSprintf, GlForm } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { __, s__, sprintf } from '~/locale';
-import { REMOVE_GROUP_LINK_MODAL_ID } from '../constants';
+import { REMOVE_GROUP_LINK_MODAL_ID } from '../../constants';
export default {
name: 'RemoveGroupLinkModal',
diff --git a/app/assets/javascripts/vue_shared/components/members/table/created_at.vue b/app/assets/javascripts/members/components/table/created_at.vue
index 0bad70894f9..0bad70894f9 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/created_at.vue
+++ b/app/assets/javascripts/members/components/table/created_at.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/table/expiration_datepicker.vue b/app/assets/javascripts/members/components/table/expiration_datepicker.vue
index 0a8af81c1d1..0a8af81c1d1 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/expiration_datepicker.vue
+++ b/app/assets/javascripts/members/components/table/expiration_datepicker.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/table/expires_at.vue b/app/assets/javascripts/members/components/table/expires_at.vue
index de65e3fb10f..c91de061b50 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/expires_at.vue
+++ b/app/assets/javascripts/members/components/table/expires_at.vue
@@ -6,7 +6,7 @@ import {
formatDate,
getDayDifference,
} from '~/lib/utils/datetime_utility';
-import { DAYS_TO_EXPIRE_SOON } from '../constants';
+import { DAYS_TO_EXPIRE_SOON } from '../../constants';
export default {
name: 'ExpiresAt',
diff --git a/app/assets/javascripts/vue_shared/components/members/table/member_action_buttons.vue b/app/assets/javascripts/members/components/table/member_action_buttons.vue
index 320d8c99223..c61ebec33bd 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/member_action_buttons.vue
+++ b/app/assets/javascripts/members/components/table/member_action_buttons.vue
@@ -3,7 +3,7 @@ import UserActionButtons from '../action_buttons/user_action_buttons.vue';
import GroupActionButtons from '../action_buttons/group_action_buttons.vue';
import InviteActionButtons from '../action_buttons/invite_action_buttons.vue';
import AccessRequestActionButtons from '../action_buttons/access_request_action_buttons.vue';
-import { MEMBER_TYPES } from '../constants';
+import { MEMBER_TYPES } from '../../constants';
export default {
name: 'MemberActionButtons',
diff --git a/app/assets/javascripts/vue_shared/components/members/table/member_avatar.vue b/app/assets/javascripts/members/components/table/member_avatar.vue
index a1f98d4008a..a1f98d4008a 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/member_avatar.vue
+++ b/app/assets/javascripts/members/components/table/member_avatar.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/table/member_source.vue b/app/assets/javascripts/members/components/table/member_source.vue
index 030d72c3420..030d72c3420 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/member_source.vue
+++ b/app/assets/javascripts/members/components/table/member_source.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index a4f67caff31..da77e5caad2 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -1,14 +1,9 @@
<script>
import { mapState } from 'vuex';
import { GlTable, GlBadge } from '@gitlab/ui';
-import MembersTableCell from 'ee_else_ce/vue_shared/components/members/table/members_table_cell.vue';
-import {
- canOverride,
- canRemove,
- canResend,
- canUpdate,
-} from 'ee_else_ce/vue_shared/components/members/utils';
-import { FIELDS } from '../constants';
+import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue';
+import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
+import { FIELDS } from '../../constants';
import initUserPopovers from '~/user_popovers';
import MemberAvatar from './member_avatar.vue';
import MemberSource from './member_source.vue';
@@ -34,9 +29,7 @@ export default {
RemoveGroupLinkModal,
ExpirationDatepicker,
LdapOverrideConfirmationModal: () =>
- import(
- 'ee_component/vue_shared/components/members/ldap/ldap_override_confirmation_modal.vue'
- ),
+ import('ee_component/members/components/ldap/ldap_override_confirmation_modal.vue'),
},
computed: {
...mapState(['members', 'tableFields', 'tableAttrs', 'currentUserId', 'sourceId']),
diff --git a/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue b/app/assets/javascripts/members/components/table/members_table_cell.vue
index 11e1aef9803..20aa01b96bc 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue
+++ b/app/assets/javascripts/members/components/table/members_table_cell.vue
@@ -1,7 +1,14 @@
<script>
import { mapState } from 'vuex';
-import { MEMBER_TYPES } from '../constants';
-import { isGroup, isDirectMember, isCurrentUser, canRemove, canResend, canUpdate } from '../utils';
+import { MEMBER_TYPES } from '../../constants';
+import {
+ isGroup,
+ isDirectMember,
+ isCurrentUser,
+ canRemove,
+ canResend,
+ canUpdate,
+} from '../../utils';
export default {
name: 'MembersTableCell',
diff --git a/app/assets/javascripts/vue_shared/components/members/table/role_dropdown.vue b/app/assets/javascripts/members/components/table/role_dropdown.vue
index 6f6cae6072d..8ad45ab6920 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/role_dropdown.vue
+++ b/app/assets/javascripts/members/components/table/role_dropdown.vue
@@ -9,8 +9,7 @@ export default {
components: {
GlDropdown,
GlDropdownItem,
- LdapDropdownItem: () =>
- import('ee_component/vue_shared/components/members/ldap/ldap_dropdown_item.vue'),
+ LdapDropdownItem: () => import('ee_component/members/components/ldap/ldap_dropdown_item.vue'),
},
props: {
member: {
diff --git a/app/assets/javascripts/vue_shared/components/members/constants.js b/app/assets/javascripts/members/constants.js
index 5885420a122..5885420a122 100644
--- a/app/assets/javascripts/vue_shared/components/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
diff --git a/app/assets/javascripts/vuex_shared/modules/members/actions.js b/app/assets/javascripts/members/store/actions.js
index 4c31b3c9744..4c31b3c9744 100644
--- a/app/assets/javascripts/vuex_shared/modules/members/actions.js
+++ b/app/assets/javascripts/members/store/actions.js
diff --git a/app/assets/javascripts/members/store/index.js b/app/assets/javascripts/members/store/index.js
new file mode 100644
index 00000000000..f219f8931b0
--- /dev/null
+++ b/app/assets/javascripts/members/store/index.js
@@ -0,0 +1,9 @@
+import createState from 'ee_else_ce/members/store/state';
+import mutations from 'ee_else_ce/members/store/mutations';
+import * as actions from 'ee_else_ce/members/store/actions';
+
+export default initialState => ({
+ state: createState(initialState),
+ actions,
+ mutations,
+});
diff --git a/app/assets/javascripts/vuex_shared/modules/members/mutation_types.js b/app/assets/javascripts/members/store/mutation_types.js
index 77307aa745b..77307aa745b 100644
--- a/app/assets/javascripts/vuex_shared/modules/members/mutation_types.js
+++ b/app/assets/javascripts/members/store/mutation_types.js
diff --git a/app/assets/javascripts/vuex_shared/modules/members/mutations.js b/app/assets/javascripts/members/store/mutations.js
index 2415e744290..2415e744290 100644
--- a/app/assets/javascripts/vuex_shared/modules/members/mutations.js
+++ b/app/assets/javascripts/members/store/mutations.js
diff --git a/app/assets/javascripts/vuex_shared/modules/members/state.js b/app/assets/javascripts/members/store/state.js
index ab3ebb34616..ab3ebb34616 100644
--- a/app/assets/javascripts/vuex_shared/modules/members/state.js
+++ b/app/assets/javascripts/members/store/state.js
diff --git a/app/assets/javascripts/vuex_shared/modules/members/utils.js b/app/assets/javascripts/members/store/utils.js
index 7dcd33111e8..7dcd33111e8 100644
--- a/app/assets/javascripts/vuex_shared/modules/members/utils.js
+++ b/app/assets/javascripts/members/store/utils.js
diff --git a/app/assets/javascripts/vue_shared/components/members/utils.js b/app/assets/javascripts/members/utils.js
index 4229a62c0a7..4229a62c0a7 100644
--- a/app/assets/javascripts/vue_shared/components/members/utils.js
+++ b/app/assets/javascripts/members/utils.js
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
index 25c357b6073..c803774f4a7 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
@@ -54,7 +54,6 @@ import { s__ } from '~/locale';
file.promptDiscardConfirmation = false;
file.resolveMode = DEFAULT_RESOLVE_MODE;
file.filePath = this.getFilePath(file);
- file.iconClass = `fa-${file.blob_icon}`;
file.blobPath = file.blob_path;
if (file.type === CONFLICT_TYPES.TEXT) {
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index a5a930572e1..229f6f3e339 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
import { deprecatedCreateFlash as createFlash } from '../flash';
import initIssuableSidebar from '../init_issuable_sidebar';
import './merge_conflict_store';
@@ -24,6 +25,7 @@ export default function initMergeConflicts() {
gl.MergeConflictsResolverApp = new Vue({
el: '#conflicts',
components: {
+ FileIcon,
'diff-file-editor': gl.mergeConflicts.diffFileEditor,
'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines,
'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines,
diff --git a/app/assets/javascripts/monitoring/stores/variable_mapping.js b/app/assets/javascripts/monitoring/stores/variable_mapping.js
index 9245ffdb3b9..4ae5cf04ff9 100644
--- a/app/assets/javascripts/monitoring/stores/variable_mapping.js
+++ b/app/assets/javascripts/monitoring/stores/variable_mapping.js
@@ -271,5 +271,3 @@ export const optionsFromSeriesData = ({ label, data = [] }) => {
return [...optionsSet].map(parseSimpleCustomValues);
};
-
-export default {};
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index 92bbce498d5..a4c5a881fae 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -404,5 +404,3 @@ export const barChartsDataParser = (data = []) =>
}),
{},
);
-
-export default {};
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 9be53fe60f2..5073922e4a4 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -23,6 +23,7 @@ import {
commentLineOptions,
formatLineRange,
} from './multiline_comment_utils';
+import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
export default {
name: 'NoteableNote',
@@ -169,12 +170,8 @@ export default {
return this.line && this.startLineNumber !== this.endLineNumber;
},
commentLineOptions() {
- const sideA = this.line.type === 'new' ? 'right' : 'left';
- const sideB = sideA === 'left' ? 'right' : 'left';
- const lines = this.diffFile.highlighted_diff_lines.length
- ? this.diffFile.highlighted_diff_lines
- : this.diffFile.parallel_diff_lines.map(l => l[sideA] || l[sideB]);
- return commentLineOptions(lines, this.commentLineStart, this.line.line_code, sideA);
+ const lines = this.diffFile[INLINE_DIFF_LINES_KEY].length;
+ return commentLineOptions(lines, this.commentLineStart, this.line.line_code);
},
diffFile() {
if (this.commentLineStart.line_code) {
diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js
index 61298a15c5d..c6932bfacae 100644
--- a/app/assets/javascripts/notes/mixins/discussion_navigation.js
+++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js
@@ -1,16 +1,17 @@
import { mapGetters, mapActions, mapState } from 'vuex';
-import { scrollToElementWithContext } from '~/lib/utils/common_utils';
+import { scrollToElementWithContext, scrollToElement } from '~/lib/utils/common_utils';
import eventHub from '../event_hub';
/**
* @param {string} selector
* @returns {boolean}
*/
-function scrollTo(selector) {
+function scrollTo(selector, { withoutContext = false } = {}) {
const el = document.querySelector(selector);
+ const scrollFunction = withoutContext ? scrollToElement : scrollToElementWithContext;
if (el) {
- scrollToElementWithContext(el);
+ scrollFunction(el);
return true;
}
@@ -35,7 +36,7 @@ function diffsJump({ expandDiscussion }, id) {
function discussionJump({ expandDiscussion }, id) {
const selector = `div.discussion[data-discussion-id="${id}"]`;
expandDiscussion({ discussionId: id });
- return scrollTo(selector);
+ return scrollTo(selector, { withoutContext: true });
}
/**
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 2c60b5ee84a..ee668f4406f 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -435,6 +435,10 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
};
const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
+ if (state.isResolvingDiscussion) {
+ return null;
+ }
+
if (resp.notes?.length) {
dispatch('updateOrCreateNotes', resp.notes);
dispatch('startTaskList');
@@ -574,6 +578,9 @@ export const submitSuggestion = (
const dispatchResolveDiscussion = () =>
dispatch('resolveDiscussion', { discussionId }).catch(() => {});
+ commit(types.SET_RESOLVING_DISCUSSION, true);
+ dispatch('stopPolling');
+
return Api.applySuggestion(suggestionId)
.then(() => commit(types.APPLY_SUGGESTION, { discussionId, noteId, suggestionId }))
.then(dispatchResolveDiscussion)
@@ -587,6 +594,10 @@ export const submitSuggestion = (
const flashMessage = errorMessage || defaultMessage;
Flash(__(flashMessage), 'alert', flashContainer);
+ })
+ .finally(() => {
+ commit(types.SET_RESOLVING_DISCUSSION, false);
+ dispatch('restartPolling');
});
};
@@ -605,6 +616,8 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { flashContai
});
commit(types.SET_APPLYING_BATCH_STATE, true);
+ commit(types.SET_RESOLVING_DISCUSSION, true);
+ dispatch('stopPolling');
return Api.applySuggestionBatch(suggestionIds)
.then(() => Promise.all(applyAllSuggestions()))
@@ -621,7 +634,11 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { flashContai
Flash(__(flashMessage), 'alert', flashContainer);
})
- .finally(() => commit(types.SET_APPLYING_BATCH_STATE, false));
+ .finally(() => {
+ commit(types.SET_APPLYING_BATCH_STATE, false);
+ commit(types.SET_RESOLVING_DISCUSSION, false);
+ dispatch('restartPolling');
+ });
};
export const addSuggestionInfoToBatch = ({ commit }, { suggestionId, noteId, discussionId }) =>
diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js
index d94fc626a3f..f34247d4eb0 100644
--- a/app/assets/javascripts/notes/stores/collapse_utils.js
+++ b/app/assets/javascripts/notes/stores/collapse_utils.js
@@ -70,6 +70,3 @@ export const collapseSystemNotes = notes => {
return acc;
}, []);
};
-
-// for babel-rewire
-export default {};
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index a8738fa7c5f..3194a2099ea 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -42,6 +42,7 @@ export default () => ({
current_user: {},
preview_note_path: 'path/to/preview',
},
+ isResolvingDiscussion: false,
commentsDisabled: false,
resolvableDiscussionsCount: 0,
unresolvedDiscussionsCount: 0,
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 7496dd630f6..8270f2a225b 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -38,6 +38,7 @@ export const SET_TIMELINE_VIEW = 'SET_TIMELINE_VIEW';
export const SET_SELECTED_COMMENT_POSITION = 'SET_SELECTED_COMMENT_POSITION';
export const SET_SELECTED_COMMENT_POSITION_HOVER = 'SET_SELECTED_COMMENT_POSITION_HOVER';
export const SET_FETCHING_DISCUSSIONS = 'SET_FETCHING_DISCUSSIONS';
+export const SET_RESOLVING_DISCUSSION = 'SET_RESOLVING_DISCUSSION';
// Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 7cc619ec1c5..85bdf60e8f9 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -213,6 +213,10 @@ export default {
}
},
+ [types.SET_RESOLVING_DISCUSSION](state, isResolving) {
+ state.isResolvingDiscussion = isResolving;
+ },
+
[types.UPDATE_NOTE](state, note) {
const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);
diff --git a/app/assets/javascripts/packages/list/constants.js b/app/assets/javascripts/packages/list/constants.js
index 6a0e92bff2d..e14696e0d1c 100644
--- a/app/assets/javascripts/packages/list/constants.js
+++ b/app/assets/javascripts/packages/list/constants.js
@@ -68,6 +68,10 @@ export const PACKAGE_REGISTRY_TABS = [
title: s__('PackageRegistry|Conan'),
type: PackageType.CONAN,
},
+ {
+ title: s__('PackageRegistry|Generic'),
+ type: PackageType.GENERIC,
+ },
{
title: s__('PackageRegistry|Maven'),
diff --git a/app/assets/javascripts/packages/shared/constants.js b/app/assets/javascripts/packages/shared/constants.js
index c481abd8658..c0f7f150337 100644
--- a/app/assets/javascripts/packages/shared/constants.js
+++ b/app/assets/javascripts/packages/shared/constants.js
@@ -7,6 +7,7 @@ export const PackageType = {
NUGET: 'nuget',
PYPI: 'pypi',
COMPOSER: 'composer',
+ GENERIC: 'generic',
};
export const TrackingActions = {
diff --git a/app/assets/javascripts/packages/shared/utils.js b/app/assets/javascripts/packages/shared/utils.js
index b0807558266..d7a883e4397 100644
--- a/app/assets/javascripts/packages/shared/utils.js
+++ b/app/assets/javascripts/packages/shared/utils.js
@@ -21,7 +21,8 @@ export const getPackageTypeLabel = packageType => {
return s__('PackageType|PyPI');
case PackageType.COMPOSER:
return s__('PackageType|Composer');
-
+ case PackageType.GENERIC:
+ return s__('PackageType|Generic');
default:
return null;
}
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index 1879e263ce7..74c1c2e981e 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -73,22 +73,20 @@ document.addEventListener('DOMContentLoaded', () => {
);
}
- if (gon.features?.suggestPipeline) {
- const successPipelineEl = document.querySelector('.js-success-pipeline-modal');
+ const successPipelineEl = document.querySelector('.js-success-pipeline-modal');
- if (successPipelineEl) {
- // eslint-disable-next-line no-new
- new Vue({
- el: successPipelineEl,
- render(createElement) {
- return createElement(PipelineTourSuccessModal, {
- props: {
- ...successPipelineEl.dataset,
- },
- });
- },
- });
- }
+ if (successPipelineEl) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: successPipelineEl,
+ render(createElement) {
+ return createElement(PipelineTourSuccessModal, {
+ props: {
+ ...successPipelineEl.dataset,
+ },
+ });
+ },
+ });
}
if (gon?.features?.gitlabCiYmlPreview) {
diff --git a/app/assets/javascripts/pages/projects/commit/pipelines/index.js b/app/assets/javascripts/pages/projects/commit/pipelines/index.js
index 26dea17ca8a..eaf340f2725 100644
--- a/app/assets/javascripts/pages/projects/commit/pipelines/index.js
+++ b/app/assets/javascripts/pages/projects/commit/pipelines/index.js
@@ -1,8 +1,5 @@
import { initCommitBoxInfo } from '~/projects/commit_box/info';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
-document.addEventListener('DOMContentLoaded', () => {
- initCommitBoxInfo();
-
- initPipelines();
-});
+initCommitBoxInfo();
+initPipelines();
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index e0bd49bf6ef..0750f472341 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -15,35 +15,33 @@ import { __ } from '~/locale';
import loadAwardsHandler from '~/awards_handler';
import { initCommitBoxInfo } from '~/projects/commit_box/info';
-document.addEventListener('DOMContentLoaded', () => {
- const hasPerfBar = document.querySelector('.with-performance-bar');
- const performanceHeight = hasPerfBar ? 35 : 0;
- initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
- new ZenMode();
- new ShortcutsNavigation();
+const hasPerfBar = document.querySelector('.with-performance-bar');
+const performanceHeight = hasPerfBar ? 35 : 0;
+initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
+new ZenMode();
+new ShortcutsNavigation();
- initCommitBoxInfo();
+initCommitBoxInfo();
- initNotes();
+initNotes();
- const filesContainer = $('.js-diffs-batch');
+const filesContainer = $('.js-diffs-batch');
- if (filesContainer.length) {
- const batchPath = filesContainer.data('diffFilesPath');
+if (filesContainer.length) {
+ const batchPath = filesContainer.data('diffFilesPath');
- axios
- .get(batchPath)
- .then(({ data }) => {
- filesContainer.html($(data.html));
- syntaxHighlight(filesContainer);
- handleLocationHash();
- new Diff();
- })
- .catch(() => {
- flash({ message: __('An error occurred while retrieving diff files') });
- });
- } else {
- new Diff();
- }
- loadAwardsHandler();
-});
+ axios
+ .get(batchPath)
+ .then(({ data }) => {
+ filesContainer.html($(data.html));
+ syntaxHighlight(filesContainer);
+ handleLocationHash();
+ new Diff();
+ })
+ .catch(() => {
+ flash({ message: __('An error occurred while retrieving diff files') });
+ });
+} else {
+ new Diff();
+}
+loadAwardsHandler();
diff --git a/app/assets/javascripts/pages/projects/commits/show/index.js b/app/assets/javascripts/pages/projects/commits/show/index.js
index b456baac612..6239e4c99d2 100644
--- a/app/assets/javascripts/pages/projects/commits/show/index.js
+++ b/app/assets/javascripts/pages/projects/commits/show/index.js
@@ -1,12 +1,9 @@
import CommitsList from '~/commits';
import GpgBadges from '~/gpg_badges';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-
import mountCommits from '~/projects/commits';
-document.addEventListener('DOMContentLoaded', () => {
- new CommitsList(document.querySelector('.js-project-commits-show').dataset.commitsLimit); // eslint-disable-line no-new
- new ShortcutsNavigation(); // eslint-disable-line no-new
- GpgBadges.fetch();
- mountCommits(document.getElementById('js-author-dropdown'));
-});
+new CommitsList(document.querySelector('.js-project-commits-show').dataset.commitsLimit); // eslint-disable-line no-new
+new ShortcutsNavigation(); // eslint-disable-line no-new
+GpgBadges.fetch();
+mountCommits(document.getElementById('js-author-dropdown'));
diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js
index 477a1ab887b..19aeb1d1ecf 100644
--- a/app/assets/javascripts/pages/projects/new/index.js
+++ b/app/assets/javascripts/pages/projects/new/index.js
@@ -2,46 +2,28 @@ import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new';
import { __ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import Tracking from '~/tracking';
-import { isExperimentEnabled } from '~/lib/utils/experimentation';
document.addEventListener('DOMContentLoaded', () => {
initProjectVisibilitySelector();
initProjectNew.bindEvents();
- const { category, property } = gon.tracking_data ?? { category: 'projects:new' };
- const hasNewCreateProjectUi = isExperimentEnabled('newCreateProjectUi');
+ import(
+ /* webpackChunkName: 'experiment_new_project_creation' */ '../../../projects/experiment_new_project_creation'
+ )
+ .then(m => {
+ const el = document.querySelector('.js-experiment-new-project-creation');
- if (!hasNewCreateProjectUi) {
- // Setting additional tracking for HAML template
+ if (!el) {
+ return;
+ }
- Array.from(
- document.querySelectorAll('.project-edit-container [data-experiment-track-label]'),
- ).forEach(node =>
- node.addEventListener('click', event => {
- const { experimentTrackLabel: label } = event.currentTarget.dataset;
- Tracking.event(category, 'click_tab', { property, label });
- }),
- );
- } else {
- import(
- /* webpackChunkName: 'experiment_new_project_creation' */ '../../../projects/experiment_new_project_creation'
- )
- .then(m => {
- const el = document.querySelector('.js-experiment-new-project-creation');
-
- if (!el) {
- return;
- }
-
- const config = {
- hasErrors: 'hasErrors' in el.dataset,
- isCiCdAvailable: 'isCiCdAvailable' in el.dataset,
- };
- m.default(el, config);
- })
- .catch(() => {
- createFlash(__('An error occurred while loading project creation UI'));
- });
- }
+ const config = {
+ hasErrors: 'hasErrors' in el.dataset,
+ isCiCdAvailable: 'isCiCdAvailable' in el.dataset,
+ };
+ m.default(el, config);
+ })
+ .catch(() => {
+ createFlash(__('An error occurred while loading project creation UI'));
+ });
});
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index 5317093c4cf..8c7aa04a0b6 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -9,47 +9,11 @@ import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
import projectSelect from '../../project_select';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import initClonePanel from '~/clone_panel';
export default class Project {
constructor() {
- const $cloneOptions = $('ul.clone-options-dropdown');
- if ($cloneOptions.length) {
- const $projectCloneField = $('#project_clone');
- const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label');
- const mobileCloneField = document.querySelector(
- '.js-mobile-git-clone .js-clone-dropdown-label',
- );
-
- const selectedCloneOption = $cloneBtnLabel.text().trim();
- if (selectedCloneOption.length > 0) {
- $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
- }
-
- $('a', $cloneOptions).on('click', e => {
- e.preventDefault();
- const $this = $(e.currentTarget);
- const url = $this.attr('href');
- const cloneType = $this.data('cloneType');
-
- $('.is-active', $cloneOptions).removeClass('is-active');
- $(`a[data-clone-type="${cloneType}"]`).each(function() {
- const $el = $(this);
- const activeText = $el.find('.dropdown-menu-inner-title').text();
- const $container = $el.closest('.project-clone-holder');
- const $label = $container.find('.js-clone-dropdown-label');
-
- $el.toggleClass('is-active');
- $label.text(activeText);
- });
-
- if (mobileCloneField) {
- mobileCloneField.dataset.clipboardText = url;
- } else {
- $projectCloneField.val(url);
- }
- $('.js-git-empty .js-clone').text(url);
- });
- }
+ initClonePanel();
// Ref switcher
if (document.querySelector('.js-project-refs-dropdown')) {
diff --git a/app/assets/javascripts/pages/projects/settings/access_tokens/index.js b/app/assets/javascripts/pages/projects/settings/access_tokens/index.js
index ae2209b0292..22dddb72f98 100644
--- a/app/assets/javascripts/pages/projects/settings/access_tokens/index.js
+++ b/app/assets/javascripts/pages/projects/settings/access_tokens/index.js
@@ -1,3 +1,3 @@
import initExpiresAtField from '~/access_tokens';
-document.addEventListener('DOMContentLoaded', initExpiresAtField);
+initExpiresAtField();
diff --git a/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js b/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js
index ffc84dc106b..1dc238b56b4 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js
@@ -1,3 +1,3 @@
import initForm from '../form';
-document.addEventListener('DOMContentLoaded', initForm);
+initForm();
diff --git a/app/assets/javascripts/performance/constants.js b/app/assets/javascripts/performance/constants.js
index 816eb9b3a66..069f3c265f3 100644
--- a/app/assets/javascripts/performance/constants.js
+++ b/app/assets/javascripts/performance/constants.js
@@ -19,16 +19,27 @@ export const SNIPPET_MEASURE_BLOBS_CONTENT = 'snippet-blobs-content';
// Marks
export const WEBIDE_MARK_APP_START = 'webide-app-start';
-export const WEBIDE_MARK_TREE_START = 'webide-tree-start';
-export const WEBIDE_MARK_TREE_FINISH = 'webide-tree-finished';
-export const WEBIDE_MARK_FILE_START = 'webide-file-start';
export const WEBIDE_MARK_FILE_CLICKED = 'webide-file-clicked';
export const WEBIDE_MARK_FILE_FINISH = 'webide-file-finished';
+export const WEBIDE_MARK_REPO_EDITOR_START = 'webide-init-editor-start';
+export const WEBIDE_MARK_REPO_EDITOR_FINISH = 'webide-init-editor-finish';
+export const WEBIDE_MARK_FETCH_BRANCH_DATA_START = 'webide-getBranchData-start';
+export const WEBIDE_MARK_FETCH_BRANCH_DATA_FINISH = 'webide-getBranchData-finish';
+export const WEBIDE_MARK_FETCH_FILE_DATA_START = 'webide-getFileData-start';
+export const WEBIDE_MARK_FETCH_FILE_DATA_FINISH = 'webide-getFileData-finish';
+export const WEBIDE_MARK_FETCH_FILES_START = 'webide-getFiles-start';
+export const WEBIDE_MARK_FETCH_FILES_FINISH = 'webide-getFiles-finish';
+export const WEBIDE_MARK_FETCH_PROJECT_DATA_START = 'webide-getProjectData-start';
+export const WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH = 'webide-getProjectData-finish';
// Measures
-export const WEBIDE_MEASURE_TREE_FROM_REQUEST = 'webide-tree-loading-from-request';
-export const WEBIDE_MEASURE_FILE_FROM_REQUEST = 'webide-file-loading-from-request';
export const WEBIDE_MEASURE_FILE_AFTER_INTERACTION = 'webide-file-loading-after-interaction';
+export const WEBIDE_MEASURE_FETCH_PROJECT_DATA = 'WebIDE: Project data';
+export const WEBIDE_MEASURE_FETCH_BRANCH_DATA = 'WebIDE: Branch data';
+export const WEBIDE_MEASURE_FETCH_FILE_DATA = 'WebIDE: File data';
+export const WEBIDE_MEASURE_BEFORE_VUE = 'WebIDE: Before Vue app';
+export const WEBIDE_MEASURE_REPO_EDITOR = 'WebIDE: Repo Editor';
+export const WEBIDE_MEASURE_FETCH_FILES = 'WebIDE: Fetch Files';
//
// MR Diffs namespace
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index 8c5f45e9d34..d4857a19ff7 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -7,6 +7,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-buy-pipeline-minutes-notification-callout',
'.js-token-expiry-callout',
'.js-registration-enabled-callout',
+ '.js-new-user-signups-cap-reached',
];
const initCallouts = () => {
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
new file mode 100644
index 00000000000..9279273283e
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
@@ -0,0 +1,139 @@
+<script>
+import {
+ GlButton,
+ GlForm,
+ GlFormCheckbox,
+ GlFormInput,
+ GlFormGroup,
+ GlFormTextarea,
+ GlSprintf,
+} from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ GlForm,
+ GlFormCheckbox,
+ GlFormInput,
+ GlFormGroup,
+ GlFormTextarea,
+ GlSprintf,
+ },
+ props: {
+ defaultBranch: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ defaultMessage: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ isSaving: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ message: this.defaultMessage,
+ branch: this.defaultBranch,
+ openMergeRequest: false,
+ };
+ },
+ computed: {
+ isDefaultBranch() {
+ return this.branch === this.defaultBranch;
+ },
+ submitDisabled() {
+ return !(this.message && this.branch);
+ },
+ },
+ methods: {
+ onSubmit() {
+ this.$emit('submit', {
+ message: this.message,
+ branch: this.branch,
+ openMergeRequest: this.openMergeRequest,
+ });
+ },
+ onReset() {
+ this.$emit('cancel');
+ },
+ },
+ i18n: {
+ commitMessage: __('Commit message'),
+ targetBranch: __('Target Branch'),
+ startMergeRequest: __('Start a %{new_merge_request} with these changes'),
+ newMergeRequest: __('new merge request'),
+ commitChanges: __('Commit changes'),
+ cancel: __('Cancel'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-form @submit.prevent="onSubmit" @reset.prevent="onReset">
+ <gl-form-group
+ id="commit-group"
+ :label="$options.i18n.commitMessage"
+ label-cols-sm="2"
+ label-for="commit-message"
+ >
+ <gl-form-textarea
+ id="commit-message"
+ v-model="message"
+ class="gl-font-monospace!"
+ required
+ :placeholder="defaultMessage"
+ />
+ </gl-form-group>
+ <gl-form-group
+ id="target-branch-group"
+ :label="$options.i18n.targetBranch"
+ label-cols-sm="2"
+ label-for="target-branch-field"
+ >
+ <gl-form-input
+ id="target-branch-field"
+ v-model="branch"
+ class="gl-font-monospace!"
+ required
+ />
+ <gl-form-checkbox
+ v-if="!isDefaultBranch"
+ v-model="openMergeRequest"
+ data-testid="new-mr-checkbox"
+ class="gl-mt-3"
+ >
+ <gl-sprintf :message="$options.i18n.startMergeRequest">
+ <template #new_merge_request>
+ <strong>{{ $options.i18n.newMergeRequest }}</strong>
+ </template>
+ </gl-sprintf>
+ </gl-form-checkbox>
+ </gl-form-group>
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-p-5 gl-bg-gray-10 gl-border-t-gray-100 gl-border-t-solid gl-border-t-1"
+ >
+ <gl-button
+ type="submit"
+ class="js-no-auto-disable"
+ category="primary"
+ variant="success"
+ :disabled="submitDisabled"
+ :loading="isSaving"
+ >
+ {{ $options.i18n.commitChanges }}
+ </gl-button>
+ <gl-button type="reset" category="secondary" class="gl-mr-3">
+ {{ $options.i18n.cancel }}
+ </gl-button>
+ </div>
+ </gl-form>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/text_editor.vue b/app/assets/javascripts/pipeline_editor/components/text_editor.vue
index a925077c906..22f2a32c9ac 100644
--- a/app/assets/javascripts/pipeline_editor/components/text_editor.vue
+++ b/app/assets/javascripts/pipeline_editor/components/text_editor.vue
@@ -5,22 +5,10 @@ export default {
components: {
EditorLite,
},
- props: {
- value: {
- type: String,
- required: false,
- default: '',
- },
- },
};
</script>
<template>
<div class="gl-border-solid gl-border-gray-100 gl-border-1">
- <editor-lite
- v-model="value"
- file-name="*.yml"
- :editor-options="{ readOnly: true }"
- @editor-ready="$emit('editor-ready')"
- />
+ <editor-lite file-name="*.yml" v-bind="$attrs" v-on="$listeners" />
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
new file mode 100644
index 00000000000..11bca42fd69
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
@@ -0,0 +1,26 @@
+mutation commitCIFileMutation(
+ $projectPath: ID!
+ $branch: String!
+ $startBranch: String
+ $message: String!
+ $filePath: String!
+ $lastCommitId: String!
+ $content: String
+) {
+ commitCreate(
+ input: {
+ projectPath: $projectPath
+ branch: $branch
+ startBranch: $startBranch
+ message: $message
+ actions: [
+ { action: UPDATE, filePath: $filePath, lastCommitId: $lastCommitId, content: $content }
+ ]
+ }
+ ) {
+ commit {
+ id
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index ccd7b74064f..8268a907a29 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -10,7 +10,11 @@ import PipelineEditorApp from './pipeline_editor_app.vue';
export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
const el = document.querySelector(selector);
- const { projectPath, defaultBranch, ciConfigPath } = el?.dataset;
+ if (!el) {
+ return null;
+ }
+
+ const { ciConfigPath, commitId, defaultBranch, newMergeRequestPath, projectPath } = el?.dataset;
Vue.use(VueApollo);
@@ -24,9 +28,11 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
render(h) {
return h(PipelineEditorApp, {
props: {
- projectPath,
- defaultBranch,
ciConfigPath,
+ commitId,
+ defaultBranch,
+ newMergeRequestPath,
+ projectPath,
},
});
},
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index 50b946af456..59635296de4 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -1,20 +1,33 @@
<script>
-import { GlLoadingIcon, GlAlert, GlTabs, GlTab } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
+import { redirectTo, mergeUrlParams, refreshCurrentPage } from '~/lib/utils/url_utility';
-import TextEditor from './components/text_editor.vue';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
+import CommitForm from './components/commit/commit_form.vue';
+import TextEditor from './components/text_editor.vue';
+import commitCiFileMutation from './graphql/mutations/commit_ci_file.mutation.graphql';
import getBlobContent from './graphql/queries/blob_content.graphql';
+const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
+const MR_TARGET_BRANCH = 'merge_request[target_branch]';
+
+const LOAD_FAILURE_NO_REF = 'LOAD_FAILURE_NO_REF';
+const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE';
+const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
+const COMMIT_FAILURE = 'COMMIT_FAILURE';
+const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
+
export default {
components: {
- GlLoadingIcon,
GlAlert,
- GlTabs,
+ GlLoadingIcon,
GlTab,
- TextEditor,
+ GlTabs,
PipelineGraph,
+ CommitForm,
+ TextEditor,
},
props: {
projectPath: {
@@ -26,16 +39,30 @@ export default {
required: false,
default: null,
},
+ commitId: {
+ type: String,
+ required: false,
+ default: null,
+ },
ciConfigPath: {
type: String,
required: true,
},
+ newMergeRequestPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
- error: null,
- content: '',
+ showFailureAlert: false,
+ failureType: null,
+ failureReasons: [],
+
+ isSaving: false,
editorIsReady: false,
+ content: '',
+ contentModel: '',
};
},
apollo: {
@@ -51,51 +78,168 @@ export default {
update(data) {
return data?.blobContent?.rawData;
},
+ result({ data }) {
+ this.contentModel = data?.blobContent?.rawData ?? '';
+ },
error(error) {
- this.error = error;
+ this.handleBlobContentError(error);
},
},
},
computed: {
- loading() {
+ isLoading() {
return this.$apollo.queries.content.loading;
},
- errorMessage() {
- const { message: generalReason, networkError } = this.error ?? {};
-
- const { data } = networkError?.response ?? {};
- // 404 for missing file uses `message`
- // 400 for a missing ref uses `error`
- const networkReason = data?.message ?? data?.error;
-
- const reason = networkReason ?? generalReason ?? this.$options.i18n.unknownError;
- return sprintf(this.$options.i18n.errorMessageWithReason, { reason });
+ defaultCommitMessage() {
+ return sprintf(this.$options.i18n.defaultCommitMessage, { sourcePath: this.ciConfigPath });
},
pipelineData() {
// Note data will loaded as part of https://gitlab.com/gitlab-org/gitlab/-/issues/263141
return {};
},
+ failure() {
+ switch (this.failureType) {
+ case LOAD_FAILURE_NO_REF:
+ return {
+ text: this.$options.errorTexts[LOAD_FAILURE_NO_REF],
+ variant: 'danger',
+ };
+ case LOAD_FAILURE_NO_FILE:
+ return {
+ text: this.$options.errorTexts[LOAD_FAILURE_NO_FILE],
+ variant: 'danger',
+ };
+ case LOAD_FAILURE_UNKNOWN:
+ return {
+ text: this.$options.errorTexts[LOAD_FAILURE_UNKNOWN],
+ variant: 'danger',
+ };
+ case COMMIT_FAILURE:
+ return {
+ text: this.$options.errorTexts[COMMIT_FAILURE],
+ variant: 'danger',
+ };
+ default:
+ return {
+ text: this.$options.errorTexts[DEFAULT_FAILURE],
+ variant: 'danger',
+ };
+ }
+ },
},
i18n: {
- unknownError: __('Unknown Error'),
- errorMessageWithReason: s__('Pipelines|CI file could not be loaded: %{reason}'),
+ defaultCommitMessage: __('Update %{sourcePath} file'),
tabEdit: s__('Pipelines|Write pipeline configuration'),
tabGraph: s__('Pipelines|Visualize'),
},
+ errorTexts: {
+ [LOAD_FAILURE_NO_REF]: s__(
+ 'Pipelines|Repository does not have a default branch, please set one.',
+ ),
+ [LOAD_FAILURE_NO_FILE]: s__('Pipelines|No CI file found in this repository, please add one.'),
+ [LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
+ [COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
+ },
+ methods: {
+ handleBlobContentError(error = {}) {
+ const { networkError } = error;
+
+ const { response } = networkError;
+ if (response?.status === 404) {
+ // 404 for missing CI file
+ this.reportFailure(LOAD_FAILURE_NO_FILE);
+ } else if (response?.status === 400) {
+ // 400 for a missing ref when no default branch is set
+ this.reportFailure(LOAD_FAILURE_NO_REF);
+ } else {
+ this.reportFailure(LOAD_FAILURE_UNKNOWN);
+ }
+ },
+ dismissFailure() {
+ this.showFailureAlert = false;
+ },
+ reportFailure(type, reasons = []) {
+ this.showFailureAlert = true;
+ this.failureType = type;
+ this.failureReasons = reasons;
+ },
+ redirectToNewMergeRequest(sourceBranch) {
+ const url = mergeUrlParams(
+ {
+ [MR_SOURCE_BRANCH]: sourceBranch,
+ [MR_TARGET_BRANCH]: this.defaultBranch,
+ },
+ this.newMergeRequestPath,
+ );
+ redirectTo(url);
+ },
+ async onCommitSubmit(event) {
+ this.isSaving = true;
+ const { message, branch, openMergeRequest } = event;
+
+ try {
+ const {
+ data: {
+ commitCreate: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: commitCiFileMutation,
+ variables: {
+ projectPath: this.projectPath,
+ branch,
+ startBranch: this.defaultBranch,
+ message,
+ filePath: this.ciConfigPath,
+ content: this.contentModel,
+ lastCommitId: this.commitId,
+ },
+ });
+
+ if (errors?.length) {
+ this.reportFailure(COMMIT_FAILURE, errors);
+ return;
+ }
+
+ if (openMergeRequest) {
+ this.redirectToNewMergeRequest(branch);
+ } else {
+ // Refresh the page to ensure commit is updated
+ refreshCurrentPage();
+ }
+ } catch (error) {
+ this.reportFailure(COMMIT_FAILURE, [error?.message]);
+ } finally {
+ this.isSaving = false;
+ }
+ },
+ onCommitCancel() {
+ this.contentModel = this.content;
+ },
+ },
};
</script>
<template>
<div class="gl-mt-4">
- <gl-alert v-if="error" :dismissible="false" variant="danger">{{ errorMessage }}</gl-alert>
+ <gl-alert
+ v-if="showFailureAlert"
+ :variant="failure.variant"
+ :dismissible="true"
+ @dismiss="dismissFailure"
+ >
+ {{ failure.text }}
+ <ul v-if="failureReasons.length" class="gl-mb-0">
+ <li v-for="reason in failureReasons" :key="reason">{{ reason }}</li>
+ </ul>
+ </gl-alert>
<div class="gl-mt-4">
- <gl-loading-icon v-if="loading" size="lg" />
- <div v-else class="file-editor">
+ <gl-loading-icon v-if="isLoading" size="lg" class="gl-m-3" />
+ <div v-else class="file-editor gl-mb-3">
<gl-tabs>
<!-- editor should be mounted when its tab is visible, so the container has a size -->
<gl-tab :title="$options.i18n.tabEdit" :lazy="!editorIsReady">
<!-- editor should be mounted only once, when the tab is displayed -->
- <text-editor v-model="content" @editor-ready="editorIsReady = true" />
+ <text-editor v-model="contentModel" @editor-ready="editorIsReady = true" />
</gl-tab>
<gl-tab :title="$options.i18n.tabGraph">
@@ -103,6 +247,13 @@ export default {
</gl-tab>
</gl-tabs>
</div>
+ <commit-form
+ :default-branch="defaultBranch"
+ :default-message="defaultCommitMessage"
+ :is-saving="isSaving"
+ @cancel="onCommitCancel"
+ @submit="onCommitSubmit"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
index e52afe08336..1ea71610897 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
@@ -32,7 +32,7 @@ export default {
if (action.scheduled_at) {
const confirmationMessage = sprintf(
s__(
- "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.",
+ 'DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after its timer finishes.',
),
{ jobName: action.name },
);
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
index 7afbb59cbd6..4b4fb6082c6 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
@@ -1,6 +1,13 @@
<script>
-import { mapGetters } from 'vuex';
-import { GlModalDirective, GlTooltipDirective, GlFriendlyWrap, GlIcon, GlButton } from '@gitlab/ui';
+import { mapState, mapGetters, mapActions } from 'vuex';
+import {
+ GlModalDirective,
+ GlTooltipDirective,
+ GlFriendlyWrap,
+ GlIcon,
+ GlButton,
+ GlPagination,
+} from '@gitlab/ui';
import { __ } from '~/locale';
import TestCaseDetails from './test_case_details.vue';
@@ -10,6 +17,7 @@ export default {
GlIcon,
GlFriendlyWrap,
GlButton,
+ GlPagination,
TestCaseDetails,
},
directives: {
@@ -24,11 +32,15 @@ export default {
},
},
computed: {
- ...mapGetters(['getSuiteTests']),
+ ...mapState(['pageInfo']),
+ ...mapGetters(['getSuiteTests', 'getSuiteTestCount']),
hasSuites() {
return this.getSuiteTests.length > 0;
},
},
+ methods: {
+ ...mapActions(['setPage']),
+ },
wrapSymbols: ['::', '#', '.', '_', '-', '/', '\\'],
};
</script>
@@ -129,6 +141,14 @@ export default {
</div>
</div>
</div>
+
+ <gl-pagination
+ v-model="pageInfo.page"
+ class="gl-display-flex gl-justify-content-center"
+ :per-page="pageInfo.perPage"
+ :total-items="getSuiteTestCount"
+ @input="setPage"
+ />
</div>
<div v-else>
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/actions.js b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
index f10bbeec77c..3c664457756 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/actions.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
@@ -47,6 +47,7 @@ export const fetchTestSuite = ({ state, commit, dispatch }, index) => {
});
};
+export const setPage = ({ commit }, page) => commit(types.SET_PAGE, page);
export const setSelectedSuiteIndex = ({ commit }, data) =>
commit(types.SET_SELECTED_SUITE_INDEX, data);
export const removeSelectedSuiteIndex = ({ commit }) =>
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/getters.js b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
index c123014756d..56f769c00fa 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/getters.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
@@ -14,5 +14,10 @@ export const getSelectedSuite = state =>
export const getSuiteTests = state => {
const { test_cases: testCases = [] } = getSelectedSuite(state);
- return testCases.map(addIconStatus);
+ const { page, perPage } = state.pageInfo;
+ const start = (page - 1) * perPage;
+
+ return testCases.map(addIconStatus).slice(start, start + perPage);
};
+
+export const getSuiteTestCount = state => getSelectedSuite(state)?.test_cases?.length || 0;
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js b/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
index 52345888cb0..803f6bf60b1 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
@@ -1,3 +1,4 @@
+export const SET_PAGE = 'SET_PAGE';
export const SET_SELECTED_SUITE_INDEX = 'SET_SELECTED_SUITE_INDEX';
export const SET_SUMMARY = 'SET_SUMMARY';
export const SET_SUITE = 'SET_SUITE';
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
index 3652a12a6ba..cf0bf8483dd 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
@@ -1,6 +1,14 @@
import * as types from './mutation_types';
export default {
+ [types.SET_PAGE](state, page) {
+ Object.assign(state, {
+ pageInfo: Object.assign(state.pageInfo, {
+ page,
+ }),
+ });
+ },
+
[types.SET_SUITE](state, { suite = {}, index = null }) {
state.testReports.test_suites[index] = { ...suite, hasFullSuite: true };
},
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/state.js b/app/assets/javascripts/pipelines/stores/test_reports/state.js
index af79521d68a..7f5da549a9d 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/state.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/state.js
@@ -4,4 +4,8 @@ export default ({ summaryEndpoint = '', suiteEndpoint = '' }) => ({
testReports: {},
selectedSuiteIndex: null,
isLoading: false,
+ pageInfo: {
+ page: 1,
+ perPage: 20,
+ },
});
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index db2b0856e1b..f7d823802b6 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -4,110 +4,116 @@ import $ from 'jquery';
import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button';
import { s__ } from './locale';
+import { loadCSSFile } from './lib/utils/css_utils';
const projectSelect = () => {
- $('.ajax-project-select').each(function(i, select) {
- let placeholder;
- const simpleFilter = $(select).data('simpleFilter') || false;
- const isInstantiated = $(select).data('select2');
- this.groupId = $(select).data('groupId');
- this.userId = $(select).data('userId');
- this.includeGroups = $(select).data('includeGroups');
- this.allProjects = $(select).data('allProjects') || false;
- this.orderBy = $(select).data('orderBy') || 'id';
- this.withIssuesEnabled = $(select).data('withIssuesEnabled');
- this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
- this.withShared =
- $(select).data('withShared') === undefined ? true : $(select).data('withShared');
- this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
- this.allowClear = $(select).data('allowClear') || false;
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ $('.ajax-project-select').each(function(i, select) {
+ let placeholder;
+ const simpleFilter = $(select).data('simpleFilter') || false;
+ const isInstantiated = $(select).data('select2');
+ this.groupId = $(select).data('groupId');
+ this.userId = $(select).data('userId');
+ this.includeGroups = $(select).data('includeGroups');
+ this.allProjects = $(select).data('allProjects') || false;
+ this.orderBy = $(select).data('orderBy') || 'id';
+ this.withIssuesEnabled = $(select).data('withIssuesEnabled');
+ this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
+ this.withShared =
+ $(select).data('withShared') === undefined ? true : $(select).data('withShared');
+ this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
+ this.allowClear = $(select).data('allowClear') || false;
- placeholder = s__('ProjectSelect|Search for project');
- if (this.includeGroups) {
- placeholder += s__('ProjectSelect| or group');
- }
-
- $(select).select2({
- placeholder,
- minimumInputLength: 0,
- query: query => {
- let projectsCallback;
- const finalCallback = function(projects) {
- const data = {
- results: projects,
- };
- return query.callback(data);
- };
+ placeholder = s__('ProjectSelect|Search for project');
if (this.includeGroups) {
- projectsCallback = function(projects) {
- const groupsCallback = function(groups) {
- const data = groups.concat(projects);
- return finalCallback(data);
- };
- return Api.groups(query.term, {}, groupsCallback);
- };
- } else {
- projectsCallback = finalCallback;
- }
- if (this.groupId) {
- return Api.groupProjects(
- this.groupId,
- query.term,
- {
- with_issues_enabled: this.withIssuesEnabled,
- with_merge_requests_enabled: this.withMergeRequestsEnabled,
- with_shared: this.withShared,
- include_subgroups: this.includeProjectsInSubgroups,
- order_by: 'similarity',
- },
- projectsCallback,
- );
- } else if (this.userId) {
- return Api.userProjects(
- this.userId,
- query.term,
- {
- with_issues_enabled: this.withIssuesEnabled,
- with_merge_requests_enabled: this.withMergeRequestsEnabled,
- with_shared: this.withShared,
- include_subgroups: this.includeProjectsInSubgroups,
- },
- projectsCallback,
- );
+ placeholder += s__('ProjectSelect| or group');
}
- return Api.projects(
- query.term,
- {
- order_by: this.orderBy,
- with_issues_enabled: this.withIssuesEnabled,
- with_merge_requests_enabled: this.withMergeRequestsEnabled,
- membership: !this.allProjects,
+
+ $(select).select2({
+ placeholder,
+ minimumInputLength: 0,
+ query: query => {
+ let projectsCallback;
+ const finalCallback = function(projects) {
+ const data = {
+ results: projects,
+ };
+ return query.callback(data);
+ };
+ if (this.includeGroups) {
+ projectsCallback = function(projects) {
+ const groupsCallback = function(groups) {
+ const data = groups.concat(projects);
+ return finalCallback(data);
+ };
+ return Api.groups(query.term, {}, groupsCallback);
+ };
+ } else {
+ projectsCallback = finalCallback;
+ }
+ if (this.groupId) {
+ return Api.groupProjects(
+ this.groupId,
+ query.term,
+ {
+ with_issues_enabled: this.withIssuesEnabled,
+ with_merge_requests_enabled: this.withMergeRequestsEnabled,
+ with_shared: this.withShared,
+ include_subgroups: this.includeProjectsInSubgroups,
+ order_by: 'similarity',
+ },
+ projectsCallback,
+ );
+ } else if (this.userId) {
+ return Api.userProjects(
+ this.userId,
+ query.term,
+ {
+ with_issues_enabled: this.withIssuesEnabled,
+ with_merge_requests_enabled: this.withMergeRequestsEnabled,
+ with_shared: this.withShared,
+ include_subgroups: this.includeProjectsInSubgroups,
+ },
+ projectsCallback,
+ );
+ }
+ return Api.projects(
+ query.term,
+ {
+ order_by: this.orderBy,
+ with_issues_enabled: this.withIssuesEnabled,
+ with_merge_requests_enabled: this.withMergeRequestsEnabled,
+ membership: !this.allProjects,
+ },
+ projectsCallback,
+ );
+ },
+ id(project) {
+ if (simpleFilter) return project.id;
+ return JSON.stringify({
+ name: project.name,
+ url: project.web_url,
+ });
+ },
+ text(project) {
+ return project.name_with_namespace || project.name;
},
- projectsCallback,
- );
- },
- id(project) {
- if (simpleFilter) return project.id;
- return JSON.stringify({
- name: project.name,
- url: project.web_url,
- });
- },
- text(project) {
- return project.name_with_namespace || project.name;
- },
- initSelection(el, callback) {
- return Api.project(el.val()).then(({ data }) => callback(data));
- },
+ initSelection(el, callback) {
+ // eslint-disable-next-line promise/no-nesting
+ return Api.project(el.val()).then(({ data }) => callback(data));
+ },
- allowClear: this.allowClear,
+ allowClear: this.allowClear,
- dropdownCssClass: 'ajax-project-dropdown',
- });
- if (isInstantiated || simpleFilter) return select;
- return new ProjectSelectComboButton(select);
- });
+ dropdownCssClass: 'ajax-project-dropdown',
+ });
+ if (isInstantiated || simpleFilter) return select;
+ return new ProjectSelectComboButton(select);
+ });
+ })
+ .catch(() => {});
};
export default () => {
diff --git a/app/assets/javascripts/project_select_combo_button.js b/app/assets/javascripts/project_select_combo_button.js
index d3b5f532dc1..865dd23bd80 100644
--- a/app/assets/javascripts/project_select_combo_button.js
+++ b/app/assets/javascripts/project_select_combo_button.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import AccessorUtilities from './lib/utils/accessor';
+import { loadCSSFile } from './lib/utils/css_utils';
export default class ProjectSelectComboButton {
constructor(select) {
@@ -46,9 +47,14 @@ export default class ProjectSelectComboButton {
openDropdown(event) {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
- $(event.currentTarget)
- .siblings('.project-item-select')
- .select2('open');
+ // eslint-disable-next-line promise/no-nesting
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ $(event.currentTarget)
+ .siblings('.project-item-select')
+ .select2('open');
+ })
+ .catch(() => {});
})
.catch(() => {});
}
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
index f404e6030f4..2e16071e563 100644
--- a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
+++ b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
@@ -12,6 +12,7 @@ import ciCdProjectIllustration from '../illustrations/ci-cd-project.svg';
const BLANK_PANEL = 'blank_project';
const CI_CD_PANEL = 'cicd_for_external_repo';
+const LAST_ACTIVE_TAB_KEY = 'new_project_last_active_tab';
const PANELS = [
{
name: BLANK_PANEL,
@@ -105,7 +106,7 @@ export default {
this.handleLocationHashChange();
if (this.hasErrors) {
- this.activeTab = BLANK_PANEL;
+ this.activeTab = localStorage.getItem(LAST_ACTIVE_TAB_KEY) || BLANK_PANEL;
}
window.addEventListener('hashchange', () => {
@@ -127,6 +128,9 @@ export default {
handleLocationHashChange() {
this.activeTab = window.location.hash.substring(1) || null;
+ if (this.activeTab) {
+ localStorage.setItem(LAST_ACTIVE_TAB_KEY, this.activeTab);
+ }
},
},
diff --git a/app/assets/javascripts/registry/explorer/pages/index.vue b/app/assets/javascripts/registry/explorer/pages/index.vue
index 4ac0bca84c1..dca63e1a569 100644
--- a/app/assets/javascripts/registry/explorer/pages/index.vue
+++ b/app/assets/javascripts/registry/explorer/pages/index.vue
@@ -1,7 +1,3 @@
-<script>
-export default {};
-</script>
-
<template>
<div>
<router-view ref="router-view" />
diff --git a/app/assets/javascripts/registry/settings/components/expiration_dropdown.vue b/app/assets/javascripts/registry/settings/components/expiration_dropdown.vue
new file mode 100644
index 00000000000..d75fb31fd98
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/components/expiration_dropdown.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlFormGroup, GlFormSelect } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlFormSelect,
+ },
+ props: {
+ formOptions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ name: {
+ type: String,
+ required: true,
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group :id="`${name}-form-group`" :label-for="name" :label="label">
+ <gl-form-select :id="name" :value="value" :disabled="disabled" @input="$emit('input', $event)">
+ <option
+ v-for="option in formOptions"
+ :key="option.key"
+ :value="option.key"
+ data-testid="option"
+ >
+ {{ option.label }}
+ </option>
+ </gl-form-select>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/registry/settings/components/expiration_run_text.vue b/app/assets/javascripts/registry/settings/components/expiration_run_text.vue
new file mode 100644
index 00000000000..186ad2f34b9
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/components/expiration_run_text.vue
@@ -0,0 +1,31 @@
+<script>
+import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { NEXT_CLEANUP_LABEL, NOT_SCHEDULED_POLICY_TEXT } from '~/registry/settings/constants';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlFormInput,
+ },
+ props: {
+ value: {
+ type: String,
+ required: false,
+ default: NOT_SCHEDULED_POLICY_TEXT,
+ },
+ },
+ i18n: {
+ NEXT_CLEANUP_LABEL,
+ },
+};
+</script>
+
+<template>
+ <gl-form-group
+ id="expiration-policy-info-text-group"
+ :label="$options.i18n.NEXT_CLEANUP_LABEL"
+ label-for="expiration-policy-info-text"
+ >
+ <gl-form-input id="expiration-policy-info-text" class="gl-pl-0!" plaintext :value="value" />
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/registry/settings/components/expiration_textarea.vue b/app/assets/javascripts/registry/settings/components/expiration_textarea.vue
new file mode 100644
index 00000000000..1e1194ebb5c
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/components/expiration_textarea.vue
@@ -0,0 +1,109 @@
+<script>
+import { GlFormGroup, GlFormTextarea, GlSprintf, GlLink } from '@gitlab/ui';
+import { NAME_REGEX_LENGTH, TEXT_AREA_INVALID_FEEDBACK } from '../constants';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlFormTextarea,
+ GlSprintf,
+ GlLink,
+ },
+ inject: ['tagsRegexHelpPagePath'],
+ props: {
+ error: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ name: {
+ type: String,
+ required: true,
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ placeholder: {
+ type: String,
+ required: true,
+ },
+ description: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ textAreaLengthErrorMessage() {
+ return this.isInputValid(this.value) ? '' : TEXT_AREA_INVALID_FEEDBACK;
+ },
+ textAreaValidation() {
+ const nameRegexErrors = this.error || this.textAreaLengthErrorMessage;
+ return {
+ state: nameRegexErrors === null ? null : !nameRegexErrors,
+ message: nameRegexErrors,
+ };
+ },
+ internalValue: {
+ get() {
+ return this.value;
+ },
+ set(value) {
+ this.$emit('input', value);
+ this.$emit('validation', this.isInputValid(value));
+ },
+ },
+ },
+ methods: {
+ isInputValid(value) {
+ return !value || value.length <= NAME_REGEX_LENGTH;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group
+ :id="`${name}-form-group`"
+ :label-for="name"
+ :state="textAreaValidation.state"
+ :invalid-feedback="textAreaValidation.message"
+ >
+ <template #label>
+ <span data-testid="label">
+ <gl-sprintf :message="label">
+ <template #italic="{content}">
+ <i>{{ content }}</i>
+ </template>
+ </gl-sprintf>
+ </span>
+ </template>
+ <gl-form-textarea
+ :id="name"
+ v-model="internalValue"
+ :placeholder="placeholder"
+ :state="textAreaValidation.state"
+ :disabled="disabled"
+ trim
+ />
+ <template #description>
+ <span data-testid="description" class="gl-text-gray-400">
+ <gl-sprintf :message="description">
+ <template #link="{content}">
+ <gl-link :href="tagsRegexHelpPagePath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ </template>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/registry/settings/components/expiration_toggle.vue b/app/assets/javascripts/registry/settings/components/expiration_toggle.vue
new file mode 100644
index 00000000000..9dabe8ac51a
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/components/expiration_toggle.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlFormGroup, GlToggle, GlSprintf } from '@gitlab/ui';
+import { ENABLED_TEXT, DISABLED_TEXT, ENABLE_TOGGLE_DESCRIPTION } from '../constants';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlToggle,
+ GlSprintf,
+ },
+ props: {
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ value: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ i18n: {
+ ENABLE_TOGGLE_DESCRIPTION,
+ },
+ computed: {
+ enabled: {
+ get() {
+ return this.value;
+ },
+ set(value) {
+ this.$emit('input', value);
+ },
+ },
+ toggleStatusText() {
+ return this.enabled ? ENABLED_TEXT : DISABLED_TEXT;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group id="expiration-policy-toggle-group" label-for="expiration-policy-toggle">
+ <div class="gl-display-flex">
+ <gl-toggle id="expiration-policy-toggle" v-model="enabled" :disabled="disabled" />
+ <span class="gl-ml-5 gl-line-height-24" data-testid="description">
+ <gl-sprintf :message="$options.i18n.ENABLE_TOGGLE_DESCRIPTION">
+ <template #toggleStatus>
+ <strong>{{ toggleStatusText }}</strong>
+ </template>
+ </gl-sprintf>
+ </span>
+ </div>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/registry/settings/constants.js b/app/assets/javascripts/registry/settings/constants.js
index e790658f491..bc3ec3104ad 100644
--- a/app/assets/javascripts/registry/settings/constants.js
+++ b/app/assets/javascripts/registry/settings/constants.js
@@ -1,6 +1,6 @@
import { s__, __ } from '~/locale';
-export const SET_CLEANUP_POLICY_BUTTON = s__('ContainerRegistry|Set cleanup policy');
+export const SET_CLEANUP_POLICY_BUTTON = __('Save');
export const CLEANUP_POLICY_CARD_HEADER = s__('ContainerRegistry|Tag expiration policy');
export const UNAVAILABLE_FEATURE_TITLE = s__(
`ContainerRegistry|Cleanup policy for tags is disabled`,
@@ -12,3 +12,46 @@ export const UNAVAILABLE_USER_FEATURE_TEXT = __(`Please contact your administrat
export const UNAVAILABLE_ADMIN_FEATURE_TEXT = s__(
`ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature.`,
);
+
+export const TEXT_AREA_INVALID_FEEDBACK = s__(
+ 'ContainerRegistry|The value of this input should be less than 256 characters',
+);
+
+export const KEEP_HEADER_TEXT = s__('ContainerRegistry|Keep these tags');
+export const KEEP_INFO_TEXT = s__(
+ 'ContainerRegistry|Tags that match these rules will always be %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{secondStrongStart}latest%{secondStrongEnd} tag will always be kept.',
+);
+export const KEEP_N_LABEL = s__('ContainerRegistry|Keep the most recent:');
+export const NAME_REGEX_KEEP_LABEL = s__('ContainerRegistry|Keep tags matching:');
+export const NAME_REGEX_KEEP_PLACEHOLDER = 'production-v.*';
+export const NAME_REGEX_KEEP_DESCRIPTION = s__(
+ 'ContainerRegistry|Tags with names matching this regex pattern will be kept. %{linkStart}More information%{linkEnd}',
+);
+
+export const REMOVE_HEADER_TEXT = s__('ContainerRegistry|Remove these tags');
+export const REMOVE_INFO_TEXT = s__(
+ 'ContainerRegistry|Tags that match these rules will be %{strongStart}removed%{strongEnd}, unless kept by a rule above.',
+);
+export const EXPIRATION_SCHEDULE_LABEL = s__('ContainerRegistry|Remove tags older than:');
+export const NAME_REGEX_LABEL = s__('ContainerRegistry|Remove tags matching:');
+export const NAME_REGEX_PLACEHOLDER = '.*';
+export const NAME_REGEX_DESCRIPTION = s__(
+ 'ContainerRegistry|Tags with names matching this regex pattern will be removed. %{linkStart}More information%{linkEnd}',
+);
+
+export const ENABLED_TEXT = __('Enabled');
+export const DISABLED_TEXT = __('Disabled');
+
+export const ENABLE_TOGGLE_DESCRIPTION = s__(
+ 'ContainerRegistry|%{toggleStatus} - Tags matching the rules defined below will be automatically scheduled for deletion.',
+);
+
+export const CADENCE_LABEL = s__('ContainerRegistry|Run cleanup every:');
+
+export const NEXT_CLEANUP_LABEL = s__('ContainerRegistry|Next cleanup scheduled to run on:');
+export const NOT_SCHEDULED_POLICY_TEXT = s__('ContainerRegistry|Not yet scheduled');
+export const EXPIRATION_POLICY_FOOTER_NOTE = s__(
+ 'ContainerRegistry|Note: Any policy update will result in a change to the scheduled run date and time',
+);
+
+export const NAME_REGEX_LENGTH = 255;
diff --git a/app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql b/app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql
index 224e0ed9472..1d6c89133af 100644
--- a/app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql
+++ b/app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql
@@ -5,4 +5,5 @@ fragment ContainerExpirationPolicyFields on ContainerExpirationPolicy {
nameRegex
nameRegexKeep
olderThan
+ nextRunAt
}
diff --git a/app/assets/javascripts/registry/settings/registry_settings_bundle.js b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
index f7b1c5abd3a..6a4584b1b28 100644
--- a/app/assets/javascripts/registry/settings/registry_settings_bundle.js
+++ b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
@@ -13,7 +13,13 @@ export default () => {
if (!el) {
return null;
}
- const { projectPath, isAdmin, adminSettingsPath, enableHistoricEntries } = el.dataset;
+ const {
+ isAdmin,
+ enableHistoricEntries,
+ projectPath,
+ adminSettingsPath,
+ tagsRegexHelpPagePath,
+ } = el.dataset;
return new Vue({
el,
apolloProvider,
@@ -21,10 +27,11 @@ export default () => {
RegistrySettingsApp,
},
provide: {
- projectPath,
isAdmin: parseBoolean(isAdmin),
- adminSettingsPath,
enableHistoricEntries: parseBoolean(enableHistoricEntries),
+ projectPath,
+ adminSettingsPath,
+ tagsRegexHelpPagePath,
},
render(createElement) {
return createElement('registry-settings-app', {});
diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue
index f245e2bfd2f..0e9975ea81f 100644
--- a/app/assets/javascripts/reports/components/report_section.vue
+++ b/app/assets/javascripts/reports/components/report_section.vue
@@ -3,7 +3,7 @@ import { __ } from '~/locale';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import Popover from '~/vue_shared/components/help_popover.vue';
import IssuesList from './issues_list.vue';
-import { status } from '../constants';
+import { status, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '../constants';
export default {
name: 'ReportSection',
@@ -152,12 +152,12 @@ export default {
},
slotName() {
if (this.isSuccess) {
- return 'success';
+ return SLOT_SUCCESS;
} else if (this.isLoading) {
- return 'loading';
+ return SLOT_LOADING;
}
- return 'error';
+ return SLOT_ERROR;
},
},
methods: {
diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/reports/constants.js
index b3905cbfcfb..9250bfd7678 100644
--- a/app/assets/javascripts/reports/constants.js
+++ b/app/assets/javascripts/reports/constants.js
@@ -18,10 +18,18 @@ export const ICON_SUCCESS = 'success';
export const ICON_NOTFOUND = 'notfound';
export const status = {
- LOADING: 'LOADING',
- ERROR: 'ERROR',
- SUCCESS: 'SUCCESS',
+ LOADING,
+ ERROR,
+ SUCCESS,
};
export const ACCESSIBILITY_ISSUE_ERROR = 'error';
export const ACCESSIBILITY_ISSUE_WARNING = 'warning';
+
+/**
+ * Slot names for the ReportSection component, corresponding to the success,
+ * loading and error statuses.
+ */
+export const SLOT_SUCCESS = 'success';
+export const SLOT_LOADING = 'loading';
+export const SLOT_ERROR = 'error';
diff --git a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
index cf6a0a4a151..3c1b3afe889 100644
--- a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
@@ -1,9 +1,11 @@
<script>
+import { GlButton } from '@gitlab/ui';
import { n__ } from '~/locale';
import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue';
export default {
components: {
+ GlButton,
UncollapsedAssigneeList,
},
inject: ['rootPath'],
@@ -27,9 +29,15 @@ export default {
<template>
<div class="gl-display-flex gl-flex-direction-column">
<div v-if="emptyUsers" data-testid="none">
- <span>
- {{ __('None') }}
- </span>
+ <span> {{ __('None') }} -</span>
+ <gl-button
+ data-testid="assign-yourself"
+ category="tertiary"
+ variant="link"
+ @click="$emit('assign-self')"
+ >
+ <span class="gl-text-gray-400">{{ __('assign yourself') }}</span>
+ </gl-button>
</div>
<uncollapsed-assignee-list v-else :users="users" :root-path="rootPath" />
</div>
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
index 51719df313f..1e3e870ec83 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -1,19 +1,18 @@
<script>
-import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
-import tooltip from '~/vue_shared/directives/tooltip';
const MARK_TEXT = __('Mark as done');
const TODO_TEXT = __('Add a To-Do');
export default {
- directives: {
- tooltip,
- },
components: {
GlIcon,
GlLoadingIcon,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
issuableId: {
type: Number,
@@ -71,16 +70,13 @@ export default {
<template>
<button
- v-tooltip
+ v-gl-tooltip.left.viewport
:class="buttonClasses"
:title="buttonTooltip"
:aria-label="buttonLabel"
:data-issuable-id="issuableId"
:data-issuable-type="issuableType"
type="button"
- data-container="body"
- data-placement="left"
- data-boundary="viewport"
@click="handleButtonClick"
>
<gl-icon
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index dccd6807f13..19846026726 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -15,6 +15,7 @@ import { parseBoolean, spriteIcon } from '../lib/utils/common_utils';
import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from './utils';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { fixTitle, dispose } from '~/tooltips';
+import { loadCSSFile } from '../lib/utils/css_utils';
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
@@ -592,92 +593,97 @@ function UsersSelect(currentUser, els, options = {}) {
if ($('.ajax-users-select').length) {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
- $('.ajax-users-select').each((i, select) => {
- const options = getAjaxUsersSelectOptions($(select), AJAX_USERS_SELECT_OPTIONS_MAP);
- options.skipLdap = $(select).hasClass('skip_ldap');
- const showNullUser = $(select).data('nullUser');
- const showAnyUser = $(select).data('anyUser');
- const showEmailUser = $(select).data('emailUser');
- const firstUser = $(select).data('firstUser');
- return $(select).select2({
- placeholder: __('Search for a user'),
- multiple: $(select).hasClass('multiselect'),
- minimumInputLength: 0,
- query(query) {
- return userSelect.users(query.term, options, users => {
- let name;
- const data = {
- results: users,
- };
- if (query.term.length === 0) {
- if (firstUser) {
- // Move current user to the front of the list
- const ref = data.results;
-
- for (let index = 0, len = ref.length; index < len; index += 1) {
- const obj = ref[index];
- if (obj.username === firstUser) {
- data.results.splice(index, 1);
- data.results.unshift(obj);
- break;
+ // eslint-disable-next-line promise/no-nesting
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ $('.ajax-users-select').each((i, select) => {
+ const options = getAjaxUsersSelectOptions($(select), AJAX_USERS_SELECT_OPTIONS_MAP);
+ options.skipLdap = $(select).hasClass('skip_ldap');
+ const showNullUser = $(select).data('nullUser');
+ const showAnyUser = $(select).data('anyUser');
+ const showEmailUser = $(select).data('emailUser');
+ const firstUser = $(select).data('firstUser');
+ return $(select).select2({
+ placeholder: __('Search for a user'),
+ multiple: $(select).hasClass('multiselect'),
+ minimumInputLength: 0,
+ query(query) {
+ return userSelect.users(query.term, options, users => {
+ let name;
+ const data = {
+ results: users,
+ };
+ if (query.term.length === 0) {
+ if (firstUser) {
+ // Move current user to the front of the list
+ const ref = data.results;
+
+ for (let index = 0, len = ref.length; index < len; index += 1) {
+ const obj = ref[index];
+ if (obj.username === firstUser) {
+ data.results.splice(index, 1);
+ data.results.unshift(obj);
+ break;
+ }
+ }
+ }
+ if (showNullUser) {
+ const nullUser = {
+ name: s__('UsersSelect|Unassigned'),
+ id: 0,
+ };
+ data.results.unshift(nullUser);
+ }
+ if (showAnyUser) {
+ name = showAnyUser;
+ if (name === true) {
+ name = s__('UsersSelect|Any User');
+ }
+ const anyUser = {
+ name,
+ id: null,
+ };
+ data.results.unshift(anyUser);
}
}
- }
- if (showNullUser) {
- const nullUser = {
- name: s__('UsersSelect|Unassigned'),
- id: 0,
- };
- data.results.unshift(nullUser);
- }
- if (showAnyUser) {
- name = showAnyUser;
- if (name === true) {
- name = s__('UsersSelect|Any User');
+ if (
+ showEmailUser &&
+ data.results.length === 0 &&
+ query.term.match(/^[^@]+@[^@]+$/)
+ ) {
+ const trimmed = query.term.trim();
+ const emailUser = {
+ name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }),
+ username: trimmed,
+ id: trimmed,
+ invite: true,
+ };
+ data.results.unshift(emailUser);
}
- const anyUser = {
- name,
- id: null,
- };
- data.results.unshift(anyUser);
- }
- }
- if (
- showEmailUser &&
- data.results.length === 0 &&
- query.term.match(/^[^@]+@[^@]+$/)
- ) {
- const trimmed = query.term.trim();
- const emailUser = {
- name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }),
- username: trimmed,
- id: trimmed,
- invite: true,
- };
- data.results.unshift(emailUser);
- }
- return query.callback(data);
+ return query.callback(data);
+ });
+ },
+ initSelection() {
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return userSelect.initSelection.apply(userSelect, args);
+ },
+ formatResult() {
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return userSelect.formatResult.apply(userSelect, args);
+ },
+ formatSelection() {
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return userSelect.formatSelection.apply(userSelect, args);
+ },
+ dropdownCssClass: 'ajax-users-dropdown',
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup(m) {
+ return m;
+ },
});
- },
- initSelection() {
- const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return userSelect.initSelection.apply(userSelect, args);
- },
- formatResult() {
- const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return userSelect.formatResult.apply(userSelect, args);
- },
- formatSelection() {
- const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return userSelect.formatSelection.apply(userSelect, args);
- },
- dropdownCssClass: 'ajax-users-dropdown',
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup(m) {
- return m;
- },
- });
- });
+ });
+ })
+ .catch(() => {});
})
.catch(() => {});
}
@@ -790,7 +796,7 @@ UsersSelect.prototype.renderRowAvatar = function(issuableType, user, img) {
const mergeIcon =
issuableType === 'merge_request' && !user.can_merge
- ? '<i class="fa fa-exclamation-triangle merge-icon"></i>'
+ ? `${spriteIcon('warning-solid', 's12 merge-icon')}`
: '';
return `<span class="position-relative mr-2">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js
index 66de4f8b682..29d067a46a6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js
@@ -6,6 +6,7 @@ export const RUNNING = 'running';
export const SUCCESS = 'success';
export const FAILED = 'failed';
export const CANCELED = 'canceled';
+export const SKIPPED = 'skipped';
// ACTION STATUSES
export const STOPPING = 'stopping';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
index 2f922b990d9..390469dec24 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
@@ -4,7 +4,15 @@ import { __ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import MemoryUsage from './memory_usage.vue';
-import { MANUAL_DEPLOY, WILL_DEPLOY, RUNNING, SUCCESS, FAILED, CANCELED } from './constants';
+import {
+ MANUAL_DEPLOY,
+ WILL_DEPLOY,
+ RUNNING,
+ SUCCESS,
+ FAILED,
+ CANCELED,
+ SKIPPED,
+} from './constants';
export default {
name: 'DeploymentInfo',
@@ -38,6 +46,7 @@ export default {
[SUCCESS]: __('Deployed to'),
[FAILED]: __('Failed to deploy to'),
[CANCELED]: __('Canceled deployment to'),
+ [SKIPPED]: __('Skipped deployment to'),
},
computed: {
deployTimeago() {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index d5fdbe726e9..6628ab7be83 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -7,12 +7,14 @@ import {
GlDropdownSectionHeader,
GlDropdownItem,
GlTooltipDirective,
+ GlModalDirective,
} from '@gitlab/ui';
import { n__, s__, sprintf } from '~/locale';
import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import MrWidgetIcon from './mr_widget_icon.vue';
+import MrWidgetHowToMergeModal from './mr_widget_how_to_merge_modal.vue';
export default {
name: 'MRWidgetHeader',
@@ -20,6 +22,7 @@ export default {
clipboardButton,
TooltipOnTruncate,
MrWidgetIcon,
+ MrWidgetHowToMergeModal,
GlButton,
GlDropdown,
GlDropdownSectionHeader,
@@ -27,6 +30,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
+ GlModalDirective,
},
props: {
mr: {
@@ -82,6 +86,9 @@ export default {
)
: '';
},
+ isFork() {
+ return this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath;
+ },
},
};
</script>
@@ -140,13 +147,22 @@ export default {
</gl-button>
</span>
<gl-button
+ v-gl-modal-directive="'modal-merge-info'"
:disabled="mr.sourceBranchRemoved"
- data-target="#modal_merge_info"
- data-toggle="modal"
class="js-check-out-branch gl-mr-3"
>
{{ s__('mrWidget|Check out branch') }}
</gl-button>
+ <mr-widget-how-to-merge-modal
+ :is-fork="isFork"
+ :can-merge="mr.canMerge"
+ :source-branch="mr.sourceBranch"
+ :source-project="mr.sourceProject"
+ :source-project-path="mr.sourceProjectFullPath"
+ :target-branch="mr.targetBranch"
+ :source-project-default-url="mr.sourceProjectDefaultUrl"
+ :reviewing-docs-path="mr.reviewingDocsPath"
+ />
</template>
<gl-dropdown
v-gl-tooltip
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
new file mode 100644
index 00000000000..957356eab27
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
@@ -0,0 +1,166 @@
+<script>
+/* eslint-disable @gitlab/require-i18n-strings */
+import { GlModal, GlLink, GlSprintf } from '@gitlab/ui';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { __ } from '~/locale';
+
+export default {
+ i18n: {
+ steps: {
+ step1: {
+ label: __('Step 1.'),
+ help: __('Fetch and check out the branch for this merge request'),
+ },
+ step2: {
+ label: __('Step 2.'),
+ help: __('Review the changes locally'),
+ },
+ step3: {
+ label: __('Step 3.'),
+ help: __('Merge the branch and fix any conflicts that come up'),
+ },
+ step4: {
+ label: __('Step 4.'),
+ help: __('Push the result of the merge to GitLab'),
+ },
+ },
+ copyCommands: __('Copy commands'),
+ tip: __(
+ '%{strongStart}Tip:%{strongEnd} You can also checkout merge requests locally by %{linkStart}following these guidelines%{linkEnd}',
+ ),
+ title: __('Check out, review, and merge locally'),
+ },
+ components: {
+ GlModal,
+ ClipboardButton,
+ GlLink,
+ GlSprintf,
+ },
+ props: {
+ canMerge: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isFork: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ sourceBranch: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ sourceProjectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ targetBranch: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ sourceProjectDefaultUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ reviewingDocsPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ mergeInfo1() {
+ return this.isFork
+ ? `git fetch "${this.sourceProjectDefaultUrl}" ${this.sourceBranch}\ngit checkout -b "${this.sourceProjectPath}-${this.sourceBranch}" FETCH_HEAD`
+ : `git fetch origin\ngit checkout -b "${this.sourceBranch}" "origin/${this.sourceBranch}"`;
+ },
+ mergeInfo2() {
+ return this.isFork
+ ? `git fetch origin\ngit checkout "${this.targetBranch}"\ngit merge --no-ff "${this.sourceProjectPath}-${this.sourceBranch}"`
+ : `git fetch origin\ngit checkout "${this.targetBranch}"\ngit merge --no-ff " ${this.sourceBranch}"`;
+ },
+ mergeInfo3() {
+ return this.canMerge
+ ? `git push origin "${this.targetBranch}"`
+ : __('Note that pushing to GitLab requires write access to this repository.');
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal modal-id="modal-merge-info" :title="$options.i18n.title" no-fade hide-footer>
+ <p>
+ <strong>
+ {{ $options.i18n.steps.step1.label }}
+ </strong>
+ {{ $options.i18n.steps.step1.help }}
+ </p>
+ <div class="gl-display-flex">
+ <pre class="gl-overflow-scroll gl-w-full" data-testid="how-to-merge-instructions">{{
+ mergeInfo1
+ }}</pre>
+ <clipboard-button
+ :text="mergeInfo1"
+ :title="$options.i18n.copyCommands"
+ class="gl-shadow-none! gl-bg-transparent! gl-flex-shrink-0"
+ />
+ </div>
+
+ <p>
+ <strong>
+ {{ $options.i18n.steps.step2.label }}
+ </strong>
+ {{ $options.i18n.steps.step2.help }}
+ </p>
+ <p>
+ <strong>
+ {{ $options.i18n.steps.step3.label }}
+ </strong>
+ {{ $options.i18n.steps.step3.help }}
+ </p>
+ <div class="gl-display-flex">
+ <pre class="gl-overflow-scroll gl-w-full" data-testid="how-to-merge-instructions">{{
+ mergeInfo2
+ }}</pre>
+ <clipboard-button
+ :text="mergeInfo2"
+ :title="$options.i18n.copyCommands"
+ class="gl-shadow-none! gl-bg-transparent! gl-flex-shrink-0"
+ />
+ </div>
+ <p>
+ <strong>
+ {{ $options.i18n.steps.step4.label }}
+ </strong>
+ {{ $options.i18n.steps.step4.help }}
+ </p>
+ <div class="gl-display-flex">
+ <pre class="gl-overflow-scroll gl-w-full" data-testid="how-to-merge-instructions">{{
+ mergeInfo3
+ }}</pre>
+ <clipboard-button
+ :text="mergeInfo3"
+ :title="$options.i18n.copyCommands"
+ class="gl-shadow-none! gl-bg-transparent! gl-flex-shrink-0"
+ />
+ </div>
+ <p v-if="reviewingDocsPath">
+ <gl-sprintf :message="$options.i18n.tip">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ <template #link="{ content }">
+ <gl-link class="gl-display-inline-block" :href="reviewingDocsPath" target="_blank">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index 55efd7e7d3b..dffe3cab904 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -1,7 +1,6 @@
<script>
import { isNumber } from 'lodash';
import ArtifactsApp from './artifacts_list_app.vue';
-import Deployment from './deployment/deployment.vue';
import MrWidgetContainer from './mr_widget_container.vue';
import MrWidgetPipeline from './mr_widget_pipeline.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -18,7 +17,7 @@ export default {
name: 'MrWidgetPipelineContainer',
components: {
ArtifactsApp,
- Deployment,
+ Deployment: () => import('./deployment/deployment.vue'),
MrWidgetContainer,
MrWidgetPipeline,
MergeTrainPositionIndicator: () =>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index 82566682bca..bc23ca6b1fc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -1,10 +1,11 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
export default {
components: {
ciIcon,
+ GlButton,
GlLoadingIcon,
},
props: {
@@ -32,21 +33,23 @@ export default {
};
</script>
<template>
- <div class="d-flex align-self-start">
+ <div class="gl-display-flex gl-align-self-start">
<div class="square s24 h-auto d-flex-center gl-mr-3">
- <div v-if="isLoading" class="mr-widget-icon d-inline-flex">
- <gl-loading-icon size="md" class="mr-loading-icon d-inline-flex" />
+ <div v-if="isLoading" class="mr-widget-icon gl-display-inline-flex">
+ <gl-loading-icon size="md" class="mr-loading-icon gl-display-inline-flex" />
</div>
<ci-icon v-else :status="statusObj" :size="24" />
</div>
- <button
+ <gl-button
v-if="showDisabledButton"
type="button"
- class="js-disabled-merge-button btn btn-success btn-sm"
- disabled="true"
+ category="primary"
+ variant="success"
+ class="js-disabled-merge-button"
+ :disabled="true"
>
{{ s__('mrWidget|Merge') }}
- </button>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index d421b744fa1..2df03fbc679 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -1,6 +1,7 @@
<script>
import $ from 'jquery';
import { escape } from 'lodash';
+import { GlButton, GlModalDirective } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover';
import StatusIcon from '../mr_widget_status_icon.vue';
@@ -9,6 +10,10 @@ export default {
name: 'MRWidgetConflicts',
components: {
StatusIcon,
+ GlButton,
+ },
+ directives: {
+ GlModalDirective,
},
props: {
/* TODO: This is providing all store and service down when it
@@ -89,22 +94,21 @@ To merge this request, first rebase locally.`)
</span>
</span>
<span v-if="showResolveButton" ref="popover">
- <a
+ <gl-button
:href="mr.conflictResolutionPath"
:disabled="mr.sourceBranchProtected"
- class="js-resolve-conflicts-button btn btn-default btn-sm"
+ class="js-resolve-conflicts-button"
>
{{ s__('mrWidget|Resolve conflicts') }}
- </a>
+ </gl-button>
</span>
- <button
+ <gl-button
v-if="mr.canMerge"
- class="js-merge-locally-button btn btn-default btn-sm"
- data-toggle="modal"
- data-target="#modal_merge_info"
+ v-gl-modal-directive="'modal-merge-info'"
+ class="js-merge-locally-button"
>
{{ s__('mrWidget|Merge locally') }}
- </button>
+ </gl-button>
</template>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
index ff0d065c71d..1c9909e7178 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
@@ -1,10 +1,11 @@
<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlTooltipDirective, GlFormCheckbox } from '@gitlab/ui';
import { SQUASH_BEFORE_MERGE } from '../../i18n';
export default {
components: {
GlIcon,
+ GlFormCheckbox,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -32,32 +33,23 @@ export default {
tooltipTitle() {
return this.isDisabled ? this.$options.i18n.tooltipTitle : null;
},
- tooltipFocusable() {
- return this.isDisabled ? '0' : null;
- },
},
};
</script>
<template>
- <div class="inline">
- <label
+ <div class="gl-display-flex gl-align-items-center">
+ <gl-form-checkbox
v-gl-tooltip
- :class="{ 'gl-text-gray-400': isDisabled }"
- :tabindex="tooltipFocusable"
- data-testid="squashLabel"
+ :checked="value"
+ :disabled="isDisabled"
+ name="squash"
+ class="qa-squash-checkbox js-squash-checkbox gl-mb-0 gl-mr-2"
:title="tooltipTitle"
+ @change="checked => $emit('input', checked)"
>
- <input
- :checked="value"
- :disabled="isDisabled"
- type="checkbox"
- name="squash"
- class="qa-squash-checkbox js-squash-checkbox"
- @change="$emit('input', $event.target.checked)"
- />
{{ $options.i18n.checkboxLabel }}
- </label>
+ </gl-form-checkbox>
<a
v-if="helpPath"
v-gl-tooltip
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 190d790f584..433dcf2e219 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -16,7 +16,6 @@ import WidgetHeader from './components/mr_widget_header.vue';
import WidgetSuggestPipeline from './components/mr_widget_suggest_pipeline.vue';
import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue';
-import Deployment from './components/deployment/deployment.vue';
import WidgetRelatedLinks from './components/mr_widget_related_links.vue';
import MrWidgetAlertMessage from './components/mr_widget_alert_message.vue';
import MergedState from './components/states/mr_widget_merged.vue';
@@ -63,7 +62,6 @@ export default {
'mr-widget-suggest-pipeline': WidgetSuggestPipeline,
'mr-widget-merge-help': WidgetMergeHelp,
MrWidgetPipelineContainer,
- Deployment,
'mr-widget-related-links': WidgetRelatedLinks,
MrWidgetAlertMessage,
'mr-widget-merged': MergedState,
@@ -155,10 +153,7 @@ export default {
},
shouldSuggestPipelines() {
return (
- gon.features?.suggestPipeline &&
- !this.mr.hasCI &&
- this.mr.mergeRequestAddCiConfigPath &&
- !this.mr.isDismissedSuggestPipeline
+ !this.mr.hasCI && this.mr.mergeRequestAddCiConfigPath && !this.mr.isDismissedSuggestPipeline
);
},
shouldRenderCodeQuality() {
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 8b235b20ad4..f50b6caf0f5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -220,6 +220,7 @@ export default class MergeRequestStore {
this.sourceProjectFullPath = data.source_project_full_path;
this.mergeRequestPipelinesHelpPath = data.merge_request_pipelines_docs_path;
this.conflictsDocsPath = data.conflicts_docs_path;
+ this.reviewingDocsPath = data.reviewing_and_managing_merge_requests_docs_path;
this.ciEnvironmentsStatusPath = data.ci_environments_status_path;
this.securityApprovalsHelpPagePath = data.security_approvals_help_page_path;
this.eligibleApproversDocsPath = data.eligible_approvers_docs_path;
@@ -229,6 +230,7 @@ export default class MergeRequestStore {
this.pipelinesEmptySvgPath = data.pipelines_empty_svg_path;
this.humanAccess = data.human_access;
this.newPipelinePath = data.new_project_pipeline_path;
+ this.sourceProjectDefaultUrl = data.source_project_default_url;
this.userCalloutsPath = data.user_callouts_path;
this.suggestPipelineFeatureId = data.suggest_pipeline_feature_id;
this.isDismissedSuggestPipeline = data.is_dismissed_suggest_pipeline;
@@ -240,6 +242,10 @@ export default class MergeRequestStore {
this.baseBlobPath = blobPath.base_path || '';
this.codequalityHelpPath = data.codequality_help_path;
this.codeclimate = data.codeclimate;
+
+ // Security reports
+ this.sastComparisonPath = data.sast_comparison_path;
+ this.secretScanningComparisonPath = data.secret_scanning_comparison_path;
}
get isNothingToMergeState() {
diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue
index 7a687ea4ad0..6d5912df96b 100644
--- a/app/assets/javascripts/vue_shared/components/awards_list.vue
+++ b/app/assets/javascripts/vue_shared/components/awards_list.vue
@@ -1,7 +1,7 @@
<script>
/* eslint-disable vue/no-v-html */
import { groupBy } from 'lodash';
-import { GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { glEmojiTag } from '../../emoji';
import { __, sprintf } from '~/locale';
@@ -10,8 +10,8 @@ const NO_USER_ID = -1;
export default {
components: {
+ GlButton,
GlIcon,
- GlLoadingIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -64,7 +64,7 @@ export default {
methods: {
getAwardClassBindings(awardList) {
return {
- active: this.hasReactionByCurrentUser(awardList),
+ selected: this.hasReactionByCurrentUser(awardList),
disabled: this.currentUserId === NO_USER_ID,
};
},
@@ -150,40 +150,39 @@ export default {
<template>
<div class="awards js-awards-block">
- <button
+ <gl-button
v-for="awardList in groupedAwards"
:key="awardList.name"
v-gl-tooltip.viewport
+ class="gl-mr-3"
:class="awardList.classes"
:title="awardList.title"
data-testid="award-button"
- class="btn award-control"
- type="button"
@click="handleAward(awardList.name)"
>
- <span data-testid="award-html" v-html="awardList.html"></span>
- <span class="award-control-text js-counter">{{ awardList.list.length }}</span>
- </button>
+ <template #emoji>
+ <span class="award-emoji-block" data-testid="award-html" v-html="awardList.html"></span>
+ </template>
+ <span class="js-counter">{{ awardList.list.length }}</span>
+ </gl-button>
<div v-if="canAwardEmoji" class="award-menu-holder">
- <button
+ <gl-button
v-gl-tooltip.viewport
:class="addButtonClass"
- class="award-control btn js-add-award"
+ class="add-reaction-button js-add-award"
title="Add reaction"
:aria-label="__('Add reaction')"
- type="button"
>
- <span class="award-control-icon award-control-icon-neutral">
+ <span class="reaction-control-icon reaction-control-icon-neutral">
<gl-icon aria-hidden="true" name="slight-smile" />
</span>
- <span class="award-control-icon award-control-icon-positive">
+ <span class="reaction-control-icon reaction-control-icon-positive">
<gl-icon aria-hidden="true" name="smiley" />
</span>
- <span class="award-control-icon award-control-icon-super-positive">
- <gl-icon aria-hidden="true" name="smiley" />
+ <span class="reaction-control-icon reaction-control-icon-super-positive">
+ <gl-icon aria-hidden="true" name="smile" />
</span>
- <gl-loading-icon size="md" color="dark" class="award-control-icon-loading" />
- </button>
+ </gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js b/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js
index d4c1808eec2..106dd7a3b97 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js
@@ -1,3 +1 @@
export const HIGHLIGHT_CLASS_NAME = 'hll';
-
-export default {};
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
index 328c7e3fd32..55526dcc381 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
@@ -77,7 +77,7 @@ export default {
</script>
<template>
- <div>
+ <div data-testid="image-viewer">
<div :class="innerCssClasses" class="position-relative">
<img ref="contentImg" :src="path" @load="onImgLoad" /> <slot name="image-overlay"></slot>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js
index 40708453d79..aaadc9766db 100644
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js
+++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js
@@ -89,5 +89,3 @@ export const inputStringToIsoDate = (value, utc = false) => {
*/
export const isoDateToInputString = (date, utc = false) =>
dateformat(date, dateFormats.inputFormat, utc);
-
-export default {};
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index b4115b0c6a4..4d07d9fcfdd 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -143,6 +143,7 @@ export default {
:style="levelIndentation"
class="file-row-name"
data-qa-selector="file_name_content"
+ data-testid="file-row-name-container"
:class="[fileClasses, { 'str-truncated': !truncateMiddle, 'gl-min-w-0': truncateMiddle }]"
>
<file-icon
diff --git a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue
new file mode 100644
index 00000000000..c45666e69eb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue
@@ -0,0 +1,90 @@
+<script>
+import Tribute from 'tributejs';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import { GfmAutocompleteType, tributeConfig } from '~/vue_shared/components/gfm_autocomplete/utils';
+
+export default {
+ errorMessage: __(
+ 'An error occurred while getting autocomplete data. Please refresh the page and try again.',
+ ),
+ props: {
+ autocompleteTypes: {
+ type: Array,
+ required: false,
+ default: () => Object.values(GfmAutocompleteType),
+ },
+ dataSources: {
+ type: Object,
+ required: false,
+ default: () => gl.GfmAutoComplete?.dataSources || {},
+ },
+ },
+ computed: {
+ config() {
+ return this.autocompleteTypes.map(type => ({
+ ...tributeConfig[type].config,
+ values: this.getValues(type),
+ }));
+ },
+ },
+ mounted() {
+ this.cache = {};
+ this.tribute = new Tribute({ collection: this.config });
+
+ const input = this.$slots.default?.[0]?.elm;
+ this.tribute.attach(input);
+ },
+ beforeDestroy() {
+ const input = this.$slots.default?.[0]?.elm;
+ this.tribute.detach(input);
+ },
+ methods: {
+ cacheAssignees() {
+ const isAssigneesLengthSame =
+ this.assignees?.length === SidebarMediator.singleton?.store?.assignees?.length;
+
+ if (!this.assignees || !isAssigneesLengthSame) {
+ this.assignees =
+ SidebarMediator.singleton?.store?.assignees?.map(assignee => assignee.username) || [];
+ }
+ },
+ filterValues(type) {
+ // The assignees AJAX response can come after the user first invokes autocomplete
+ // so we need to check more than once if we need to update the assignee cache
+ this.cacheAssignees();
+
+ return tributeConfig[type].filterValues
+ ? tributeConfig[type].filterValues({
+ assignees: this.assignees,
+ collection: this.cache[type],
+ fullText: this.$slots.default?.[0]?.elm?.value,
+ selectionStart: this.$slots.default?.[0]?.elm?.selectionStart,
+ })
+ : this.cache[type];
+ },
+ getValues(type) {
+ return (inputText, processValues) => {
+ if (this.cache[type]) {
+ processValues(this.filterValues(type));
+ } else if (this.dataSources[type]) {
+ axios
+ .get(this.dataSources[type])
+ .then(response => {
+ this.cache[type] = response.data;
+ processValues(this.filterValues(type));
+ })
+ .catch(() => createFlash({ message: this.$options.errorMessage }));
+ } else {
+ processValues([]);
+ }
+ };
+ },
+ },
+ render(createElement) {
+ return createElement('div', this.$slots.default);
+ },
+};
+</script>
diff --git a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
new file mode 100644
index 00000000000..b2e995d0f17
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
@@ -0,0 +1,142 @@
+import { escape, last } from 'lodash';
+import { spriteIcon } from '~/lib/utils/common_utils';
+
+const groupType = 'Group'; // eslint-disable-line @gitlab/require-i18n-strings
+
+const nonWordOrInteger = /\W|^\d+$/;
+
+export const GfmAutocompleteType = {
+ Issues: 'issues',
+ Labels: 'labels',
+ Members: 'members',
+ MergeRequests: 'mergeRequests',
+ Milestones: 'milestones',
+ Snippets: 'snippets',
+};
+
+function doesCurrentLineStartWith(searchString, fullText, selectionStart) {
+ const currentLineNumber = fullText.slice(0, selectionStart).split('\n').length;
+ const currentLine = fullText.split('\n')[currentLineNumber - 1];
+ return currentLine.startsWith(searchString);
+}
+
+export const tributeConfig = {
+ [GfmAutocompleteType.Issues]: {
+ config: {
+ trigger: '#',
+ lookup: value => value.iid + value.title,
+ menuItemTemplate: ({ original }) =>
+ `<small>${original.reference || original.iid}</small> ${escape(original.title)}`,
+ selectTemplate: ({ original }) => original.reference || `#${original.iid}`,
+ },
+ },
+
+ [GfmAutocompleteType.Labels]: {
+ config: {
+ trigger: '~',
+ lookup: 'title',
+ menuItemTemplate: ({ original }) => `
+ <span class="dropdown-label-box" style="background: ${escape(original.color)};"></span>
+ ${escape(original.title)}`,
+ selectTemplate: ({ original }) =>
+ nonWordOrInteger.test(original.title)
+ ? `~"${escape(original.title)}"`
+ : `~${escape(original.title)}`,
+ },
+ filterValues({ collection, fullText, selectionStart }) {
+ if (doesCurrentLineStartWith('/label', fullText, selectionStart)) {
+ return collection.filter(label => !label.set);
+ }
+
+ if (doesCurrentLineStartWith('/unlabel', fullText, selectionStart)) {
+ return collection.filter(label => label.set);
+ }
+
+ return collection;
+ },
+ },
+
+ [GfmAutocompleteType.Members]: {
+ config: {
+ trigger: '@',
+ fillAttr: 'username',
+ lookup: value =>
+ value.type === groupType ? last(value.name.split(' / ')) : value.name + value.username,
+ menuItemTemplate: ({ original }) => {
+ const commonClasses = 'gl-avatar gl-avatar-s24 gl-flex-shrink-0';
+ const noAvatarClasses = `${commonClasses} gl-rounded-small
+ gl-display-flex gl-align-items-center gl-justify-content-center`;
+
+ const avatar = original.avatar_url
+ ? `<img class="${commonClasses} gl-avatar-circle" src="${original.avatar_url}" alt="" />`
+ : `<div class="${noAvatarClasses}" aria-hidden="true">
+ ${original.username.charAt(0).toUpperCase()}</div>`;
+
+ let displayName = original.name;
+ let parentGroupOrUsername = `@${original.username}`;
+
+ if (original.type === groupType) {
+ const splitName = original.name.split(' / ');
+ displayName = splitName.pop();
+ parentGroupOrUsername = splitName.pop();
+ }
+
+ const count = original.count && !original.mentionsDisabled ? ` (${original.count})` : '';
+
+ const disabledMentionsIcon = original.mentionsDisabled
+ ? spriteIcon('notifications-off', 's16 gl-ml-3')
+ : '';
+
+ return `
+ <div class="gl-display-flex gl-align-items-center">
+ ${avatar}
+ <div class="gl-font-sm gl-line-height-normal gl-ml-3">
+ <div>${escape(displayName)}${count}</div>
+ <div class="gl-text-gray-700">${escape(parentGroupOrUsername)}</div>
+ </div>
+ ${disabledMentionsIcon}
+ </div>
+ `;
+ },
+ },
+ filterValues({ assignees, collection, fullText, selectionStart }) {
+ if (doesCurrentLineStartWith('/assign', fullText, selectionStart)) {
+ return collection.filter(member => !assignees.includes(member.username));
+ }
+
+ if (doesCurrentLineStartWith('/unassign', fullText, selectionStart)) {
+ return collection.filter(member => assignees.includes(member.username));
+ }
+
+ return collection;
+ },
+ },
+
+ [GfmAutocompleteType.MergeRequests]: {
+ config: {
+ trigger: '!',
+ lookup: value => value.iid + value.title,
+ menuItemTemplate: ({ original }) =>
+ `<small>${original.reference || original.iid}</small> ${escape(original.title)}`,
+ selectTemplate: ({ original }) => original.reference || `!${original.iid}`,
+ },
+ },
+
+ [GfmAutocompleteType.Milestones]: {
+ config: {
+ trigger: '%',
+ lookup: 'title',
+ menuItemTemplate: ({ original }) => escape(original.title),
+ selectTemplate: ({ original }) => `%"${escape(original.title)}"`,
+ },
+ },
+
+ [GfmAutocompleteType.Snippets]: {
+ config: {
+ trigger: '$',
+ fillAttr: 'id',
+ lookup: value => value.id + value.title,
+ menuItemTemplate: ({ original }) => `<small>${original.id}</small> ${escape(original.title)}`,
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/components/gl_mentions.vue b/app/assets/javascripts/vue_shared/components/gl_mentions.vue
deleted file mode 100644
index dde7e3ebe13..00000000000
--- a/app/assets/javascripts/vue_shared/components/gl_mentions.vue
+++ /dev/null
@@ -1,238 +0,0 @@
-<script>
-import { escape, last } from 'lodash';
-import Tribute from 'tributejs';
-import axios from '~/lib/utils/axios_utils';
-import { spriteIcon } from '~/lib/utils/common_utils';
-import SidebarMediator from '~/sidebar/sidebar_mediator';
-
-const AutoComplete = {
- Issues: 'issues',
- Labels: 'labels',
- Members: 'members',
- MergeRequests: 'mergeRequests',
- Milestones: 'milestones',
- Snippets: 'snippets',
-};
-
-const groupType = 'Group'; // eslint-disable-line @gitlab/require-i18n-strings
-
-function doesCurrentLineStartWith(searchString, fullText, selectionStart) {
- const currentLineNumber = fullText.slice(0, selectionStart).split('\n').length;
- const currentLine = fullText.split('\n')[currentLineNumber - 1];
- return currentLine.startsWith(searchString);
-}
-
-const autoCompleteMap = {
- [AutoComplete.Issues]: {
- filterValues() {
- return this[AutoComplete.Issues];
- },
- menuItemTemplate({ original }) {
- return `<small>${original.reference || original.iid}</small> ${escape(original.title)}`;
- },
- },
- [AutoComplete.Labels]: {
- filterValues() {
- const fullText = this.$slots.default?.[0]?.elm?.value;
- const selectionStart = this.$slots.default?.[0]?.elm?.selectionStart;
-
- if (doesCurrentLineStartWith('/label', fullText, selectionStart)) {
- return this.labels.filter(label => !label.set);
- }
-
- if (doesCurrentLineStartWith('/unlabel', fullText, selectionStart)) {
- return this.labels.filter(label => label.set);
- }
-
- return this.labels;
- },
- menuItemTemplate({ original }) {
- return `
- <span class="dropdown-label-box" style="background: ${escape(original.color)};"></span>
- ${escape(original.title)}`;
- },
- },
- [AutoComplete.Members]: {
- filterValues() {
- const fullText = this.$slots.default?.[0]?.elm?.value;
- const selectionStart = this.$slots.default?.[0]?.elm?.selectionStart;
-
- // Need to check whether sidebar store assignees has been updated
- // in the case where the assignees AJAX response comes after the user does @ autocomplete
- const isAssigneesLengthSame =
- this.assignees?.length === SidebarMediator.singleton?.store?.assignees?.length;
-
- if (!this.assignees || !isAssigneesLengthSame) {
- this.assignees =
- SidebarMediator.singleton?.store?.assignees?.map(assignee => assignee.username) || [];
- }
-
- if (doesCurrentLineStartWith('/assign', fullText, selectionStart)) {
- return this.members.filter(member => !this.assignees.includes(member.username));
- }
-
- if (doesCurrentLineStartWith('/unassign', fullText, selectionStart)) {
- return this.members.filter(member => this.assignees.includes(member.username));
- }
-
- return this.members;
- },
- menuItemTemplate({ original }) {
- const commonClasses = 'gl-avatar gl-avatar-s24 gl-flex-shrink-0';
- const noAvatarClasses = `${commonClasses} gl-rounded-small
- gl-display-flex gl-align-items-center gl-justify-content-center`;
-
- const avatar = original.avatar_url
- ? `<img class="${commonClasses} gl-avatar-circle" src="${original.avatar_url}" alt="" />`
- : `<div class="${noAvatarClasses}" aria-hidden="true">
- ${original.username.charAt(0).toUpperCase()}</div>`;
-
- let displayName = original.name;
- let parentGroupOrUsername = `@${original.username}`;
-
- if (original.type === groupType) {
- const splitName = original.name.split(' / ');
- displayName = splitName.pop();
- parentGroupOrUsername = splitName.pop();
- }
-
- const count = original.count && !original.mentionsDisabled ? ` (${original.count})` : '';
-
- const disabledMentionsIcon = original.mentionsDisabled
- ? spriteIcon('notifications-off', 's16 gl-ml-3')
- : '';
-
- return `
- <div class="gl-display-flex gl-align-items-center">
- ${avatar}
- <div class="gl-font-sm gl-line-height-normal gl-ml-3">
- <div>${escape(displayName)}${count}</div>
- <div class="gl-text-gray-700">${escape(parentGroupOrUsername)}</div>
- </div>
- ${disabledMentionsIcon}
- </div>
- `;
- },
- },
- [AutoComplete.MergeRequests]: {
- filterValues() {
- return this[AutoComplete.MergeRequests];
- },
- menuItemTemplate({ original }) {
- return `<small>${original.reference || original.iid}</small> ${escape(original.title)}`;
- },
- },
- [AutoComplete.Milestones]: {
- filterValues() {
- return this[AutoComplete.Milestones];
- },
- menuItemTemplate({ original }) {
- return escape(original.title);
- },
- },
- [AutoComplete.Snippets]: {
- filterValues() {
- return this[AutoComplete.Snippets];
- },
- menuItemTemplate({ original }) {
- return `<small>${original.id}</small> ${escape(original.title)}`;
- },
- },
-};
-
-export default {
- name: 'GlMentions',
- props: {
- dataSources: {
- type: Object,
- required: false,
- default: () => gl.GfmAutoComplete?.dataSources || {},
- },
- },
- mounted() {
- const NON_WORD_OR_INTEGER = /\W|^\d+$/;
-
- this.tribute = new Tribute({
- collection: [
- {
- trigger: '#',
- lookup: value => value.iid + value.title,
- menuItemTemplate: autoCompleteMap[AutoComplete.Issues].menuItemTemplate,
- selectTemplate: ({ original }) => original.reference || `#${original.iid}`,
- values: this.getValues(AutoComplete.Issues),
- },
- {
- trigger: '@',
- fillAttr: 'username',
- lookup: value =>
- value.type === groupType ? last(value.name.split(' / ')) : value.name + value.username,
- menuItemTemplate: autoCompleteMap[AutoComplete.Members].menuItemTemplate,
- values: this.getValues(AutoComplete.Members),
- },
- {
- trigger: '~',
- lookup: 'title',
- menuItemTemplate: autoCompleteMap[AutoComplete.Labels].menuItemTemplate,
- selectTemplate: ({ original }) =>
- NON_WORD_OR_INTEGER.test(original.title)
- ? `~"${escape(original.title)}"`
- : `~${escape(original.title)}`,
- values: this.getValues(AutoComplete.Labels),
- },
- {
- trigger: '!',
- lookup: value => value.iid + value.title,
- menuItemTemplate: autoCompleteMap[AutoComplete.MergeRequests].menuItemTemplate,
- selectTemplate: ({ original }) => original.reference || `!${original.iid}`,
- values: this.getValues(AutoComplete.MergeRequests),
- },
- {
- trigger: '%',
- lookup: 'title',
- menuItemTemplate: autoCompleteMap[AutoComplete.Milestones].menuItemTemplate,
- selectTemplate: ({ original }) => `%"${escape(original.title)}"`,
- values: this.getValues(AutoComplete.Milestones),
- },
- {
- trigger: '$',
- fillAttr: 'id',
- lookup: value => value.id + value.title,
- menuItemTemplate: autoCompleteMap[AutoComplete.Snippets].menuItemTemplate,
- values: this.getValues(AutoComplete.Snippets),
- },
- ],
- });
-
- const input = this.$slots.default?.[0]?.elm;
- this.tribute.attach(input);
- },
- beforeDestroy() {
- const input = this.$slots.default?.[0]?.elm;
- this.tribute.detach(input);
- },
- methods: {
- getValues(autoCompleteType) {
- return (inputText, processValues) => {
- if (this[autoCompleteType]) {
- const filteredValues = autoCompleteMap[autoCompleteType].filterValues.call(this);
- processValues(filteredValues);
- } else if (this.dataSources[autoCompleteType]) {
- axios
- .get(this.dataSources[autoCompleteType])
- .then(response => {
- this[autoCompleteType] = response.data;
- const filteredValues = autoCompleteMap[autoCompleteType].filterValues.call(this);
- processValues(filteredValues);
- })
- .catch(() => {});
- } else {
- processValues([]);
- }
- };
- },
- },
- render(createElement) {
- return createElement('div', this.$slots.default);
- },
-};
-</script>
diff --git a/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js b/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js
index 02f28da8bb0..61ab2a698ce 100644
--- a/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js
@@ -1,5 +1,3 @@
export function pixeliseValue(val) {
return val ? `${val}px` : '';
}
-
-export default {};
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 9cfba85e0d8..0d703545073 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -10,14 +10,14 @@ import { deprecatedCreateFlash as Flash } from '~/flash';
import GLForm from '~/gl_form';
import MarkdownHeader from './header.vue';
import MarkdownToolbar from './toolbar.vue';
-import GlMentions from '~/vue_shared/components/gl_mentions.vue';
+import GfmAutocomplete from '~/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import axios from '~/lib/utils/axios_utils';
export default {
components: {
- GlMentions,
+ GfmAutocomplete,
MarkdownHeader,
MarkdownToolbar,
GlIcon,
@@ -246,9 +246,9 @@ export default {
/>
<div v-show="!previewMarkdown" class="md-write-holder">
<div class="zen-backdrop">
- <gl-mentions v-if="glFeatures.tributeAutocomplete">
+ <gfm-autocomplete v-if="glFeatures.tributeAutocomplete">
<slot name="textarea"></slot>
- </gl-mentions>
+ </gfm-autocomplete>
<slot v-else name="textarea"></slot>
<a
class="zen-control zen-control-leave js-zen-leave gl-text-gray-500"
diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue
index 8104d919bf6..85481f3f7b4 100644
--- a/app/assets/javascripts/vue_shared/components/pikaday.vue
+++ b/app/assets/javascripts/vue_shared/components/pikaday.vue
@@ -1,10 +1,14 @@
<script>
import Pikaday from 'pikaday';
+import { GlIcon } from '@gitlab/ui';
import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
export default {
name: 'DatePicker',
+ components: {
+ GlIcon,
+ },
props: {
label: {
type: String,
@@ -66,7 +70,7 @@ export default {
<div class="dropdown open">
<button type="button" class="dropdown-menu-toggle" data-toggle="dropdown" @click="toggled">
<span class="dropdown-toggle-text"> {{ label }} </span>
- <i class="fa fa-chevron-down" aria-hidden="true"> </i>
+ <gl-icon name="chevron-down" class="gl-absolute gl-right-3 gl-top-3 gl-text-gray-500" />
</button>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/select2_select.vue b/app/assets/javascripts/vue_shared/components/select2_select.vue
index c90bd4da6c2..3dbf0ccdfa9 100644
--- a/app/assets/javascripts/vue_shared/components/select2_select.vue
+++ b/app/assets/javascripts/vue_shared/components/select2_select.vue
@@ -1,6 +1,7 @@
<script>
import $ from 'jquery';
import 'select2';
+import { loadCSSFile } from '~/lib/utils/css_utils';
export default {
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
@@ -20,10 +21,14 @@ export default {
},
mounted() {
- $(this.$refs.dropdownInput)
- .val(this.value)
- .select2(this.options)
- .on('change', event => this.$emit('input', event.target.value));
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ $(this.$refs.dropdownInput)
+ .val(this.value)
+ .select2(this.options)
+ .on('change', event => this.$emit('input', event.target.value));
+ })
+ .catch(() => {});
},
beforeDestroy() {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
index 2f71907f772..8ce624aa303 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
@@ -105,6 +105,11 @@ export default {
required: false,
default: __('Manage group labels'),
},
+ isEditing: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -131,6 +136,11 @@ export default {
showDropdownContents(showDropdownContents) {
this.setContentIsOnViewport(showDropdownContents);
},
+ isEditing(newVal) {
+ if (newVal) {
+ this.toggleDropdownContents();
+ }
+ },
},
mounted() {
this.setInitialState({
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
index 579ad53e6db..b48dfa8b452 100644
--- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
+++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
@@ -1,6 +1,7 @@
<script>
import { isFunction } from 'lodash';
import tooltip from '../directives/tooltip';
+import { hasHorizontalOverflow } from '~/lib/utils/dom_utils';
export default {
directives: {
@@ -49,7 +50,7 @@ export default {
},
updateTooltip() {
const target = this.selectTarget();
- this.showTooltip = Boolean(target && target.scrollWidth > target.offsetWidth);
+ this.showTooltip = hasHorizontalOverflow(target);
},
},
};
diff --git a/app/assets/javascripts/vue_shared/security_reports/components/constants.js b/app/assets/javascripts/vue_shared/security_reports/components/constants.js
new file mode 100644
index 00000000000..9b1cbfe218b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/components/constants.js
@@ -0,0 +1,8 @@
+export const SEVERITY_CLASS_NAME_MAP = {
+ critical: 'text-danger-800',
+ high: 'text-danger-600',
+ medium: 'text-warning-400',
+ low: 'text-warning-200',
+ info: 'text-primary-400',
+ unknown: 'text-secondary-400',
+};
diff --git a/app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue b/app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue
new file mode 100644
index 00000000000..babb9fddcf6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue
@@ -0,0 +1,59 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import { SEVERITY_CLASS_NAME_MAP } from './constants';
+
+export default {
+ components: {
+ GlSprintf,
+ },
+ props: {
+ message: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ shouldShowCountMessage() {
+ return !this.message.status && Boolean(this.message.countMessage);
+ },
+ },
+ methods: {
+ getSeverityClass(severity) {
+ return SEVERITY_CLASS_NAME_MAP[severity];
+ },
+ },
+ slotNames: ['critical', 'high', 'other'],
+ spacingClasses: {
+ critical: 'gl-pl-4',
+ high: 'gl-px-2',
+ other: 'gl-px-2',
+ },
+};
+</script>
+
+<template>
+ <span>
+ <gl-sprintf :message="message.message">
+ <template #total="{content}">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ <span v-if="shouldShowCountMessage" class="gl-font-sm">
+ <gl-sprintf :message="message.countMessage">
+ <template v-for="slotName in $options.slotNames" #[slotName]="{content}">
+ <span :key="slotName">
+ <strong
+ v-if="message[slotName] > 0"
+ :class="[getSeverityClass(slotName), $options.spacingClasses[slotName]]"
+ >
+ {{ content }}
+ </strong>
+ <span v-else :class="$options.spacingClasses[slotName]">
+ {{ content }}
+ </span>
+ </span>
+ </template>
+ </gl-sprintf>
+ </span>
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js
index 2f87c4e7878..413b4a70b40 100644
--- a/app/assets/javascripts/vue_shared/security_reports/constants.js
+++ b/app/assets/javascripts/vue_shared/security_reports/constants.js
@@ -1,3 +1,9 @@
export const FEEDBACK_TYPE_DISMISSAL = 'dismissal';
export const FEEDBACK_TYPE_ISSUE = 'issue';
export const FEEDBACK_TYPE_MERGE_REQUEST = 'merge_request';
+
+/**
+ * Security scan report types, as provided by the backend.
+ */
+export const REPORT_TYPE_SAST = 'sast';
+export const REPORT_TYPE_SECRET_DETECTION = 'secret_detection';
diff --git a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
index 89253cc7116..b61783ed7b0 100644
--- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
@@ -1,19 +1,28 @@
<script>
+import { mapActions, mapGetters } from 'vuex';
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReportSection from '~/reports/components/report_section.vue';
-import { status } from '~/reports/constants';
+import { LOADING, ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants';
import { s__ } from '~/locale';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
-import Flash from '~/flash';
+import createFlash from '~/flash';
import Api from '~/api';
+import SecuritySummary from './components/security_summary.vue';
+import store from './store';
+import { MODULE_SAST, MODULE_SECRET_DETECTION } from './store/constants';
+import { REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION } from './constants';
export default {
+ store,
components: {
GlIcon,
GlLink,
GlSprintf,
ReportSection,
+ SecuritySummary,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
pipelineId: {
type: Number,
@@ -27,25 +36,53 @@ export default {
type: String,
required: true,
},
+ sastComparisonPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ secretScanningComparisonPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
- hasSecurityReports: false,
+ availableSecurityReports: [],
+ canShowCounts: false,
- // Error state is shown even when successfully loaded, since success
+ // When core_security_mr_widget_counts is not enabled, the
+ // error state is shown even when successfully loaded, since success
// state suggests that the security scans detected no security problems,
// which is not necessarily the case. A future iteration will actually
// check whether problems were found and display the appropriate status.
- status: status.ERROR,
+ status: ERROR,
};
},
+ computed: {
+ ...mapGetters(['groupedSummaryText', 'summaryStatus']),
+ hasSecurityReports() {
+ return this.availableSecurityReports.length > 0;
+ },
+ hasSastReports() {
+ return this.availableSecurityReports.includes(REPORT_TYPE_SAST);
+ },
+ hasSecretDetectionReports() {
+ return this.availableSecurityReports.includes(REPORT_TYPE_SECRET_DETECTION);
+ },
+ isLoaded() {
+ return this.summaryStatus !== LOADING;
+ },
+ },
created() {
- this.checkHasSecurityReports(this.$options.reportTypes)
- .then(hasSecurityReports => {
- this.hasSecurityReports = hasSecurityReports;
+ this.checkAvailableSecurityReports(this.$options.reportTypes)
+ .then(availableSecurityReports => {
+ this.availableSecurityReports = Array.from(availableSecurityReports);
+ this.fetchCounts();
})
.catch(error => {
- Flash({
+ createFlash({
message: this.$options.i18n.apiError,
captureError: true,
error,
@@ -53,7 +90,18 @@ export default {
});
},
methods: {
- async checkHasSecurityReports(reportTypes) {
+ ...mapActions(MODULE_SAST, {
+ setSastDiffEndpoint: 'setDiffEndpoint',
+ fetchSastDiff: 'fetchDiff',
+ }),
+ ...mapActions(MODULE_SECRET_DETECTION, {
+ setSecretDetectionDiffEndpoint: 'setDiffEndpoint',
+ fetchSecretDetectionDiff: 'fetchDiff',
+ }),
+ async checkAvailableSecurityReports(reportTypes) {
+ const reportTypesSet = new Set(reportTypes);
+ const availableReportTypes = new Set();
+
let page = 1;
while (page) {
// eslint-disable-next-line no-await-in-loop
@@ -62,18 +110,40 @@ export default {
page,
});
- const hasSecurityReports = jobs.some(({ artifacts = [] }) =>
- artifacts.some(({ file_type }) => reportTypes.includes(file_type)),
- );
+ jobs.forEach(({ artifacts = [] }) => {
+ artifacts.forEach(({ file_type }) => {
+ if (reportTypesSet.has(file_type)) {
+ availableReportTypes.add(file_type);
+ }
+ });
+ });
- if (hasSecurityReports) {
- return true;
+ // If we've found artifacts for all the report types, stop looking!
+ if (availableReportTypes.size === reportTypesSet.size) {
+ return availableReportTypes;
}
page = parseIntPagination(normalizeHeaders(headers)).nextPage;
}
- return false;
+ return availableReportTypes;
+ },
+ fetchCounts() {
+ if (!this.glFeatures.coreSecurityMrWidgetCounts) {
+ return;
+ }
+
+ if (this.sastComparisonPath && this.hasSastReports) {
+ this.setSastDiffEndpoint(this.sastComparisonPath);
+ this.fetchSastDiff();
+ this.canShowCounts = true;
+ }
+
+ if (this.secretScanningComparisonPath && this.hasSecretDetectionReports) {
+ this.setSecretDetectionDiffEndpoint(this.secretScanningComparisonPath);
+ this.fetchSecretDetectionDiff();
+ this.canShowCounts = true;
+ }
},
activatePipelinesTab() {
if (window.mrTabs) {
@@ -81,7 +151,7 @@ export default {
}
},
},
- reportTypes: ['sast', 'secret_detection'],
+ reportTypes: [REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION],
i18n: {
apiError: s__(
'SecurityReports|Failed to get security report information. Please reload the page or try again later.',
@@ -89,13 +159,57 @@ export default {
scansHaveRun: s__(
'SecurityReports|Security scans have run. Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
),
+ downloadFromPipelineTab: s__(
+ 'SecurityReports|Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
+ ),
securityReportsHelp: s__('SecurityReports|Security reports help page link'),
},
+ summarySlots: [SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR],
};
</script>
<template>
<report-section
- v-if="hasSecurityReports"
+ v-if="canShowCounts"
+ :status="summaryStatus"
+ :has-issues="false"
+ class="mr-widget-border-top mr-report"
+ data-testid="security-mr-widget"
+ >
+ <template v-for="slot in $options.summarySlots" #[slot]>
+ <span :key="slot">
+ <security-summary :message="groupedSummaryText" />
+
+ <gl-link
+ target="_blank"
+ data-testid="help"
+ :href="securityReportsDocsPath"
+ :aria-label="$options.i18n.securityReportsHelp"
+ >
+ <gl-icon name="question" />
+ </gl-link>
+ </span>
+ </template>
+
+ <template v-if="isLoaded" #sub-heading>
+ <span class="gl-font-sm">
+ <gl-sprintf :message="$options.i18n.downloadFromPipelineTab">
+ <template #link="{ content }">
+ <gl-link
+ class="gl-font-sm"
+ data-testid="show-pipelines"
+ @click="activatePipelinesTab"
+ >{{ content }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </span>
+ </template>
+ </report-section>
+
+ <!-- TODO: Remove this section when removing core_security_mr_widget_counts
+ feature flag. See https://gitlab.com/gitlab-org/gitlab/-/issues/284097 -->
+ <report-section
+ v-else-if="hasSecurityReports"
:status="status"
:has-issues="false"
class="mr-widget-border-top mr-report"
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/constants.js b/app/assets/javascripts/vue_shared/security_reports/store/constants.js
new file mode 100644
index 00000000000..6aeab56eea2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/store/constants.js
@@ -0,0 +1,7 @@
+/**
+ * Vuex module names corresponding to security scan types. These are similar to
+ * the snake_case report types from the backend, but should not be considered
+ * to be equivalent.
+ */
+export const MODULE_SAST = 'sast';
+export const MODULE_SECRET_DETECTION = 'secretDetection';
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/getters.js b/app/assets/javascripts/vue_shared/security_reports/store/getters.js
new file mode 100644
index 00000000000..1e5a60c32fd
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/store/getters.js
@@ -0,0 +1,66 @@
+import { s__, sprintf } from '~/locale';
+import { countVulnerabilities, groupedTextBuilder } from './utils';
+import { LOADING, ERROR, SUCCESS } from '~/reports/constants';
+import { TRANSLATION_IS_LOADING } from './messages';
+
+export const summaryCounts = state =>
+ countVulnerabilities(
+ state.reportTypes.reduce((acc, reportType) => {
+ acc.push(...state[reportType].newIssues);
+ return acc;
+ }, []),
+ );
+
+export const groupedSummaryText = (state, getters) => {
+ const reportType = s__('ciReport|Security scanning');
+ let status = '';
+
+ // All reports are loading
+ if (getters.areAllReportsLoading) {
+ return { message: sprintf(TRANSLATION_IS_LOADING, { reportType }) };
+ }
+
+ // All reports returned error
+ if (getters.allReportsHaveError) {
+ return { message: s__('ciReport|Security scanning failed loading any results') };
+ }
+
+ if (getters.areReportsLoading && getters.anyReportHasError) {
+ status = s__('ciReport|is loading, errors when loading results');
+ } else if (getters.areReportsLoading && !getters.anyReportHasError) {
+ status = s__('ciReport|is loading');
+ } else if (!getters.areReportsLoading && getters.anyReportHasError) {
+ status = s__('ciReport|: Loading resulted in an error');
+ }
+
+ const { critical, high, other } = getters.summaryCounts;
+
+ return groupedTextBuilder({ reportType, status, critical, high, other });
+};
+
+export const summaryStatus = (state, getters) => {
+ if (getters.areReportsLoading) {
+ return LOADING;
+ }
+
+ if (getters.anyReportHasError || getters.anyReportHasIssues) {
+ return ERROR;
+ }
+
+ return SUCCESS;
+};
+
+export const areReportsLoading = state =>
+ state.reportTypes.some(reportType => state[reportType].isLoading);
+
+export const areAllReportsLoading = state =>
+ state.reportTypes.every(reportType => state[reportType].isLoading);
+
+export const allReportsHaveError = state =>
+ state.reportTypes.every(reportType => state[reportType].hasError);
+
+export const anyReportHasError = state =>
+ state.reportTypes.some(reportType => state[reportType].hasError);
+
+export const anyReportHasIssues = state =>
+ state.reportTypes.some(reportType => state[reportType].newIssues.length > 0);
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/index.js b/app/assets/javascripts/vue_shared/security_reports/store/index.js
new file mode 100644
index 00000000000..10705e04a21
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/store/index.js
@@ -0,0 +1,16 @@
+import Vuex from 'vuex';
+import * as getters from './getters';
+import state from './state';
+import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants';
+import sast from './modules/sast';
+import secretDetection from './modules/secret_detection';
+
+export default () =>
+ new Vuex.Store({
+ modules: {
+ [MODULE_SAST]: sast,
+ [MODULE_SECRET_DETECTION]: secretDetection,
+ },
+ getters,
+ state,
+ });
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/messages.js b/app/assets/javascripts/vue_shared/security_reports/store/messages.js
new file mode 100644
index 00000000000..c25e252a768
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/store/messages.js
@@ -0,0 +1,4 @@
+import { s__ } from '~/locale';
+
+export const TRANSLATION_IS_LOADING = s__('ciReport|%{reportType} is loading');
+export const TRANSLATION_HAS_ERROR = s__('ciReport|%{reportType}: Loading resulted in an error');
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/state.js b/app/assets/javascripts/vue_shared/security_reports/store/state.js
new file mode 100644
index 00000000000..5dc4d1ad2fb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/store/state.js
@@ -0,0 +1,5 @@
+import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants';
+
+export default () => ({
+ reportTypes: [MODULE_SAST, MODULE_SECRET_DETECTION],
+});
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/app/assets/javascripts/vue_shared/security_reports/store/utils.js
index 6e50efae741..c5e786c92b1 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/utils.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/utils.js
@@ -1,5 +1,7 @@
import pollUntilComplete from '~/lib/utils/poll_until_complete';
import axios from '~/lib/utils/axios_utils';
+import { __, n__, sprintf } from '~/locale';
+import { CRITICAL, HIGH } from '~/vulnerabilities/constants';
import {
FEEDBACK_TYPE_DISMISSAL,
FEEDBACK_TYPE_ISSUE,
@@ -73,3 +75,79 @@ export const parseDiff = (diff, enrichData) => {
existing: diff.existing ? diff.existing.map(enrichVulnerability) : [],
};
};
+
+const createCountMessage = ({ critical, high, other, total }) => {
+ const otherMessage = n__('%d Other', '%d Others', other);
+ const countMessage = __(
+ '%{criticalStart}%{critical} Critical%{criticalEnd} %{highStart}%{high} High%{highEnd} and %{otherStart}%{otherMessage}%{otherEnd}',
+ );
+ return total ? sprintf(countMessage, { critical, high, otherMessage }) : '';
+};
+
+const createStatusMessage = ({ reportType, status, total }) => {
+ const vulnMessage = n__('vulnerability', 'vulnerabilities', total);
+ let message;
+ if (status) {
+ message = __('%{reportType} %{status}');
+ } else if (!total) {
+ message = __('%{reportType} detected %{totalStart}no%{totalEnd} vulnerabilities.');
+ } else {
+ message = __(
+ '%{reportType} detected %{totalStart}%{total}%{totalEnd} potential %{vulnMessage}',
+ );
+ }
+ return sprintf(message, { reportType, status, total, vulnMessage });
+};
+
+/**
+ * Counts vulnerabilities.
+ * Returns the amount of critical, high, and other vulnerabilities.
+ *
+ * @param {Array} vulnerabilities The raw vulnerabilities to parse
+ * @returns {{critical: number, high: number, other: number}}
+ */
+export const countVulnerabilities = (vulnerabilities = []) =>
+ vulnerabilities.reduce(
+ (acc, { severity }) => {
+ if (severity === CRITICAL) {
+ acc.critical += 1;
+ } else if (severity === HIGH) {
+ acc.high += 1;
+ } else {
+ acc.other += 1;
+ }
+
+ return acc;
+ },
+ { critical: 0, high: 0, other: 0 },
+ );
+
+/**
+ * Takes an object of options and returns the object with an externalized string representing
+ * the critical, high, and other severity vulnerabilities for a given report.
+ *
+ * The resulting string _may_ still contain sprintf-style placeholders. These
+ * are left in place so they can be replaced with markup, via the
+ * SecuritySummary component.
+ * @param {{reportType: string, status: string, critical: number, high: number, other: number}} options
+ * @returns {Object} the parameters with an externalized string
+ */
+export const groupedTextBuilder = ({
+ reportType = '',
+ status = '',
+ critical = 0,
+ high = 0,
+ other = 0,
+} = {}) => {
+ const total = critical + high + other;
+
+ return {
+ countMessage: createCountMessage({ critical, high, other, total }),
+ message: createStatusMessage({ reportType, status, total }),
+ critical,
+ high,
+ other,
+ status,
+ total,
+ };
+};
diff --git a/app/assets/javascripts/vuex_shared/modules/members/index.js b/app/assets/javascripts/vuex_shared/modules/members/index.js
deleted file mode 100644
index 586d52a5288..00000000000
--- a/app/assets/javascripts/vuex_shared/modules/members/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import createState from 'ee_else_ce/vuex_shared/modules/members/state';
-import mutations from 'ee_else_ce/vuex_shared/modules/members/mutations';
-import * as actions from 'ee_else_ce/vuex_shared/modules/members/actions';
-
-export default initialState => ({
- namespaced: true,
- state: createState(initialState),
- actions,
- mutations,
-});
diff --git a/app/assets/javascripts/vulnerabilities/constants.js b/app/assets/javascripts/vulnerabilities/constants.js
new file mode 100644
index 00000000000..42fb38e8e7e
--- /dev/null
+++ b/app/assets/javascripts/vulnerabilities/constants.js
@@ -0,0 +1,15 @@
+/**
+ * Vulnerability severities as provided by the backend on vulnerability
+ * objects.
+ */
+export const CRITICAL = 'critical';
+export const HIGH = 'high';
+export const MEDIUM = 'medium';
+export const LOW = 'low';
+export const INFO = 'info';
+export const UNKNOWN = 'unknown';
+
+/**
+ * All vulnerability severities in decreasing order.
+ */
+export const SEVERITIES = [CRITICAL, HIGH, MEDIUM, LOW, INFO, UNKNOWN];