diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-26 12:09:53 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-26 12:09:53 +0300 |
commit | 0ccabeb3f62c5fbc81f52cc16fa654404bb87874 (patch) | |
tree | 27c81cfa9d498fa0b604acaa9c4f5400743f83fd /app | |
parent | 6819cb95c9c0aa63fce1d246026978df5cac9e44 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
20 files changed, 395 insertions, 223 deletions
diff --git a/app/assets/javascripts/actioncable_link.js b/app/assets/javascripts/actioncable_link.js new file mode 100644 index 00000000000..4c642db2f3c --- /dev/null +++ b/app/assets/javascripts/actioncable_link.js @@ -0,0 +1,40 @@ +import { ApolloLink, Observable } from 'apollo-link'; +import { print } from 'graphql'; +import cable from '~/actioncable_consumer'; +import { uuids } from '~/diffs/utils/uuids'; + +export default class ActionCableLink extends ApolloLink { + // eslint-disable-next-line class-methods-use-this + request(operation) { + return new Observable((observer) => { + const subscription = cable.subscriptions.create( + { + channel: 'GraphqlChannel', + query: operation.query ? print(operation.query) : null, + variables: operation.variables, + operationName: operation.operationName, + nonce: uuids()[0], + }, + { + received(data) { + if (data.errors) { + observer.error(data.errors); + } else if (data.result) { + observer.next(data.result); + } + + if (!data.more) { + observer.complete(); + } + }, + }, + ); + + return { + unsubscribe() { + subscription.unsubscribe(); + }, + }; + }); + } +} diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 22f88b1caa7..ee8384f734d 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -238,10 +238,13 @@ class GfmAutoComplete { const MEMBER_COMMAND = { ASSIGN: '/assign', UNASSIGN: '/unassign', + ASSIGN_REVIEWER: '/assign_reviewer', + UNASSIGN_REVIEWER: '/unassign_reviewer', REASSIGN: '/reassign', CC: '/cc', }; let assignees = []; + let reviewers = []; let command = ''; // Team Members @@ -286,9 +289,11 @@ class GfmAutoComplete { return null; }); - // Cache assignees list for easier filtering later + // Cache assignees & reviewers list for easier filtering later assignees = SidebarMediator.singleton?.store?.assignees?.map(createMemberSearchString) || []; + reviewers = + SidebarMediator.singleton?.store?.reviewers?.map(createMemberSearchString) || []; const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers); return match && match.length ? match[1] : null; @@ -309,6 +314,12 @@ class GfmAutoComplete { } else if (command === MEMBER_COMMAND.UNASSIGN) { // Only include members which are assigned to Issuable currently return data.filter((member) => assignees.includes(member.search)); + } else if (command === MEMBER_COMMAND.ASSIGN_REVIEWER) { + // Only include members which are not assigned as a reviewer to Issuable currently + return data.filter((member) => !reviewers.includes(member.search)); + } else if (command === MEMBER_COMMAND.UNASSIGN_REVIEWER) { + // Only include members which are not assigned as a reviewer to Issuable currently + return data.filter((member) => reviewers.includes(member.search)); } return data; diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js index 1630f0d689c..cec689a44ca 100644 --- a/app/assets/javascripts/lib/graphql.js +++ b/app/assets/javascripts/lib/graphql.js @@ -4,6 +4,7 @@ import { ApolloLink } from 'apollo-link'; import { BatchHttpLink } from 'apollo-link-batch-http'; import { createHttpLink } from 'apollo-link-http'; import { createUploadLink } from 'apollo-upload-client'; +import ActionCableLink from '~/actioncable_link'; import { apolloCaptchaLink } from '~/captcha/apollo_captcha_link'; import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link'; import csrf from '~/lib/utils/csrf'; @@ -83,15 +84,27 @@ export default (resolvers = {}, config = {}) => { }); }); - return new ApolloClient({ - typeDefs, - link: ApolloLink.from([ + const hasSubscriptionOperation = ({ query: { definitions } }) => { + return definitions.some( + ({ kind, operation }) => kind === 'OperationDefinition' && operation === 'subscription', + ); + }; + + const appLink = ApolloLink.split( + hasSubscriptionOperation, + new ActionCableLink(), + ApolloLink.from([ requestCounterLink, performanceBarLink, new StartupJSLink(), apolloCaptchaLink, uploadsLink, ]), + ); + + return new ApolloClient({ + typeDefs, + link: appLink, cache: new InMemoryCache({ ...cacheConfig, freezeResults: assumeImmutableResults, diff --git a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue new file mode 100644 index 00000000000..091b202e10b --- /dev/null +++ b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue @@ -0,0 +1,155 @@ +<script> +import { GlAlert } from '@gitlab/ui'; +import { getParameterValues, removeParams } from '~/lib/utils/url_utility'; +import { __, s__ } from '~/locale'; +import { + COMMIT_FAILURE, + COMMIT_SUCCESS, + DEFAULT_FAILURE, + DEFAULT_SUCCESS, + LOAD_FAILURE_UNKNOWN, +} from '../../constants'; +import CodeSnippetAlert from '../code_snippet_alert/code_snippet_alert.vue'; +import { + CODE_SNIPPET_SOURCE_URL_PARAM, + CODE_SNIPPET_SOURCES, +} from '../code_snippet_alert/constants'; + +export default { + components: { + GlAlert, + CodeSnippetAlert, + }, + errorTexts: { + [COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'), + [DEFAULT_FAILURE]: __('Something went wrong on our end.'), + [LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'), + }, + successTexts: { + [COMMIT_SUCCESS]: __('Your changes have been successfully committed.'), + [DEFAULT_SUCCESS]: __('Your action succeeded.'), + }, + props: { + failureType: { + type: String, + required: false, + default: null, + }, + failureReasons: { + type: Array, + required: false, + default: () => [], + }, + showFailure: { + type: Boolean, + required: false, + default: false, + }, + showSuccess: { + type: Boolean, + required: false, + default: false, + }, + successType: { + type: String, + required: false, + default: null, + }, + }, + data() { + return { + codeSnippetCopiedFrom: '', + }; + }, + computed: { + failure() { + switch (this.failureType) { + 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', + }; + } + }, + success() { + switch (this.successType) { + case COMMIT_SUCCESS: + return { + text: this.$options.successTexts[COMMIT_SUCCESS], + variant: 'info', + }; + default: + return { + text: this.$options.successTexts[DEFAULT_SUCCESS], + variant: 'info', + }; + } + }, + }, + created() { + this.parseCodeSnippetSourceParam(); + }, + methods: { + dismissCodeSnippetAlert() { + this.codeSnippetCopiedFrom = ''; + }, + dismissFailure() { + this.$emit('hide-failure'); + }, + dismissSuccess() { + this.$emit('hide-success'); + }, + parseCodeSnippetSourceParam() { + const [codeSnippetCopiedFrom] = getParameterValues(CODE_SNIPPET_SOURCE_URL_PARAM); + if (codeSnippetCopiedFrom && CODE_SNIPPET_SOURCES.includes(codeSnippetCopiedFrom)) { + this.codeSnippetCopiedFrom = codeSnippetCopiedFrom; + window.history.replaceState( + {}, + document.title, + removeParams([CODE_SNIPPET_SOURCE_URL_PARAM]), + ); + } + }, + }, +}; +</script> + +<template> + <div> + <code-snippet-alert + v-if="codeSnippetCopiedFrom" + :source="codeSnippetCopiedFrom" + class="gl-mb-5" + @dismiss="dismissCodeSnippetAlert" + /> + <gl-alert + v-if="showSuccess" + :variant="success.variant" + class="gl-mb-5" + @dismiss="dismissSuccess" + > + {{ success.text }} + </gl-alert> + <gl-alert + v-if="showFailure" + :variant="failure.variant" + class="gl-mb-5" + @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> +</template> diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js index 8d0ec6c3e2d..56862f17858 100644 --- a/app/assets/javascripts/pipeline_editor/constants.js +++ b/app/assets/javascripts/pipeline_editor/constants.js @@ -14,6 +14,7 @@ export const COMMIT_FAILURE = 'COMMIT_FAILURE'; export const COMMIT_SUCCESS = 'COMMIT_SUCCESS'; export const DEFAULT_FAILURE = 'DEFAULT_FAILURE'; +export const DEFAULT_SUCCESS = 'DEFAULT_SUCCESS'; export const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN'; export const CREATE_TAB = 'CREATE_TAB'; diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue index 5f9662e90e6..bf36e00c662 100644 --- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue +++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue @@ -1,22 +1,15 @@ <script> -import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon } from '@gitlab/ui'; import { fetchPolicies } from '~/lib/graphql'; import httpStatusCodes from '~/lib/utils/http_status'; -import { getParameterValues, removeParams } from '~/lib/utils/url_utility'; -import { __, s__ } from '~/locale'; +import { s__ } from '~/locale'; import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils'; -import CodeSnippetAlert from './components/code_snippet_alert/code_snippet_alert.vue'; -import { - CODE_SNIPPET_SOURCE_URL_PARAM, - CODE_SNIPPET_SOURCES, -} from './components/code_snippet_alert/constants'; + import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes_dialog.vue'; import PipelineEditorEmptyState from './components/ui/pipeline_editor_empty_state.vue'; +import PipelineEditorMessages from './components/ui/pipeline_editor_messages.vue'; import { - COMMIT_FAILURE, - COMMIT_SUCCESS, - DEFAULT_FAILURE, EDITOR_APP_STATUS_EMPTY, EDITOR_APP_STATUS_ERROR, EDITOR_APP_STATUS_LOADING, @@ -32,11 +25,10 @@ import PipelineEditorHome from './pipeline_editor_home.vue'; export default { components: { ConfirmUnsavedChangesDialog, - GlAlert, GlLoadingIcon, PipelineEditorEmptyState, PipelineEditorHome, - CodeSnippetAlert, + PipelineEditorMessages, }, inject: { ciConfigPath: { @@ -51,15 +43,14 @@ export default { ciConfigData: {}, failureType: null, failureReasons: [], - showStartScreen: false, initialCiFileContent: '', isNewCiConfigFile: false, lastCommittedContent: '', currentCiFileContent: '', - showFailureAlert: false, - showSuccessAlert: false, successType: null, - codeSnippetCopiedFrom: '', + showStartScreen: false, + showSuccess: false, + showFailure: false, }; }, @@ -152,50 +143,12 @@ export default { isEmpty() { return this.currentCiFileContent === ''; }, - failure() { - switch (this.failureType) { - 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', - }; - } - }, - success() { - switch (this.successType) { - case COMMIT_SUCCESS: - return { - text: this.$options.successTexts[COMMIT_SUCCESS], - variant: 'info', - }; - default: - return null; - } - }, }, i18n: { tabEdit: s__('Pipelines|Write pipeline configuration'), tabGraph: s__('Pipelines|Visualize'), tabLint: s__('Pipelines|Lint'), }, - errorTexts: { - [COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'), - [DEFAULT_FAILURE]: __('Something went wrong on our end.'), - [LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'), - }, - successTexts: { - [COMMIT_SUCCESS]: __('Your changes have been successfully committed.'), - }, watch: { isEmpty(flag) { if (flag) { @@ -203,9 +156,6 @@ export default { } }, }, - created() { - this.parseCodeSnippetSourceParam(); - }, methods: { handleBlobContentError(error = {}) { const { networkError } = error; @@ -223,12 +173,11 @@ export default { this.reportFailure(LOAD_FAILURE_UNKNOWN); } }, - - dismissFailure() { - this.showFailureAlert = false; + hideFailure() { + this.showFailure = false; }, - dismissSuccess() { - this.showSuccessAlert = false; + hideSuccess() { + this.showSuccess = false; }, async refetchContent() { this.$apollo.queries.initialCiFileContent.skip = false; @@ -238,13 +187,13 @@ export default { this.setAppStatus(EDITOR_APP_STATUS_ERROR); window.scrollTo({ top: 0, behavior: 'smooth' }); - this.showFailureAlert = true; + this.showFailure = true; this.failureType = type; this.failureReasons = reasons; }, reportSuccess(type) { window.scrollTo({ top: 0, behavior: 'smooth' }); - this.showSuccessAlert = true; + this.showSuccess = true; this.successType = type; }, resetContent() { @@ -277,20 +226,6 @@ export default { // if the user has made changes to the file that are unsaved. this.lastCommittedContent = this.currentCiFileContent; }, - parseCodeSnippetSourceParam() { - const [codeSnippetCopiedFrom] = getParameterValues(CODE_SNIPPET_SOURCE_URL_PARAM); - if (codeSnippetCopiedFrom && CODE_SNIPPET_SOURCES.includes(codeSnippetCopiedFrom)) { - this.codeSnippetCopiedFrom = codeSnippetCopiedFrom; - window.history.replaceState( - {}, - document.title, - removeParams([CODE_SNIPPET_SOURCE_URL_PARAM]), - ); - } - }, - dismissCodeSnippetAlert() { - this.codeSnippetCopiedFrom = ''; - }, }, }; </script> @@ -303,31 +238,15 @@ export default { @createEmptyConfigFile="setNewEmptyCiConfigFile" /> <div v-else> - <code-snippet-alert - v-if="codeSnippetCopiedFrom" - :source="codeSnippetCopiedFrom" - class="gl-mb-5" - @dismiss="dismissCodeSnippetAlert" + <pipeline-editor-messages + :failure-type="failureType" + :failure-reasons="failureReasons" + :show-failure="showFailure" + :show-success="showSuccess" + :success-type="successType" + @hide-success="hideSuccess" + @hide-failure="hideFailure" /> - <gl-alert - v-if="showSuccessAlert" - :variant="success.variant" - class="gl-mb-5" - @dismiss="dismissSuccess" - > - {{ success.text }} - </gl-alert> - <gl-alert - v-if="showFailureAlert" - :variant="failure.variant" - class="gl-mb-5" - @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> <pipeline-editor-home :ci-config-data="ciConfigData" :ci-file-content="currentCiFileContent" diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue index f98798582c1..e7ef731eed8 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue @@ -1,6 +1,7 @@ <script> -import actionCable from '~/actioncable_consumer'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import produce from 'immer'; +import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { IssuableType } from '~/issue_show/constants'; import { assigneesQueries } from '~/sidebar/constants'; export default { @@ -12,60 +13,62 @@ export default { required: false, default: null, }, - issuableIid: { + issuableType: { type: String, required: true, }, - projectPath: { - type: String, + issuableId: { + type: Number, required: true, }, - issuableType: { - type: String, + queryVariables: { + type: Object, required: true, }, }, + computed: { + issuableClass() { + return Object.keys(IssuableType).find((key) => IssuableType[key] === this.issuableType); + }, + }, apollo: { - workspace: { + issuable: { query() { return assigneesQueries[this.issuableType].query; }, variables() { - return { - iid: this.issuableIid, - fullPath: this.projectPath, - }; + return this.queryVariables; + }, + update(data) { + return data.workspace?.issuable; }, - result(data) { - if (this.mediator) { - this.handleFetchResult(data); - } + subscribeToMore: { + document() { + return assigneesQueries[this.issuableType].subscription; + }, + variables() { + return { + issuableId: convertToGraphQLId(this.issuableClass, this.issuableId), + }; + }, + updateQuery(prev, { subscriptionData }) { + if (prev && subscriptionData?.data?.issuableAssigneesUpdated) { + const data = produce(prev, (draftData) => { + draftData.workspace.issuable.assignees.nodes = + subscriptionData.data.issuableAssigneesUpdated.assignees.nodes; + }); + if (this.mediator) { + this.handleFetchResult(data); + } + return data; + } + return prev; + }, }, }, }, - mounted() { - this.initActionCablePolling(); - }, - beforeDestroy() { - this.$options.subscription.unsubscribe(); - }, methods: { - received(data) { - if (data.event === 'updated') { - this.$apollo.queries.workspace.refetch(); - } - }, - initActionCablePolling() { - this.$options.subscription = actionCable.subscriptions.create( - { - channel: 'IssuesChannel', - project_path: this.projectPath, - iid: this.issuableIid, - }, - { received: this.received }, - ); - }, - handleFetchResult({ data }) { + handleFetchResult(data) { const { nodes } = data.workspace.issuable.assignees; const assignees = nodes.map((n) => ({ diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index e15ea595190..ca95599742a 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -44,6 +44,10 @@ export default { type: String, required: true, }, + issuableId: { + type: Number, + required: true, + }, assigneeAvailabilityStatus: { type: Object, required: false, @@ -61,6 +65,12 @@ export default { // Note: Realtime is only available on issues right now, future support for MR wil be built later. return this.glFeatures.realTimeIssueSidebar && this.issuableType === 'issue'; }, + queryVariables() { + return { + iid: this.issuableIid, + fullPath: this.projectPath, + }; + }, relativeUrlRoot() { return gon.relative_url_root ?? ''; }, @@ -121,9 +131,9 @@ export default { <div> <assignees-realtime v-if="shouldEnableRealtime" - :issuable-iid="issuableIid" - :project-path="projectPath" :issuable-type="issuableType" + :issuable-id="issuableId" + :query-variables="queryVariables" :mediator="mediator" /> <assignee-title diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue index 78cac989850..2fc25151d1c 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue @@ -73,6 +73,11 @@ export default { return [IssuableType.Issue, IssuableType.MergeRequest].includes(value); }, }, + issuableId: { + type: Number, + required: false, + default: null, + }, multipleAssignees: { type: Boolean, required: false, @@ -340,9 +345,9 @@ export default { <div data-testid="assignees-widget"> <sidebar-assignees-realtime v-if="shouldEnableRealtime" - :project-path="fullPath" - :issuable-iid="iid" :issuable-type="issuableType" + :issuable-id="issuableId" + :query-variables="queryVariables" /> <sidebar-editable-item ref="toggle" diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js index 80e07d556bf..58e4b0348ca 100644 --- a/app/assets/javascripts/sidebar/constants.js +++ b/app/assets/javascripts/sidebar/constants.js @@ -1,5 +1,6 @@ import { IssuableType } from '~/issue_show/constants'; import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql'; +import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql'; import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql'; import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql'; import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql'; @@ -17,6 +18,7 @@ export const ASSIGNEES_DEBOUNCE_DELAY = 250; export const assigneesQueries = { [IssuableType.Issue]: { query: getIssueParticipants, + subscription: issuableAssigneesSubscription, mutation: updateAssigneesMutation, }, [IssuableType.MergeRequest]: { diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 1304e84814b..52a1efa04e4 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -53,7 +53,7 @@ function mountAssigneesComponentDeprecated(mediator) { if (!el) return; - const { iid, fullPath } = getSidebarOptions(); + const { id, iid, fullPath } = getSidebarOptions(); const assigneeAvailabilityStatus = getSidebarAssigneeAvailabilityData(); // eslint-disable-next-line no-new new Vue({ @@ -74,6 +74,7 @@ function mountAssigneesComponentDeprecated(mediator) { isInIssuePage() || isInIncidentPage() || isInDesignPage() ? IssuableType.Issue : IssuableType.MergeRequest, + issuableId: id, assigneeAvailabilityStatus, }, }), @@ -85,7 +86,7 @@ function mountAssigneesComponent() { if (!el) return; - const { iid, fullPath, editable, projectMembersPath } = getSidebarOptions(); + const { id, iid, fullPath, editable, projectMembersPath } = getSidebarOptions(); // eslint-disable-next-line no-new new Vue({ el, @@ -108,6 +109,7 @@ function mountAssigneesComponent() { isInIssuePage() || isInIncidentPage() || isInDesignPage() ? IssuableType.Issue : IssuableType.MergeRequest, + issuableId: id, multipleAssignees: !el.dataset.maxAssignees, }, scopedSlots: { diff --git a/app/assets/javascripts/sidebar/queries/issuable_assignees.subscription.graphql b/app/assets/javascripts/sidebar/queries/issuable_assignees.subscription.graphql new file mode 100644 index 00000000000..47ce094418c --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/issuable_assignees.subscription.graphql @@ -0,0 +1,16 @@ +#import "~/graphql_shared/fragments/user.fragment.graphql" + +subscription issuableAssigneesUpdated($issuableId: IssuableID!) { + issuableAssigneesUpdated(issuableId: $issuableId) { + ... on Issue { + assignees { + nodes { + ...User + status { + availability + } + } + } + } + } +} diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index c6fde38579b..e5ea2920eaa 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -386,6 +386,7 @@ module IssuablesHelper rootPath: root_path, fullPath: issuable[:project_full_path], iid: issuable[:iid], + id: issuable[:id], severity: issuable[:severity], timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours, createNoteEmail: issuable[:create_note_email], diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index ffa09cb12fb..0711a6b7e97 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -153,9 +153,9 @@ module ServicesHelper private def integration_level(integration) - if integration.instance + if integration.instance_level? 'instance' - elsif integration.group_id + elsif integration.group_level? 'group' else 'project' diff --git a/app/models/service.rb b/app/models/service.rb index 7782b016b52..51ac4555c1f 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -51,14 +51,14 @@ class Service < ApplicationRecord belongs_to :group, inverse_of: :services has_one :service_hook - validates :project_id, presence: true, unless: -> { template? || instance? || group_id } - validates :group_id, presence: true, unless: -> { template? || instance? || project_id } - validates :project_id, :group_id, absence: true, if: -> { template? || instance? } + validates :project_id, presence: true, unless: -> { template? || instance_level? || group_level? } + validates :group_id, presence: true, unless: -> { template? || instance_level? || project_level? } + validates :project_id, :group_id, absence: true, if: -> { template? || instance_level? } validates :type, presence: true validates :type, uniqueness: { scope: :template }, if: :template? - validates :type, uniqueness: { scope: :instance }, if: :instance? - validates :type, uniqueness: { scope: :project_id }, if: :project_id? - validates :type, uniqueness: { scope: :group_id }, if: :group_id? + validates :type, uniqueness: { scope: :instance }, if: :instance_level? + validates :type, uniqueness: { scope: :project_id }, if: :project_level? + validates :type, uniqueness: { scope: :group_id }, if: :group_level? validate :validate_is_instance_or_template validate :validate_belongs_to_project_or_group @@ -240,7 +240,7 @@ class Service < ApplicationRecord service.instance = false service.project_id = project_id service.group_id = group_id - service.inherit_from_id = integration.id if integration.instance? || integration.group + service.inherit_from_id = integration.id if integration.instance_level? || integration.group_level? service end @@ -409,7 +409,7 @@ class Service < ApplicationRecord # Disable test for instance-level and group-level services. # https://gitlab.com/gitlab-org/gitlab/-/issues/213138 def can_test? - !instance? && !group_id + !(instance_level? || group_level?) end def project_level? @@ -460,11 +460,11 @@ class Service < ApplicationRecord private def validate_is_instance_or_template - errors.add(:template, 'The service should be a service template or instance-level integration') if template? && instance? + errors.add(:template, 'The service should be a service template or instance-level integration') if template? && instance_level? end def validate_belongs_to_project_or_group - errors.add(:project_id, 'The service cannot belong to both a project and a group') if project_id && group_id + errors.add(:project_id, 'The service cannot belong to both a project and a group') if project_level? && group_level? end def validate_recipients? diff --git a/app/services/admin/propagate_integration_service.rb b/app/services/admin/propagate_integration_service.rb index 253c3a84fef..2ce1756ef1a 100644 --- a/app/services/admin/propagate_integration_service.rb +++ b/app/services/admin/propagate_integration_service.rb @@ -5,7 +5,7 @@ module Admin include PropagateService def propagate - if integration.instance? + if integration.instance_level? update_inherited_integrations create_integration_for_groups_without_integration create_integration_for_projects_without_integration diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index c7ec3ab66d7..db5f58f8bd0 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -12,7 +12,7 @@ %li = image_tag avatar_icon_for_user(@user, 60), class: "avatar s60" %li - %span.light Profile page: + %span.light= _('Profile page:') %strong = link_to user_path(@user) do = @user.username @@ -20,25 +20,25 @@ .card .card-header - Account: + = _('Account:') %ul.content-list %li - %span.light Name: + %span.light= _('Name:') %strong= @user.name %li - %span.light Username: + %span.light= _('Username:') %strong = @user.username %li - %span.light Email: + %span.light= _('Email:') %strong = render partial: 'shared/email_with_badge', locals: { email: mail_to(@user.email), verified: @user.confirmed? } - @user.emails.each do |email| %li - %span.light Secondary email: + %span.light= _('Secondary email:') %strong = render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? } - = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon float-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do + = link_to remove_email_admin_user_path(@user, email), data: { confirm: _("Are you sure you want to remove %{email}?") % { email: email.email } }, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon float-right", title: _('Remove secondary email'), id: "remove_email_#{email.id}" do = sprite_icon('close', size: 16, css_class: 'gl-icon') %li %span.light ID: @@ -50,65 +50,65 @@ = @user.namespace_id %li.two-factor-status - %span.light Two-factor Authentication: + %span.light= _('Two-factor Authentication:') %strong{ class: @user.two_factor_enabled? ? 'cgreen' : 'cred' } - if @user.two_factor_enabled? - Enabled - = link_to 'Disable', disable_two_factor_admin_user_path(@user), data: {confirm: 'Are you sure?'}, method: :patch, class: 'btn gl-button btn-sm btn-danger float-right', title: 'Disable Two-factor Authentication' + = _('Enabled') + = link_to _('Disable'), disable_two_factor_admin_user_path(@user), data: { confirm: _('Are you sure?') }, method: :patch, class: 'btn gl-button btn-sm btn-danger float-right', title: _('Disable Two-factor Authentication') - else - Disabled + = _('Disabled') = render_if_exists 'admin/namespace_plan_info', namespace: @user.namespace %li - %span.light External User: + %span.light= _('External User:') %strong - = @user.external? ? "Yes" : "No" + = @user.external? ? _('Yes') : _('No') %li - %span.light Can create groups: + %span.light= _('Can create groups:') %strong - = @user.can_create_group ? "Yes" : "No" + = @user.can_create_group ? _('Yes') : _('No') %li - %span.light Personal projects limit: + %span.light= _('Personal projects limit:') %strong = @user.projects_limit %li - %span.light Member since: + %span.light= _('Member since:') %strong = @user.created_at.to_s(:medium) - if @user.confirmed_at %li - %span.light Confirmed at: + %span.light= _('Confirmed at:') %strong = @user.confirmed_at.to_s(:medium) - else %li - %span.light Confirmed: + %span.ligh= _('Confirmed:') %strong.cred - No + = _('No') %li - %span.light Current sign-in IP: + %span.light= _('Current sign-in IP:') %strong = @user.current_sign_in_ip || _('never') %li - %span.light Current sign-in at: + %span.light= _('Current sign-in at:') %strong = @user.current_sign_in_at&.to_s(:medium) || _('never') %li - %span.light Last sign-in IP: + %span.light= _('Last sign-in IP:') %strong = @user.last_sign_in_ip || _('never') %li - %span.light Last sign-in at: + %span.light= _('Last sign-in at:') %strong = @user.last_sign_in_at&.to_s(:medium) || _('never') %li - %span.light Sign-in count: + %span.light= _('Sign-in count:') %strong = @user.sign_in_count @@ -121,13 +121,13 @@ - if @user.ldap_user? %li - %span.light LDAP uid: + %span.light= _('LDAP uid:') %strong = @user.ldap_identity.extern_uid - if @user.created_by %li - %span.light Created by: + %span.light= _('Created by:') %strong = link_to @user.created_by.name, [:admin, @user.created_by] @@ -140,13 +140,13 @@ - if can_force_email_confirmation?(@user) .gl-card.border-info.gl-mb-5 .gl-card-header.bg-info.text-white - Confirm user + = _('Confirm user') .gl-card-body - if @user.unconfirmed_email.present? - email = " (#{@user.unconfirmed_email})" - %p This user has an unconfirmed email address#{email}. You may force a confirmation. + %p= _('This user has an unconfirmed email address %{email}. You may force a confirmation.') % { email: email } %br - = link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?', qa_selector: 'confirm_user_button' } + = link_to _('Confirm user'), confirm_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: _('Are you sure?'), qa_selector: 'confirm_user_button' } = render 'admin/users/user_detail_note' @@ -154,7 +154,7 @@ - if @user.deactivated? .gl-card.border-info.gl-mb-5 .gl-card-header.bg-info.text-white - Reactivate this user + = _('Reactivate this user') .gl-card-body = render partial: 'admin/users/user_activation_effects' %br @@ -163,7 +163,7 @@ - elsif @user.can_be_deactivated? .gl-card.border-warning.gl-mb-5 .gl-card-header.bg-warning.text-white - Deactivate this user + = _('Deactivate this user') .gl-card-body = user_deactivation_effects %br @@ -176,12 +176,12 @@ - else .gl-card.border-info.gl-mb-5 .gl-card-header.gl-bg-blue-500.gl-text-white - This user is blocked + = _('This user is blocked') .gl-card-body - %p A blocked user cannot: + %p= _('A blocked user cannot:') %ul - %li Log in - %li Access Git repositories + %li= _('Log in') + %li= _('Access Git repositories') %br %button.btn.gl-button.btn-info.js-confirm-modal-button{ data: user_unblock_data(@user) } = s_('AdminUsers|Unblock user') @@ -191,18 +191,18 @@ - if @user.access_locked? .card.border-info.gl-mb-5 .card-header.bg-info.text-white - This account has been locked + = _('This account has been locked') .card-body - %p This user has been temporarily locked due to excessive number of failed logins. You may manually unlock the account. + %p= _('This user has been temporarily locked due to excessive number of failed logins. You may manually unlock the account.') %br - = link_to 'Unlock user', unlock_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?' } + = link_to _('Unlock user'), unlock_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: _('Are you sure?') } - if !@user.blocked_pending_approval? .gl-card.border-danger.gl-mb-5 .gl-card-header.bg-danger.text-white = s_('AdminUsers|Delete user') .gl-card-body - if @user.can_be_removed? && can?(current_user, :destroy_user, @user) - %p Deleting a user has the following effects: + %p= _('Deleting a user has the following effects:') = render 'users/deletion_guidance', user: @user %br %button.js-delete-user-modal-button.btn.gl-button.btn-danger{ data: { 'gl-modal-action': 'delete', @@ -213,13 +213,13 @@ - else - if @user.solo_owned_groups.present? %p - This user is currently an owner in these groups: + = _('This user is currently an owner in these groups:') %strong= @user.solo_owned_groups.map(&:name).join(', ') %p - You must transfer ownership or delete these groups before you can delete this user. + = _('You must transfer ownership or delete these groups before you can delete this user.') - else %p - You don't have access to delete this user. + = _("You don't have access to delete this user.") .gl-card.border-danger .gl-card-header.bg-danger.text-white @@ -227,13 +227,8 @@ .gl-card-body - if can?(current_user, :destroy_user, @user) %p - This option deletes the user and any contributions that - would usually be moved to the - = succeed "." do - = link_to "system ghost user", help_page_path("user/profile/account/delete_account") - As well as the user's personal projects, groups owned solely by - the user, and projects in them, will also be removed. Commits - to other projects are unaffected. + - link_to_ghost_user = link_to(_("system ghost user"), help_page_path("user/profile/account/delete_account")) + = _("This option deletes the user and any contributions that would usually be moved to the %{link_to_ghost_user}. As well as the user's personal projects, groups owned solely by the user, and projects in them, will also be removed. Commits to other projects are unaffected.").html_safe % { link_to_ghost_user: link_to_ghost_user } %br %button.js-delete-user-modal-button.btn.gl-button.btn-danger{ data: { 'gl-modal-action': 'delete-with-contributions', delete_user_url: admin_user_path(@user, hard_delete: true), @@ -242,6 +237,6 @@ = s_('AdminUsers|Delete user and contributions') - else %p - You don't have access to delete this user. + = _("You don't have access to delete this user.") = render partial: 'admin/users/modals' diff --git a/app/views/projects/settings/access_tokens/index.html.haml b/app/views/projects/settings/access_tokens/index.html.haml index 01f3e441eef..1bf252b6282 100644 --- a/app/views/projects/settings/access_tokens/index.html.haml +++ b/app/views/projects/settings/access_tokens/index.html.haml @@ -9,13 +9,13 @@ %h4.gl-mt-0 = page_title %p + - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/project_access_tokens') } - if current_user.can?(:create_resource_access_tokens, @project) - = _('You can generate an access token scoped to this project for each application to use the GitLab API.') - -# Commented out until https://gitlab.com/gitlab-org/gitlab/-/issues/219551 is fixed - -# %p - -# = _('You can also use project access tokens to authenticate against Git over HTTP.') + = _('Generate project access tokens scoped to this project for your applications that need access to the GitLab API.') + %p + = _('You can also use project access tokens with Git to authenticate over HTTP(S). %{link_start}Learn more.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } - else - = _('Project access token creation is disabled in this group. You can still use and manage existing tokens.') + = _('Project access token creation is disabled in this group. You can still use and manage existing tokens. %{link_start}Learn more.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } %p - root_group = @project.group.root_ancestor - if current_user.can?(:admin_group, root_group) @@ -23,7 +23,6 @@ - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: group_settings_link } = _('You can enable project access token creation in %{link_start}group settings%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } - .col-lg-8 - if @new_project_access_token = render 'shared/access_tokens/created_container', diff --git a/app/workers/propagate_integration_group_worker.rb b/app/workers/propagate_integration_group_worker.rb index 01155753877..f763406b380 100644 --- a/app/workers/propagate_integration_group_worker.rb +++ b/app/workers/propagate_integration_group_worker.rb @@ -11,7 +11,7 @@ class PropagateIntegrationGroupWorker integration = Service.find_by_id(integration_id) return unless integration - batch = if integration.instance? + batch = if integration.instance_level? Group.where(id: min_id..max_id).without_integration(integration) else integration.group.descendants.where(id: min_id..max_id).without_integration(integration) diff --git a/app/workers/propagate_integration_project_worker.rb b/app/workers/propagate_integration_project_worker.rb index 188d81e5fc1..50c68c85f03 100644 --- a/app/workers/propagate_integration_project_worker.rb +++ b/app/workers/propagate_integration_project_worker.rb @@ -12,7 +12,7 @@ class PropagateIntegrationProjectWorker return unless integration batch = Project.where(id: min_id..max_id).without_integration(integration) - batch = batch.in_namespace(integration.group.self_and_descendants) if integration.group_id + batch = batch.in_namespace(integration.group.self_and_descendants) if integration.group_level? return if batch.empty? |