summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-10-13 12:12:20 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-10-13 12:12:20 +0000
commit429a0c87c1f36ff9ebfe131ebb6c71a83d9f917c (patch)
treeffe9ecb8fc29c6fd9aaeef295bd92d3da3d0a312 /app
parentbc2f7ab125361e4180018b1b933f42a8709df356 (diff)
downloadgitlab-ce-429a0c87c1f36ff9ebfe131ebb6c71a83d9f917c.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue64
-rw-r--r--app/assets/javascripts/boards/index.js3
-rw-r--r--app/assets/javascripts/boards/stores/actions.js3
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js4
-rw-r--r--app/assets/javascripts/boards/stores/state.js1
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue12
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue2
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue2
-rw-r--r--app/assets/javascripts/issuable_suggestions/components/item.vue35
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue81
-rw-r--r--app/assets/javascripts/members/constants.js16
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue15
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue49
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue31
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql19
-rw-r--r--app/assets/javascripts/related_issues/components/add_issuable_form.vue2
-rw-r--r--app/assets/javascripts/search_settings/components/search_settings.vue58
-rw-r--r--app/assets/javascripts/search_settings/constants.js3
-rw-r--r--app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue26
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue21
-rw-r--r--app/assets/stylesheets/pages/issuable.scss12
-rw-r--r--app/controllers/groups/boards_controller.rb1
-rw-r--r--app/controllers/groups/group_members_controller.rb2
-rw-r--r--app/controllers/import/bulk_imports_controller.rb22
-rw-r--r--app/controllers/projects/boards_controller.rb1
-rw-r--r--app/controllers/projects/ci/pipeline_editor_controller.rb1
-rw-r--r--app/controllers/projects/project_members_controller.rb2
-rw-r--r--app/controllers/repositories/git_http_controller.rb7
-rw-r--r--app/graphql/types/packages/nuget/metadatum_type.rb6
-rw-r--r--app/models/bulk_import.rb11
-rw-r--r--app/models/bulk_imports/entity.rb4
-rw-r--r--app/models/member.rb5
-rw-r--r--app/models/packages/helm/file_metadatum.rb2
-rw-r--r--app/serializers/member_entity.rb6
-rw-r--r--app/services/bulk_imports/create_service.rb13
-rw-r--r--app/services/bulk_imports/get_importable_data_service.rb45
-rw-r--r--app/views/admin/users/_access_levels.html.haml24
-rw-r--r--app/views/admin/users/_form.html.haml2
-rw-r--r--app/views/layouts/_page.html.haml1
-rw-r--r--app/views/projects/ci/pipeline_editor/show.html.haml2
45 files changed, 512 insertions, 132 deletions
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index e0105d63d99..9bbb8a1a1b2 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -3,15 +3,18 @@ import { GlDrawer } from '@gitlab/ui';
import { MountingPortal } from 'portal-vue';
import { mapState, mapActions, mapGetters } from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
+import { __, sprintf } from '~/locale';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
+import SidebarLabelsWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
@@ -23,6 +26,7 @@ export default {
SidebarConfidentialityWidget,
BoardSidebarTimeTracker,
BoardSidebarLabelsSelect,
+ SidebarLabelsWidget,
SidebarSubscriptionsWidget,
SidebarDropdownWidget,
SidebarTodoWidget,
@@ -46,16 +50,20 @@ export default {
weightFeatureAvailable: {
default: false,
},
+ allowLabelEdit: {
+ default: false,
+ },
},
inheritAttrs: false,
computed: {
...mapGetters([
+ 'isGroupBoard',
'isSidebarOpen',
'activeBoardItem',
'groupPathForActiveIssue',
'projectPathForActiveIssue',
]),
- ...mapState(['sidebarType', 'issuableType']),
+ ...mapState(['sidebarType', 'issuableType', 'isSettingLabels']),
isIssuableSidebar() {
return this.sidebarType === ISSUABLE;
},
@@ -65,17 +73,48 @@ export default {
fullPath() {
return this.activeBoardItem?.referencePath?.split('#')[0] || '';
},
+ createLabelTitle() {
+ return sprintf(__('Create %{workspace} label'), {
+ workspace: this.isGroupBoard ? 'group' : 'project',
+ });
+ },
+ manageLabelTitle() {
+ return sprintf(__('Manage %{workspace} labels'), {
+ workspace: this.isGroupBoard ? 'group' : 'project',
+ });
+ },
+ attrWorkspacePath() {
+ return this.isGroupBoard ? this.groupPathForActiveIssue : undefined;
+ },
},
methods: {
...mapActions([
'toggleBoardItem',
'setAssignees',
'setActiveItemConfidential',
+ 'setActiveBoardItemLabels',
'setActiveItemWeight',
]),
handleClose() {
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
},
+ handleUpdateSelectedLabels(input) {
+ this.setActiveBoardItemLabels({
+ iid: this.activeBoardItem.iid,
+ projectPath: this.projectPathForActiveIssue,
+ addLabelIds: input.map((label) => getIdFromGraphQLId(label.id)),
+ removeLabelIds: this.activeBoardItem.labels
+ .filter((label) => !input.find((selected) => selected.id === label.id))
+ .map((label) => label.id),
+ });
+ },
+ handleLabelRemove(input) {
+ this.setActiveBoardItemLabels({
+ iid: this.activeBoardItem.iid,
+ projectPath: this.projectPathForActiveIssue,
+ removeLabelIds: [input],
+ });
+ },
},
};
</script>
@@ -160,7 +199,28 @@ export default {
:issuable-type="issuableType"
data-testid="sidebar-due-date"
/>
- <board-sidebar-labels-select class="block labels" />
+ <sidebar-labels-widget
+ v-if="glFeatures.labelsWidget"
+ class="block labels"
+ data-testid="sidebar-labels"
+ :iid="activeBoardItem.iid"
+ :full-path="projectPathForActiveIssue"
+ :allow-label-remove="allowLabelEdit"
+ :allow-multiselect="true"
+ :selected-labels="activeBoardItem.labels"
+ :labels-select-in-progress="isSettingLabels"
+ :footer-create-label-title="createLabelTitle"
+ :footer-manage-label-title="manageLabelTitle"
+ :labels-create-title="createLabelTitle"
+ :labels-filter-base-path="projectPathForActiveIssue"
+ :attr-workspace-path="attrWorkspacePath"
+ :issuable-type="issuableType"
+ @onLabelRemove="handleLabelRemove"
+ @updateSelectedLabels="handleUpdateSelectedLabels"
+ >
+ {{ __('None') }}
+ </sidebar-labels-widget>
+ <board-sidebar-labels-select v-else class="block labels" />
<sidebar-weight-widget
v-if="weightFeatureAvailable"
:iid="activeBoardItem.iid"
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 1c8b1f9b009..b6b1094fb3a 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -87,6 +87,9 @@ function mountBoardApp(el) {
iterationListsAvailable: parseBoolean(el.dataset.iterationListsAvailable),
issuableType: issuableTypes.issue,
emailsDisabled: parseBoolean(el.dataset.emailsDisabled),
+ allowLabelCreate: parseBoolean(el.dataset.canUpdate),
+ allowLabelEdit: parseBoolean(el.dataset.canUpdate),
+ allowScopedLabels: parseBoolean(el.dataset.scopedLabels),
},
render: (createComponent) => createComponent(BoardApp),
});
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 270f2ff085b..ca993e75cf9 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -656,6 +656,7 @@ export default {
},
setActiveIssueLabels: async ({ commit, getters }, input) => {
+ commit(types.SET_LABELS_LOADING, true);
const { activeBoardItem } = getters;
const { data } = await gqlClient.mutate({
mutation: issueSetLabelsMutation,
@@ -669,6 +670,8 @@ export default {
},
});
+ commit(types.SET_LABELS_LOADING, false);
+
if (data.updateIssue?.errors?.length > 0) {
throw new Error(data.updateIssue.errors);
}
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 928cece19f7..26b785932bb 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -28,6 +28,7 @@ export const ADD_BOARD_ITEM_TO_LIST = 'ADD_BOARD_ITEM_TO_LIST';
export const REMOVE_BOARD_ITEM_FROM_LIST = 'REMOVE_BOARD_ITEM_FROM_LIST';
export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
export const UPDATE_BOARD_ITEM_BY_ID = 'UPDATE_BOARD_ITEM_BY_ID';
+export const SET_LABELS_LOADING = 'SET_LABELS_LOADING';
export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING';
export const RESET_ISSUES = 'RESET_ISSUES';
export const REQUEST_GROUP_PROJECTS = 'REQUEST_GROUP_PROJECTS';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index ef5b84b4575..d381c076c19 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -195,6 +195,10 @@ export default {
Vue.set(state.boardItems[itemId], prop, value);
},
+ [mutationTypes.SET_LABELS_LOADING](state, isLoading) {
+ state.isSettingLabels = isLoading;
+ },
+
[mutationTypes.SET_ASSIGNEE_LOADING](state, isLoading) {
state.isSettingAssignees = isLoading;
},
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index 80c51c966d2..2a6605e687b 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -12,6 +12,7 @@ export default () => ({
listsFlags: {},
boardItemsByListId: {},
backupItemsList: [],
+ isSettingLabels: false,
isSettingAssignees: false,
pageInfoByListId: {},
boardItems: {},
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
index 4aee02e45c8..9d4eddc510a 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
@@ -293,7 +293,7 @@ export default {
:items="roles"
:loading="isLoadingRoles"
:loading-text="s__('ClusterIntegration|Loading IAM Roles')"
- :placeholder="s__('ClusterIntergation|Select service role')"
+ :placeholder="s__('ClusterIntegration|Select service role')"
:search-field-placeholder="s__('ClusterIntegration|Search IAM Roles')"
:empty-text="s__('ClusterIntegration|No IAM Roles found')"
:has-errors="Boolean(loadingRolesError)"
@@ -330,7 +330,7 @@ export default {
:disabled-text="s__('ClusterIntegration|Select a region to choose a Key Pair')"
:loading="isLoadingKeyPairs"
:loading-text="s__('ClusterIntegration|Loading Key Pairs')"
- :placeholder="s__('ClusterIntergation|Select key pair')"
+ :placeholder="s__('ClusterIntegration|Select key pair')"
:search-field-placeholder="s__('ClusterIntegration|Search Key Pairs')"
:empty-text="s__('ClusterIntegration|No Key Pairs found')"
:has-errors="Boolean(loadingKeyPairsError)"
@@ -359,7 +359,7 @@ export default {
:disabled="vpcDropdownDisabled"
:disabled-text="s__('ClusterIntegration|Select a region to choose a VPC')"
:loading-text="s__('ClusterIntegration|Loading VPCs')"
- :placeholder="s__('ClusterIntergation|Select a VPC')"
+ :placeholder="s__('ClusterIntegration|Select a VPC')"
:search-field-placeholder="s__('ClusterIntegration|Search VPCs')"
:empty-text="s__('ClusterIntegration|No VPCs found')"
:has-errors="Boolean(loadingVpcsError)"
@@ -389,7 +389,7 @@ export default {
:disabled="subnetDropdownDisabled"
:disabled-text="s__('ClusterIntegration|Select a VPC to choose a subnet')"
:loading-text="s__('ClusterIntegration|Loading subnets')"
- :placeholder="s__('ClusterIntergation|Select a subnet')"
+ :placeholder="s__('ClusterIntegration|Select a subnet')"
:search-field-placeholder="s__('ClusterIntegration|Search subnets')"
:empty-text="s__('ClusterIntegration|No subnet found')"
:has-errors="displaySubnetError"
@@ -420,7 +420,7 @@ export default {
:disabled="securityGroupDropdownDisabled"
:disabled-text="s__('ClusterIntegration|Select a VPC to choose a security group')"
:loading-text="s__('ClusterIntegration|Loading security groups')"
- :placeholder="s__('ClusterIntergation|Select a security group')"
+ :placeholder="s__('ClusterIntegration|Select a security group')"
:search-field-placeholder="s__('ClusterIntegration|Search security groups')"
:empty-text="s__('ClusterIntegration|No security group found')"
:has-errors="Boolean(loadingSecurityGroupsError)"
@@ -451,7 +451,7 @@ export default {
:items="instanceTypes"
:loading="isLoadingInstanceTypes"
:loading-text="s__('ClusterIntegration|Loading instance types')"
- :placeholder="s__('ClusterIntergation|Select an instance type')"
+ :placeholder="s__('ClusterIntegration|Select an instance type')"
:search-field-placeholder="s__('ClusterIntegration|Search instance types')"
:empty-text="s__('ClusterIntegration|No instance type found')"
:has-errors="Boolean(loadingInstanceTypesError)"
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue
index 12b6070a79a..8f18ac29c0f 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue
+++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue
@@ -43,7 +43,7 @@ export default {
:loading="isLoadingItems"
:has-errors="Boolean(loadingItemsError)"
:loading-text="s__('ClusterIntegration|Loading networks')"
- :placeholder="s__('ClusterIntergation|Select a network')"
+ :placeholder="s__('ClusterIntegration|Select a network')"
:search-field-placeholder="s__('ClusterIntegration|Search networks')"
:empty-text="s__('ClusterIntegration|No networks found')"
:error-message="s__('ClusterIntegration|Could not load networks')"
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue
index ec7889e2907..dab4adc3789 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue
+++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue
@@ -34,7 +34,7 @@ export default {
:loading="isLoadingItems"
:has-errors="Boolean(loadingItemsError)"
:loading-text="s__('ClusterIntegration|Loading subnetworks')"
- :placeholder="s__('ClusterIntergation|Select a subnetwork')"
+ :placeholder="s__('ClusterIntegration|Select a subnetwork')"
:search-field-placeholder="s__('ClusterIntegration|Search subnetworks')"
:empty-text="s__('ClusterIntegration|No subnetworks found')"
:error-message="s__('ClusterIntegration|Could not load subnetworks')"
diff --git a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
index 858c30649bb..1a470d74b59 100644
--- a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
+++ b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
@@ -17,7 +17,7 @@ export default {
},
},
i18n: {
- percentageDescription: __('Enter an integer number number between 0 and 100'),
+ percentageDescription: __('Enter an integer number between 0 and 100'),
percentageInvalid: __('Percent rollout must be an integer number between 0 and 100'),
percentageLabel: __('Percentage'),
stickinessDescription: __('Consistency guarantee method'),
diff --git a/app/assets/javascripts/issuable_suggestions/components/item.vue b/app/assets/javascripts/issuable_suggestions/components/item.vue
index dea7608685a..a01f4f747b9 100644
--- a/app/assets/javascripts/issuable_suggestions/components/item.vue
+++ b/app/assets/javascripts/issuable_suggestions/components/item.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlLink, GlTooltip, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { __ } from '~/locale';
@@ -26,12 +25,6 @@ export default {
},
},
computed: {
- isOpen() {
- return this.suggestion.state === 'opened';
- },
- isClosed() {
- return this.suggestion.state === 'closed';
- },
counts() {
return [
{
@@ -48,7 +41,13 @@ export default {
},
].filter(({ count }) => count);
},
- stateIcon() {
+ isClosed() {
+ return this.suggestion.state === 'closed';
+ },
+ stateIconClass() {
+ return this.isClosed ? 'gl-text-blue-500' : 'gl-text-green-500';
+ },
+ stateIconName() {
return this.isClosed ? 'issue-close' : 'issue-open-m';
},
stateTitle() {
@@ -72,7 +71,7 @@ export default {
v-gl-tooltip.bottom
:title="__('Confidential')"
name="eye-slash"
- class="suggestion-help-hover mr-1 suggestion-confidential"
+ class="gl-cursor-help gl-mr-2 gl-text-orange-500"
/>
<gl-link
:href="suggestion.webUrl"
@@ -83,15 +82,7 @@ export default {
</gl-link>
</div>
<div class="text-secondary suggestion-footer">
- <gl-icon
- ref="state"
- :name="stateIcon"
- :class="{
- 'suggestion-state-open': isOpen,
- 'suggestion-state-closed': isClosed,
- }"
- class="suggestion-help-hover"
- />
+ <gl-icon ref="state" :name="stateIconName" :class="stateIconClass" class="gl-cursor-help" />
<gl-tooltip :target="() => $refs.state" placement="bottom">
<span class="d-block">
<span class="bold"> {{ stateTitle }} </span> {{ timeFormatted(closedOrCreatedDate) }}
@@ -102,9 +93,9 @@ export default {
<timeago-tooltip
:time="suggestion.createdAt"
tooltip-placement="bottom"
- class="suggestion-help-hover"
+ class="gl-cursor-help"
/>
- by
+ {{ __('by') }}
<gl-link :href="suggestion.author.webUrl">
<user-avatar-image
:img-src="suggestion.author.avatarUrl"
@@ -122,7 +113,7 @@ export default {
<timeago-tooltip
:time="suggestion.updatedAt"
tooltip-placement="bottom"
- class="suggestion-help-hover"
+ class="gl-cursor-help"
/>
</template>
<span class="suggestion-counts">
@@ -131,7 +122,7 @@ export default {
:key="id"
v-gl-tooltip.bottom
:title="tooltipTitle"
- class="suggestion-help-hover gl-ml-3 text-tertiary"
+ class="gl-cursor-help gl-ml-3 text-tertiary"
>
<gl-icon :name="icon" /> {{ count }}
</span>
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 3caecdd1d4b..202f3aa89e1 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -5,7 +5,13 @@ import MembersTableCell from 'ee_else_ce/members/components/table/members_table_
import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import initUserPopovers from '~/user_popovers';
-import { FIELDS, ACTIVE_TAB_QUERY_PARAM_NAME } from '../../constants';
+import {
+ FIELDS,
+ ACTIVE_TAB_QUERY_PARAM_NAME,
+ MEMBER_STATE_AWAITING,
+ USER_STATE_BLOCKED_PENDING_APPROVAL,
+ BADGE_LABELS_PENDING_OWNER_APPROVAL,
+} from '../../constants';
import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
import RemoveMemberModal from '../modals/remove_member_modal.vue';
import CreatedAt from './created_at.vue';
@@ -129,6 +135,74 @@ export default {
window.location.href,
);
},
+ /**
+ * Returns whether it's a new or existing user
+ *
+ * If memberInviteMetadata doesn't exist, it means we're adding an existing user
+ * to the Group/Project, so `isNewUser` should be false.
+ * If memberInviteMetadata exists but `userState` has content,
+ * the user has registered but is awaiting root approval
+ *
+ * @param {object} memberInviteMetadata - MemberEntity.invite
+ * @see {@link ~/app/serializers/member_entity.rb}
+ * @returns {boolean}
+ */
+ isNewUser(memberInviteMetadata) {
+ return memberInviteMetadata && !memberInviteMetadata.userState;
+ },
+ /**
+ * Returns whether the user is awaiting root approval
+ *
+ * This checks User.state exposed via MemberEntity
+ *
+ * @param {object} memberInviteMetadata - MemberEntity.invite
+ * @see {@link ~/app/serializers/member_entity.rb}
+ * @returns {boolean}
+ */
+ isUserPendingRootApproval(memberInviteMetadata) {
+ return memberInviteMetadata?.userState === USER_STATE_BLOCKED_PENDING_APPROVAL;
+ },
+ /**
+ * Returns whether the member is awaiting owner approval
+ *
+ * This checks Member.state exposed via MemberEntity
+ *
+ * @param {Number} memberState - Member.state exposed via MemberEntity.state
+ * @see {@link ~/ee/app/models/ee/member.rb}
+ * @see {@link ~/app/serializers/member_entity.rb}
+ * @returns {boolean}
+ */
+ isMemberPendingOwnerApproval(memberState) {
+ return memberState === MEMBER_STATE_AWAITING;
+ },
+ isUserAwaiting(memberInviteMetadata, memberState) {
+ return (
+ this.isUserPendingRootApproval(memberInviteMetadata) ||
+ this.isMemberPendingOwnerApproval(memberState)
+ );
+ },
+ shouldAddPendingOwnerApprovalBadge(memberInviteMetadata, memberState) {
+ return (
+ this.isUserAwaiting(memberInviteMetadata, memberState) &&
+ !this.isNewUser(memberInviteMetadata)
+ );
+ },
+ /**
+ * Returns the string to be used in the invite badge
+ *
+ * @param {object} memberInviteMetadata - MemberEntity.invite
+ * @see {@link ~/app/serializers/member_entity.rb}
+ * @param {Number} memberState - Member.state exposed via MemberEntity.state
+ * @see {@link ~/ee/app/models/ee/member.rb}
+ * @returns {string}
+ */
+ inviteBadge(memberInviteMetadata, memberState) {
+ if (this.shouldAddPendingOwnerApprovalBadge(memberInviteMetadata, memberState)) {
+ return BADGE_LABELS_PENDING_OWNER_APPROVAL;
+ }
+
+ return '';
+ },
},
};
</script>
@@ -172,8 +246,11 @@ export default {
<created-at :date="createdAt" :created-by="createdBy" />
</template>
- <template #cell(invited)="{ item: { createdAt, createdBy } }">
+ <template #cell(invited)="{ item: { createdAt, createdBy, invite, state } }">
<created-at :date="createdAt" :created-by="createdBy" />
+ <gl-badge v-if="inviteBadge(invite, state)" data-testid="invited-badge">{{
+ inviteBadge(invite, state)
+ }}</gl-badge>
</template>
<template #cell(requested)="{ item: { createdAt } }">
diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js
index 54d4442d5ce..f5ca881ab0d 100644
--- a/app/assets/javascripts/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
@@ -89,6 +89,22 @@ export const TAB_QUERY_PARAM_VALUES = {
accessRequest: 'access_requests',
};
+/**
+ * This user state value comes from the User model
+ * see the state machine in app/models/user.rb
+ */
+export const USER_STATE_BLOCKED_PENDING_APPROVAL = 'blocked_pending_approval';
+
+/**
+ * This and following member state constants' values
+ * come from ee/app/models/ee/member.rb
+ */
+export const MEMBER_STATE_CREATED = 0;
+export const MEMBER_STATE_AWAITING = 1;
+export const MEMBER_STATE_ACTIVE = 2;
+
+export const BADGE_LABELS_PENDING_OWNER_APPROVAL = __('Pending owner approval');
+
export const DAYS_TO_EXPIRE_SOON = 7;
export const LEAVE_MODAL_ID = 'member-leave-modal';
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue
index f0da7db6c91..1360b03856f 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue
@@ -24,7 +24,13 @@ export default {
<template>
<div>
- <details-row icon="project" padding="gl-p-4" dashed data-testid="nuget-source">
+ <details-row
+ v-if="packageEntity.metadata.projectUrl"
+ icon="project"
+ padding="gl-p-4"
+ dashed
+ data-testid="nuget-source"
+ >
<gl-sprintf :message="$options.i18n.sourceText">
<template #link>
<gl-link :href="packageEntity.metadata.projectUrl" target="_blank">{{
@@ -33,7 +39,12 @@ export default {
</template>
</gl-sprintf>
</details-row>
- <details-row icon="license" padding="gl-p-4" data-testid="nuget-license">
+ <details-row
+ v-if="packageEntity.metadata.licenseUrl"
+ icon="license"
+ padding="gl-p-4"
+ data-testid="nuget-license"
+ >
<gl-sprintf :message="$options.i18n.licenseText">
<template #link>
<gl-link :href="packageEntity.metadata.licenseUrl" target="_blank">{{
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
new file mode 100644
index 00000000000..75b1398a3c2
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
@@ -0,0 +1,49 @@
+<script>
+import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
+
+export default {
+ components: {
+ PipelineMiniGraph,
+ },
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ pipelinePath() {
+ return this.pipeline.detailedStatus?.detailsPath || '';
+ },
+ pipelineStages() {
+ const stages = this.pipeline.stages?.edges;
+ if (!stages) {
+ return [];
+ }
+
+ return stages.map(({ node }) => {
+ const { name, detailedStatus } = node;
+ return {
+ // TODO: fetch dropdown_path from graphql when available
+ // see https://gitlab.com/gitlab-org/gitlab/-/issues/342585
+ dropdown_path: `${this.pipelinePath}/stage.json?stage=${name}`,
+ name,
+ path: `${this.pipelinePath}#${name}`,
+ status: {
+ details_path: `${this.pipelinePath}#${name}`,
+ has_details: detailedStatus.hasDetails,
+ ...detailedStatus,
+ },
+ title: `${name}: ${detailedStatus.text}`,
+ };
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="pipelineStages.length > 0" class="stage-cell gl-mr-5">
+ <pipeline-mini-graph class="gl-display-inline" :stages="pipelineStages" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
index ec240854be5..a1fa2147994 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
@@ -10,6 +10,8 @@ import {
toggleQueryPollingByVisibility,
} from '~/pipelines/components/graph/utils';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
const POLL_INTERVAL = 10000;
export const i18n = {
@@ -30,7 +32,9 @@ export default {
GlLink,
GlLoadingIcon,
GlSprintf,
+ PipelineEditorMiniGraph,
},
+ mixins: [glFeatureFlagMixin()],
inject: ['projectFullPath'],
props: {
commitSha: {
@@ -55,12 +59,15 @@ export default {
};
},
update(data) {
- const { id, commitPath = '', detailedStatus = {} } = data.project?.pipeline || {};
+ const { id, commitPath = '', detailedStatus = {}, stages, status } =
+ data.project?.pipeline || {};
return {
id,
commitPath,
detailedStatus,
+ stages,
+ status,
};
},
result(res) {
@@ -111,9 +118,7 @@ export default {
</script>
<template>
- <div
- class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-white-space-nowrap gl-max-w-full"
- >
+ <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-wrap">
<template v-if="showLoadingState">
<div>
<gl-loading-icon class="gl-mr-auto gl-display-inline-block" size="sm" />
@@ -129,19 +134,12 @@ export default {
<template v-else>
<div>
<a :href="status.detailsPath" class="gl-mr-auto">
- <ci-icon :status="status" :size="16" />
+ <ci-icon :status="status" :size="16" data-testid="pipeline-status-icon" />
</a>
<span class="gl-font-weight-bold">
<gl-sprintf :message="$options.i18n.pipelineInfo">
<template #id="{ content }">
- <gl-link
- :href="status.detailsPath"
- class="pipeline-id gl-font-weight-normal pipeline-number"
- target="_blank"
- data-testid="pipeline-id"
- >
- {{ content }}{{ pipelineId }}</gl-link
- >
+ <span data-testid="pipeline-id"> {{ content }}{{ pipelineId }} </span>
</template>
<template #status>{{ status.text }}</template>
<template #commit>
@@ -157,8 +155,13 @@ export default {
</gl-sprintf>
</span>
</div>
- <div>
+ <div class="gl-display-flex gl-flex-wrap">
+ <pipeline-editor-mini-graph
+ v-if="glFeatures.pipelineEditorMiniGraph"
+ :pipeline="pipeline"
+ />
<gl-button
+ class="gl-mt-2 gl-md-mt-0"
target="_blank"
category="secondary"
variant="confirm"
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql
index d3a7387ad2d..0c3653a2880 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline.graphql
@@ -11,6 +11,25 @@ query getPipeline($fullPath: ID!, $sha: String!) {
group
text
}
+ stages {
+ edges {
+ node {
+ id
+ name
+ status
+ detailedStatus {
+ detailsPath
+ group
+ hasDetails
+ icon
+ id
+ label
+ text
+ tooltip
+ }
+ }
+ }
+ }
}
}
}
diff --git a/app/assets/javascripts/related_issues/components/add_issuable_form.vue b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
index 545263a9e37..f936c03c5d3 100644
--- a/app/assets/javascripts/related_issues/components/add_issuable_form.vue
+++ b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
@@ -201,7 +201,7 @@ export default {
<gl-button
ref="addButton"
category="primary"
- variant="success"
+ variant="confirm"
:disabled="isSubmitButtonDisabled"
:loading="isSubmitting"
type="submit"
diff --git a/app/assets/javascripts/search_settings/components/search_settings.vue b/app/assets/javascripts/search_settings/components/search_settings.vue
index 116967a62c8..3e23b8a3435 100644
--- a/app/assets/javascripts/search_settings/components/search_settings.vue
+++ b/app/assets/javascripts/search_settings/components/search_settings.vue
@@ -1,7 +1,13 @@
<script>
import { GlSearchBoxByType } from '@gitlab/ui';
-import { uniq } from 'lodash';
-import { EXCLUDED_NODES, HIDE_CLASS, HIGHLIGHT_CLASS, TYPING_DELAY } from '../constants';
+import { uniq, escapeRegExp } from 'lodash';
+import {
+ EXCLUDED_NODES,
+ HIDE_CLASS,
+ HIGHLIGHT_CLASS,
+ NONE_PADDING_CLASS,
+ TYPING_DELAY,
+} from '../constants';
const origExpansions = new Map();
@@ -37,9 +43,13 @@ const resetSections = ({ sectionSelector }) => {
};
const clearHighlights = () => {
- document
- .querySelectorAll(`.${HIGHLIGHT_CLASS}`)
- .forEach((element) => element.classList.remove(HIGHLIGHT_CLASS));
+ document.querySelectorAll(`.${HIGHLIGHT_CLASS}`).forEach((element) => {
+ const { parentNode } = element;
+ const textNode = document.createTextNode(element.textContent);
+ parentNode.replaceChild(textNode, element);
+
+ parentNode.normalize();
+ });
};
const hideSectionsExcept = (sectionSelector, visibleSections) => {
@@ -50,17 +60,41 @@ const hideSectionsExcept = (sectionSelector, visibleSections) => {
});
};
-const highlightElements = (elements = []) => {
- elements.forEach((element) => element.classList.add(HIGHLIGHT_CLASS));
+const transformMatchElement = (element, searchTerm) => {
+ const textStr = element.textContent;
+ const escapedSearchTerm = new RegExp(`(${escapeRegExp(searchTerm)})`, 'gi');
+
+ const textList = textStr.split(escapedSearchTerm);
+ const replaceFragment = document.createDocumentFragment();
+ textList.forEach((text) => {
+ let addElement = document.createTextNode(text);
+ if (escapedSearchTerm.test(text)) {
+ addElement = document.createElement('mark');
+ addElement.className = `${HIGHLIGHT_CLASS} ${NONE_PADDING_CLASS}`;
+ addElement.textContent = text;
+ escapedSearchTerm.lastIndex = 0;
+ }
+ replaceFragment.appendChild(addElement);
+ });
+
+ return replaceFragment;
+};
+
+const highlightElements = (elements = [], searchTerm) => {
+ elements.forEach((element) => {
+ const replaceFragment = transformMatchElement(element, searchTerm);
+ element.innerHTML = '';
+ element.appendChild(replaceFragment);
+ });
};
-const displayResults = ({ sectionSelector, expandSection }, matches) => {
+const displayResults = ({ sectionSelector, expandSection, searchTerm }, matches) => {
const elements = matches.map((match) => match.parentElement);
const sections = uniq(elements.map((element) => findSettingsSection(sectionSelector, element)));
hideSectionsExcept(sectionSelector, sections);
sections.forEach(expandSection);
- highlightElements(elements);
+ highlightElements(elements, searchTerm);
};
const clearResults = (params) => {
@@ -116,21 +150,21 @@ export default {
},
methods: {
search(value) {
+ this.searchTerm = value;
const displayOptions = {
sectionSelector: this.sectionSelector,
expandSection: this.expandSection,
collapseSection: this.collapseSection,
isExpanded: this.isExpandedFn,
+ searchTerm: this.searchTerm,
};
- this.searchTerm = value;
-
clearResults(displayOptions);
if (value.length) {
saveExpansionState(document.querySelectorAll(this.sectionSelector), displayOptions);
- displayResults(displayOptions, search(this.searchRoot, value));
+ displayResults(displayOptions, search(this.searchRoot, this.searchTerm));
} else {
restoreExpansionState(displayOptions);
}
diff --git a/app/assets/javascripts/search_settings/constants.js b/app/assets/javascripts/search_settings/constants.js
index 9452d149122..a49351dc7b0 100644
--- a/app/assets/javascripts/search_settings/constants.js
+++ b/app/assets/javascripts/search_settings/constants.js
@@ -7,5 +7,8 @@ export const HIDE_CLASS = 'gl-display-none';
// used to highlight the text that matches the * search term
export const HIGHLIGHT_CLASS = 'gl-bg-orange-100';
+// used to remove padding for text that matches the * search term
+export const NONE_PADDING_CLASS = 'gl-p-0';
+
// How many seconds to wait until the user * stops typing
export const TYPING_DELAY = 400;
diff --git a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
index af426584f4f..d5647619ea3 100644
--- a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
+++ b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
@@ -36,6 +36,7 @@ export default {
'allowLabelEdit',
'allowScopedLabels',
'iid',
+ 'fullPath',
'initiallySelectedLabels',
'issuableType',
'labelsFetchPath',
@@ -145,6 +146,8 @@ export default {
<labels-select-widget
v-if="glFeatures.labelsWidget"
class="block labels js-labels-block"
+ :iid="iid"
+ :full-path="fullPath"
:allow-label-remove="allowLabelEdit"
:allow-multiselect="true"
:footer-create-label-title="__('Create project label')"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
index 13701cbbd64..3ee0baf8812 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
@@ -57,6 +57,15 @@ export default {
required: false,
default: false,
},
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ attrWorkspacePath: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
},
data() {
return {
@@ -182,6 +191,8 @@ export default {
:selected-labels="selectedLabels"
:allow-multiselect="allowMultiselect"
:issuable-type="issuableType"
+ :full-path="fullPath"
+ :attr-workspace-path="attrWorkspacePath"
@hideCreateView="toggleDropdownContentsCreateView"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
index 0f660c92e7c..a2ed08e6b28 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
@@ -19,16 +19,20 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: {
- fullPath: {
- default: '',
- },
- },
props: {
issuableType: {
type: String,
required: true,
},
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ attrWorkspacePath: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
},
data() {
return {
@@ -46,11 +50,19 @@ export default {
return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] }));
},
mutationVariables() {
- return this.issuableType === IssuableType.Epic
+ if (this.issuableType === IssuableType.Epic) {
+ return {
+ title: this.labelTitle,
+ color: this.selectedColor,
+ groupPath: this.fullPath,
+ };
+ }
+
+ return this.attrWorkspacePath !== undefined
? {
title: this.labelTitle,
color: this.selectedColor,
- groupPath: this.fullPath,
+ groupPath: this.attrWorkspacePath,
}
: {
title: this.labelTitle,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
index 2215f2cba91..e6a25362ff0 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
@@ -24,7 +24,6 @@ export default {
GlIntersectionObserver,
LabelItem,
},
- inject: ['fullPath'],
model: {
prop: 'localSelectedLabels',
},
@@ -45,6 +44,10 @@ export default {
type: Array,
required: true,
},
+ fullPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -84,7 +87,7 @@ export default {
return this.$apollo.queries.labels.loading;
},
localSelectedLabelsIds() {
- return this.localSelectedLabels.map((label) => label.id);
+ return this.localSelectedLabels.map((label) => getIdFromGraphQLId(label.id));
},
visibleLabels() {
if (this.searchKey) {
@@ -130,7 +133,9 @@ export default {
updateSelectedLabels(label) {
let labels;
if (this.isLabelSelected(label)) {
- labels = this.localSelectedLabels.filter(({ id }) => id !== getIdFromGraphQLId(label.id));
+ labels = this.localSelectedLabels.filter(
+ ({ id }) => id !== getIdFromGraphQLId(label.id) && id !== label.id,
+ );
} else {
labels = [
...this.localSelectedLabels,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
index e4ec7909bdd..6bd43da2203 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
@@ -21,15 +21,20 @@ export default {
SidebarEditableItem,
},
inject: {
- iid: {
- default: '',
- },
allowLabelEdit: {
default: false,
},
- fullPath: {},
},
props: {
+ iid: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
allowLabelRemove: {
type: Boolean,
required: false,
@@ -99,6 +104,11 @@ export default {
type: String,
required: true,
},
+ attrWorkspacePath: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
},
data() {
return {
@@ -206,6 +216,8 @@ export default {
:variant="variant"
:issuable-type="issuableType"
:is-visible="edit"
+ :full-path="fullPath"
+ :attr-workspace-path="attrWorkspacePath"
@setLabels="handleDropdownClose"
@closeDropdown="collapseEditableItem"
/>
@@ -224,6 +236,7 @@ export default {
:selected-labels="selectedLabels"
:variant="variant"
:issuable-type="issuableType"
+ :full-path="fullPath"
@setLabels="handleDropdownClose"
/>
</div>
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 6296b023e90..a2d822bb5de 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -877,18 +877,6 @@
vertical-align: sub;
}
-.suggestion-confidential {
- color: $orange-500;
-}
-
-.suggestion-state-open {
- color: $green-500;
-}
-
-.suggestion-state-closed {
- color: $blue-500;
-}
-
.suggestion-help-hover {
cursor: help;
}
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index 60708c13b85..e8e6a7e5c1a 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -11,6 +11,7 @@ class Groups::BoardsController < Groups::ApplicationController
push_frontend_feature_flag(:board_multi_select, group, default_enabled: :yaml)
push_frontend_feature_flag(:swimlanes_buffered_rendering, group, default_enabled: :yaml)
push_frontend_feature_flag(:iteration_cadences, group, default_enabled: :yaml)
+ push_frontend_feature_flag(:labels_widget, group, default_enabled: :yaml)
end
feature_category :boards
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 558a4bbfd45..6e59f159636 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -54,7 +54,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def invited_members
- group_members.invite
+ group_members.invite.with_invited_user_state
end
def non_invited_members
diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb
index 2dca9385da6..bec26cb547d 100644
--- a/app/controllers/import/bulk_imports_controller.rb
+++ b/app/controllers/import/bulk_imports_controller.rb
@@ -22,13 +22,16 @@ class Import::BulkImportsController < ApplicationController
def status
respond_to do |format|
format.json do
- data = importable_data
+ data = ::BulkImports::GetImportableDataService.new(params, query_params, credentials).execute
pagination_headers.each do |header|
- response.set_header(header, data.headers[header])
+ response.set_header(header, data[:response].headers[header])
end
- render json: { importable_data: serialized_data(data.parsed_response) }
+ json_response = { importable_data: serialized_data(data[:response].parsed_response) }
+ json_response[:version_validation] = data[:version_validation]
+
+ render json: json_response
end
format.html do
@source_url = session[url_key]
@@ -66,10 +69,6 @@ class Import::BulkImportsController < ApplicationController
@serializer ||= BaseSerializer.new(current_user: current_user)
end
- def importable_data
- client.get('groups', query_params)
- end
-
# Default query string params used to fetch groups from GitLab source instance
#
# top_level_only: fetch only top level groups (subgroups are fetched during import itself)
@@ -85,15 +84,6 @@ class Import::BulkImportsController < ApplicationController
query_params
end
- def client
- @client ||= BulkImports::Clients::HTTP.new(
- url: session[url_key],
- token: session[access_token_key],
- per_page: params[:per_page],
- page: params[:page]
- )
- end
-
def configure_params
params.permit(access_token_key, url_key)
end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 316582f3994..834e4baa7dd 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -11,6 +11,7 @@ class Projects::BoardsController < Projects::ApplicationController
push_frontend_feature_flag(:issue_boards_filtered_search, project, default_enabled: :yaml)
push_frontend_feature_flag(:board_multi_select, project, default_enabled: :yaml)
push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml)
+ push_frontend_feature_flag(:labels_widget, project, default_enabled: :yaml)
end
feature_category :boards
diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb
index 953b9d83a66..e925bbea5e9 100644
--- a/app/controllers/projects/ci/pipeline_editor_controller.rb
+++ b/app/controllers/projects/ci/pipeline_editor_controller.rb
@@ -4,6 +4,7 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController
before_action :check_can_collaborate!
before_action do
push_frontend_feature_flag(:pipeline_editor_drawer, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:pipeline_editor_mini_graph, @project, default_enabled: :yaml)
push_frontend_feature_flag(:schema_linting, @project, default_enabled: :yaml)
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index cce2497ae25..e8074f7d793 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -58,7 +58,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def invited_members
- members.invite
+ members.invite.with_invited_user_state
end
def non_invited_members
diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb
index e51bfe6a37e..c3c6a51239d 100644
--- a/app/controllers/repositories/git_http_controller.rb
+++ b/app/controllers/repositories/git_http_controller.rb
@@ -11,6 +11,9 @@ module Repositories
rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404_with_exception
rescue_from Gitlab::GitAccessProject::CreationError, with: :render_422_with_exception
rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503_with_exception
+ rescue_from GRPC::Unavailable do |e|
+ render_503_with_exception(e, message: 'The git server, Gitaly, is not available at this time. Please contact your administrator.')
+ end
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
@@ -71,8 +74,8 @@ module Repositories
render plain: exception.message, status: :unprocessable_entity
end
- def render_503_with_exception(exception)
- render plain: exception.message, status: :service_unavailable
+ def render_503_with_exception(exception, message: nil)
+ render plain: message || exception.message, status: :service_unavailable
end
def update_fetch_statistics
diff --git a/app/graphql/types/packages/nuget/metadatum_type.rb b/app/graphql/types/packages/nuget/metadatum_type.rb
index ed9d97724af..b58fd954a74 100644
--- a/app/graphql/types/packages/nuget/metadatum_type.rb
+++ b/app/graphql/types/packages/nuget/metadatum_type.rb
@@ -10,9 +10,9 @@ module Types
authorize :read_package
field :id, ::Types::GlobalIDType[::Packages::Nuget::Metadatum], null: false, description: 'ID of the metadatum.'
- field :license_url, GraphQL::Types::String, null: false, description: 'License URL of the Nuget package.'
- field :project_url, GraphQL::Types::String, null: false, description: 'Project URL of the Nuget package.'
- field :icon_url, GraphQL::Types::String, null: false, description: 'Icon URL of the Nuget package.'
+ field :license_url, GraphQL::Types::String, null: true, description: 'License URL of the Nuget package.'
+ field :project_url, GraphQL::Types::String, null: true, description: 'Project URL of the Nuget package.'
+ field :icon_url, GraphQL::Types::String, null: true, description: 'Icon URL of the Nuget package.'
end
end
end
diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb
index dee55675304..818ae04ba29 100644
--- a/app/models/bulk_import.rb
+++ b/app/models/bulk_import.rb
@@ -4,7 +4,8 @@
# projects to a GitLab instance. It associates the import with the responsible
# user.
class BulkImport < ApplicationRecord
- MINIMUM_GITLAB_MAJOR_VERSION = 14
+ MIN_MAJOR_VERSION = 14
+ MIN_MINOR_VERSION_FOR_PROJECT = 4
belongs_to :user, optional: false
@@ -34,6 +35,14 @@ class BulkImport < ApplicationRecord
end
end
+ def source_version_info
+ Gitlab::VersionInfo.parse(source_version)
+ end
+
+ def self.min_gl_version_for_project_migration
+ Gitlab::VersionInfo.new(MIN_MAJOR_VERSION, MIN_MINOR_VERSION_FOR_PROJECT)
+ end
+
def self.all_human_statuses
state_machine.states.map(&:human_name)
end
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index d2b9e1f567a..ecac4ab95f4 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -83,9 +83,9 @@ class BulkImports::Entity < ApplicationRecord
def pipelines
@pipelines ||= case source_type
when 'group_entity'
- BulkImports::Groups::Stage.pipelines
+ BulkImports::Groups::Stage.new(bulk_import).pipelines
when 'project_entity'
- BulkImports::Projects::Stage.pipelines
+ BulkImports::Projects::Stage.new(bulk_import).pipelines
end
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 45af34a3e6b..7e0b4705217 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -50,6 +50,11 @@ class Member < ApplicationRecord
},
if: :project_bot?
+ scope :with_invited_user_state, -> do
+ joins('LEFT JOIN users as invited_user ON invited_user.email = members.invite_email')
+ .select('members.*', 'invited_user.state as invited_user_state')
+ end
+
scope :in_hierarchy, ->(source) do
groups = source.root_ancestor.self_and_descendants
group_members = Member.default_scoped.where(source: groups)
diff --git a/app/models/packages/helm/file_metadatum.rb b/app/models/packages/helm/file_metadatum.rb
index 1771003d1f9..dfa4ab6df82 100644
--- a/app/models/packages/helm/file_metadatum.rb
+++ b/app/models/packages/helm/file_metadatum.rb
@@ -12,7 +12,7 @@ module Packages
validates :channel,
presence: true,
- length: { maximum: 63 },
+ length: { maximum: 255 },
format: { with: Gitlab::Regex.helm_channel_regex }
validates :metadata,
diff --git a/app/serializers/member_entity.rb b/app/serializers/member_entity.rb
index 5100a41638e..d7221109ecb 100644
--- a/app/serializers/member_entity.rb
+++ b/app/serializers/member_entity.rb
@@ -44,6 +44,8 @@ class MemberEntity < Grape::Entity
MemberUserEntity.represent(member.user, source: options[:source])
end
+ expose :state
+
expose :invite, if: -> (member) { member.invite? } do
expose :email do |member|
member.invite_email
@@ -56,6 +58,10 @@ class MemberEntity < Grape::Entity
expose :can_resend do |member|
member.can_resend_invite?
end
+
+ expose :user_state do |member|
+ member.respond_to?(:invited_user_state) ? member.invited_user_state : ""
+ end
end
end
diff --git a/app/services/bulk_imports/create_service.rb b/app/services/bulk_imports/create_service.rb
index 1cea7632aa1..c1becbb5609 100644
--- a/app/services/bulk_imports/create_service.rb
+++ b/app/services/bulk_imports/create_service.rb
@@ -52,7 +52,11 @@ module BulkImports
def create_bulk_import
BulkImport.transaction do
- bulk_import = BulkImport.create!(user: current_user, source_type: 'gitlab')
+ bulk_import = BulkImport.create!(
+ user: current_user,
+ source_type: 'gitlab',
+ source_version: client.instance_version
+ )
bulk_import.create_configuration!(credentials.slice(:url, :access_token))
params.each do |entity|
@@ -68,5 +72,12 @@ module BulkImports
bulk_import
end
end
+
+ def client
+ @client ||= BulkImports::Clients::HTTP.new(
+ url: @credentials[:url],
+ token: @credentials[:access_token]
+ )
+ end
end
end
diff --git a/app/services/bulk_imports/get_importable_data_service.rb b/app/services/bulk_imports/get_importable_data_service.rb
new file mode 100644
index 00000000000..07e0b3976a1
--- /dev/null
+++ b/app/services/bulk_imports/get_importable_data_service.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class GetImportableDataService
+ def initialize(params, query_params, credentials)
+ @params = params
+ @query_params = query_params
+ @credentials = credentials
+ end
+
+ def execute
+ {
+ version_validation: version_validation,
+ response: importables
+ }
+ end
+
+ private
+
+ def importables
+ client.get('groups', @query_params)
+ end
+
+ def version_validation
+ {
+ features: {
+ project_migration: {
+ available: client.compatible_for_project_migration?,
+ min_version: BulkImport.min_gl_version_for_project_migration.to_s
+ },
+ source_instance_version: client.instance_version.to_s
+ }
+ }
+ end
+
+ def client
+ @client ||= BulkImports::Clients::HTTP.new(
+ url: @credentials[:url],
+ token: @credentials[:access_token],
+ per_page: @params[:per_page],
+ page: @params[:page]
+ )
+ end
+ end
+end
diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml
index aeb274fe2cb..6a5f07dd2db 100644
--- a/app/views/admin/users/_access_levels.html.haml
+++ b/app/views/admin/users/_access_levels.html.haml
@@ -19,22 +19,20 @@
.col-sm-10
- editing_current_user = (current_user == @user)
- = f.radio_button :access_level, :regular, disabled: editing_current_user
- = f.label :access_level_regular, class: 'font-weight-bold' do
- = s_('AdminUsers|Regular')
- %p.light
- = s_('AdminUsers|Regular users have access to their groups and projects')
+ = f.gitlab_ui_radio_component :access_level, :regular,
+ s_('AdminUsers|Regular'),
+ radio_options: { disabled: editing_current_user },
+ help_text: s_('AdminUsers|Regular users have access to their groups and projects.')
= render_if_exists 'admin/users/auditor_access_level_radio', f: f, disabled: editing_current_user
- = f.radio_button :access_level, :admin, disabled: editing_current_user
- = f.label :access_level_admin, class: 'font-weight-bold' do
- = s_('AdminUsers|Admin')
- %p.light
- = s_('AdminUsers|Administrators have access to all groups, projects and users and can manage all features in this installation')
- - if editing_current_user
- %p.light
- = s_('AdminUsers|You cannot remove your own admin rights.')
+ - help_text = s_('AdminUsers|Administrators have access to all groups, projects and users and can manage all features in this installation.')
+ - help_text += ' ' + s_('AdminUsers|You cannot remove your own admin rights.') if editing_current_user
+ = f.gitlab_ui_radio_component :access_level, :admin,
+ s_('AdminUsers|Admin'),
+ radio_options: { disabled: editing_current_user },
+ help_text: help_text
+
.form-group.row
.col-sm-2.col-form-label.gl-pt-0
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index 9d62c19e2fc..96fd0f5aac1 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -1,5 +1,5 @@
.user_new
- = form_for [:admin, @user], html: { class: 'fieldset-form' } do |f|
+ = gitlab_ui_form_for [:admin, @user], html: { class: 'fieldset-form' } do |f|
= form_errors(@user)
%fieldset
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index ec2904245d3..dff1b5e3d04 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -17,6 +17,7 @@
= render_two_factor_auth_recovery_settings_check
= render_if_exists "layouts/header/ee_subscribable_banner"
= render_if_exists "shared/namespace_storage_limit_alert"
+ = render_if_exists "shared/namespace_user_cap_reached_alert"
= render_if_exists "shared/new_user_signups_cap_reached_alert"
= yield :page_level_alert
= yield :customize_homepage_banner
diff --git a/app/views/projects/ci/pipeline_editor/show.html.haml b/app/views/projects/ci/pipeline_editor/show.html.haml
index 674765e9f89..ce6f7553ab4 100644
--- a/app/views/projects/ci/pipeline_editor/show.html.haml
+++ b/app/views/projects/ci/pipeline_editor/show.html.haml
@@ -1,3 +1,5 @@
+- add_page_specific_style 'page_bundles/pipelines'
+
- page_title s_('Pipelines|Pipeline Editor')
- content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco')