Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1e73f4d9e2
commit
e3eb4d55ef
|
|
@ -289,10 +289,6 @@
|
|||
- "tooling/docs/**/*"
|
||||
- "lib/tasks/gitlab/docs/compile_deprecations.rake"
|
||||
|
||||
.bundler-patterns: &bundler-patterns
|
||||
- '{Gemfile.lock,*/Gemfile.lock,*/*/Gemfile.lock}'
|
||||
- '{Gemfile.next.lock,*/Gemfile.next.lock,*/*/Gemfile.next.lock}'
|
||||
|
||||
.nodejs-patterns: &nodejs-patterns
|
||||
- '{package.json,*/package.json,*/*/package.json}'
|
||||
- '{yarn.lock,*/yarn.lock,*/*/yarn.lock}'
|
||||
|
|
@ -463,12 +459,6 @@
|
|||
.frontend-predictive-patterns: &frontend-predictive-patterns
|
||||
- "{,ee/,jh/}{app/assets/javascripts,spec/frontend}/**/*"
|
||||
|
||||
# Frontend view patterns + .qa-patterns
|
||||
.frontend-qa-patterns: &frontend-qa-patterns
|
||||
- "{,ee/,jh/}{app/assets,app/components,app/helpers,app/presenters,app/views}/**/*"
|
||||
# QA changes
|
||||
- "{,jh/}qa/**/*"
|
||||
|
||||
# Code patterns + .ci-patterns
|
||||
.code-patterns: &code-patterns
|
||||
- ".{eslintrc.yml,eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
|
||||
|
|
@ -679,13 +669,6 @@
|
|||
- "{,jh/}Gemfile.next{,.lock}"
|
||||
- "{,ee/,jh/}config/**/*.rb"
|
||||
|
||||
.core-frontend-patterns: &core-frontend-patterns
|
||||
- "{package.json,yarn.lock}"
|
||||
- "babel.config.js"
|
||||
- "jest.config.{base,integration,unit}.js"
|
||||
- "config/helpers/**/*.js"
|
||||
- "vendor/assets/javascripts/**/*"
|
||||
|
||||
.feature-flag-development-config-patterns: &feature-flag-development-config-patterns
|
||||
- "{,ee/,jh/}config/feature_flags/**/*.yml"
|
||||
|
||||
|
|
|
|||
|
|
@ -2150,7 +2150,6 @@ Layout/LineLength:
|
|||
- 'ee/spec/workers/ci/minutes/update_project_and_namespace_usage_worker_spec.rb'
|
||||
- 'ee/spec/workers/ci/upstream_projects_subscriptions_cleanup_worker_spec.rb'
|
||||
- 'ee/spec/workers/compliance_management/merge_requests/compliance_violations_worker_spec.rb'
|
||||
- 'ee/spec/workers/concerns/elastic/indexing_control_spec.rb'
|
||||
- 'ee/spec/workers/elastic/migration_worker_spec.rb'
|
||||
- 'ee/spec/workers/geo/destroy_worker_spec.rb'
|
||||
- 'ee/spec/workers/geo/prune_event_log_worker_spec.rb'
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ RSpec/ExpectInHook:
|
|||
- 'ee/spec/tasks/gitlab/license_rake_spec.rb'
|
||||
- 'ee/spec/tasks/gitlab/spdx_rake_spec.rb'
|
||||
- 'ee/spec/workers/analytics/cycle_analytics/consistency_worker_spec.rb'
|
||||
- 'ee/spec/workers/concerns/elastic/indexing_control_spec.rb'
|
||||
- 'ee/spec/workers/elastic_index_bulk_cron_worker_spec.rb'
|
||||
- 'ee/spec/workers/elastic_indexing_control_worker_spec.rb'
|
||||
- 'ee/spec/workers/geo/secondary/registry_consistency_worker_spec.rb'
|
||||
|
|
|
|||
|
|
@ -3287,7 +3287,6 @@ RSpec/NamedSubject:
|
|||
- 'spec/services/projects/operations/update_service_spec.rb'
|
||||
- 'spec/services/projects/overwrite_project_service_spec.rb'
|
||||
- 'spec/services/projects/prometheus/alerts/notify_service_spec.rb'
|
||||
- 'spec/services/projects/prometheus/metrics/destroy_service_spec.rb'
|
||||
- 'spec/services/projects/readme_renderer_service_spec.rb'
|
||||
- 'spec/services/projects/transfer_service_spec.rb'
|
||||
- 'spec/services/projects/unlink_fork_service_spec.rb'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import BroadcastMessagesBase from '~/admin/broadcast_messages/components/base.vue';
|
||||
import { generateMockMessages } from '../../../../../../spec/frontend/admin/broadcast_messages/mock_data';
|
||||
|
||||
export default {
|
||||
title: 'admin/broadcast_messages/base',
|
||||
component: BroadcastMessagesBase,
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { BroadcastMessagesBase },
|
||||
template: '<broadcast-messages-base v-bind="$props" />',
|
||||
});
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
page: 1,
|
||||
messagesCount: 5,
|
||||
messages: generateMockMessages(5),
|
||||
};
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
Empty.args = {
|
||||
page: 1,
|
||||
messagesCount: 0,
|
||||
messages: [],
|
||||
};
|
||||
|
|
@ -16,6 +16,7 @@ import deletePipelineMutation from '../graphql/mutations/delete_pipeline.mutatio
|
|||
import retryPipelineMutation from '../graphql/mutations/retry_pipeline.mutation.graphql';
|
||||
import { getQueryHeaders } from '../graph/utils';
|
||||
import { POLL_INTERVAL } from '../graph/constants';
|
||||
import { MERGE_TRAIN_EVENT_TYPE } from './constants';
|
||||
import HeaderActions from './components/header_actions.vue';
|
||||
import HeaderBadges from './components/header_badges.vue';
|
||||
import getPipelineQuery from './graphql/queries/get_pipeline_header_data.query.graphql';
|
||||
|
|
@ -40,6 +41,8 @@ export default {
|
|||
TimeAgoTooltip,
|
||||
PipelineAccountVerificationAlert: () =>
|
||||
import('ee_component/vue_shared/components/pipeline_account_verification_alert.vue'),
|
||||
HeaderMergeTrainsLink: () =>
|
||||
import('ee_component/ci/pipeline_details/header/components/header_merge_trains_link.vue'),
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
|
@ -219,6 +222,9 @@ export default {
|
|||
refText() {
|
||||
return this.pipeline?.refText;
|
||||
},
|
||||
isMergeTrainPipeline() {
|
||||
return this.pipeline.mergeRequestEventType === MERGE_TRAIN_EVENT_TYPE;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reportFailure(errorType, errorMessages = []) {
|
||||
|
|
@ -406,6 +412,9 @@ export default {
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isMergeTrainPipeline" class="gl-mt-2">
|
||||
<header-merge-trains-link />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<header-actions
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ export const createPipelineHeaderApp = (elSelector, apolloProvider, graphqlResou
|
|||
pipelinesPath,
|
||||
identityVerificationPath,
|
||||
identityVerificationRequired,
|
||||
mergeTrainsAvailable,
|
||||
canReadMergeTrain,
|
||||
mergeTrainsPath,
|
||||
} = el.dataset;
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
|
|
@ -34,6 +37,9 @@ export const createPipelineHeaderApp = (elSelector, apolloProvider, graphqlResou
|
|||
pipelineIid,
|
||||
identityVerificationPath,
|
||||
identityVerificationRequired: parseBoolean(identityVerificationRequired),
|
||||
mergeTrainsAvailable: parseBoolean(mergeTrainsAvailable),
|
||||
canReadMergeTrain: parseBoolean(canReadMergeTrain),
|
||||
mergeTrainsPath,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(PipelineHeader);
|
||||
|
|
|
|||
|
|
@ -2,31 +2,10 @@ import { Table } from '@tiptap/extension-table';
|
|||
import { debounce } from 'lodash';
|
||||
import { VARIANT_WARNING } from '~/alert';
|
||||
import { __ } from '~/locale';
|
||||
import { ALERT_EVENT } from '../constants';
|
||||
import { getMarkdownSource } from '../services/markdown_sourcemap';
|
||||
import { shouldRenderHTMLTable } from '../services/serializer/table';
|
||||
|
||||
let alertShown = false;
|
||||
const onUpdate = debounce((editor) => {
|
||||
if (alertShown) return;
|
||||
|
||||
editor.state.doc.descendants((node) => {
|
||||
if (node.type.name === 'table' && node.attrs.isMarkdown && shouldRenderHTMLTable(node)) {
|
||||
editor.emit('alert', {
|
||||
message: __(
|
||||
'The content editor may change the markdown formatting style of the document, which may not match your original markdown style.',
|
||||
),
|
||||
variant: VARIANT_WARNING,
|
||||
});
|
||||
|
||||
alertShown = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
export default Table.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
|
|
@ -37,7 +16,24 @@ export default Table.extend({
|
|||
};
|
||||
},
|
||||
|
||||
onUpdate({ editor }) {
|
||||
onUpdate(editor);
|
||||
},
|
||||
onUpdate: debounce(function onUpdate({ editor }) {
|
||||
if (this.options.alertShown) return;
|
||||
|
||||
editor.state.doc.descendants((node) => {
|
||||
if (node.type.name === 'table' && node.attrs.isMarkdown && shouldRenderHTMLTable(node)) {
|
||||
this.options.eventHub.$emit(ALERT_EVENT, {
|
||||
message: __(
|
||||
'Tables containing block elements (like multiple paragraphs, lists or blockquotes) are not supported in Markdown and will be converted to HTML.',
|
||||
),
|
||||
variant: VARIANT_WARNING,
|
||||
});
|
||||
|
||||
this.options.alertShown = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, 1000),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
UNAVAILABLE_ADMIN_FEATURE_TEXT,
|
||||
} from '~/packages_and_registries/settings/project/constants';
|
||||
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
|
||||
import SettingsSection from '~/vue_shared/components/settings/settings_section.vue';
|
||||
|
||||
import ContainerExpirationPolicyForm from './container_expiration_policy_form.vue';
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ export default {
|
|||
GlSprintf,
|
||||
GlLink,
|
||||
ContainerExpirationPolicyForm,
|
||||
SettingsSection,
|
||||
},
|
||||
inject: ['projectPath', 'isAdmin', 'adminSettingsPath', 'enableHistoricEntries', 'helpPagePath'],
|
||||
i18n: {
|
||||
|
|
@ -77,17 +79,18 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div data-testid="container-expiration-policy-project-settings">
|
||||
<h3 data-testid="title" class="gl-heading-3 gl-mt-3!">
|
||||
{{ $options.i18n.CONTAINER_CLEANUP_POLICY_TITLE }}
|
||||
</h3>
|
||||
<p data-testid="description">
|
||||
<settings-section
|
||||
:heading="$options.i18n.CONTAINER_CLEANUP_POLICY_TITLE"
|
||||
data-testid="container-expiration-policy-project-settings"
|
||||
class="!gl-pt-5"
|
||||
>
|
||||
<template #description>
|
||||
<gl-sprintf :message="$options.i18n.CONTAINER_CLEANUP_POLICY_DESCRIPTION">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="helpPagePath">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</template>
|
||||
<container-expiration-policy-form
|
||||
v-if="isEnabled"
|
||||
v-model="workingCopy"
|
||||
|
|
@ -113,5 +116,5 @@ export default {
|
|||
<gl-sprintf :message="$options.i18n.FETCH_SETTINGS_ERROR_MESSAGE" />
|
||||
</gl-alert>
|
||||
</template>
|
||||
</div>
|
||||
</settings-section>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlAlert, GlCard, GlButton, GlSprintf } from '@gitlab/ui';
|
||||
import { GlAlert, GlButton, GlSprintf } from '@gitlab/ui';
|
||||
import { objectToQuery, visitUrl } from '~/lib/utils/url_utility';
|
||||
import { s__ } from '~/locale';
|
||||
import {
|
||||
|
|
@ -19,6 +19,7 @@ import {
|
|||
CADENCE_LABEL,
|
||||
EXPIRATION_POLICY_FOOTER_NOTE,
|
||||
} from '~/packages_and_registries/settings/project/constants';
|
||||
import CrudComponent from '~/vue_shared/components/crud_component.vue';
|
||||
import updateContainerExpirationPolicyMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_container_expiration_policy.mutation.graphql';
|
||||
import { updateContainerExpirationPolicy } from '~/packages_and_registries/settings/project/graphql/utils/cache_update';
|
||||
import { formOptionsGenerator } from '~/packages_and_registries/settings/project/utils';
|
||||
|
|
@ -31,13 +32,13 @@ import ExpirationToggle from './expiration_toggle.vue';
|
|||
export default {
|
||||
components: {
|
||||
GlAlert,
|
||||
GlCard,
|
||||
GlButton,
|
||||
GlSprintf,
|
||||
ExpirationDropdown,
|
||||
ExpirationInput,
|
||||
ExpirationToggle,
|
||||
ExpirationRunText,
|
||||
CrudComponent,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
inject: ['projectPath', 'projectSettingsPath'],
|
||||
|
|
@ -202,7 +203,7 @@ export default {
|
|||
@input="onModelChange($event, 'enabled')"
|
||||
/>
|
||||
|
||||
<div class="gl-display-flex gl-mt-7">
|
||||
<div class="gl-flex gl-mt-5">
|
||||
<expiration-dropdown
|
||||
:value="prefilledForm.cadence"
|
||||
:disabled="isFieldDisabled"
|
||||
|
|
@ -219,86 +220,74 @@ export default {
|
|||
class="gl-mb-0!"
|
||||
/>
|
||||
</div>
|
||||
<gl-alert class="gl-mt-7" :dismissible="false">
|
||||
<gl-alert class="gl-mt-5" :dismissible="false">
|
||||
<gl-sprintf :message="$options.i18n.EXPIRATION_POLICY_REGEX_NOTE">
|
||||
<template #code="{ content }">
|
||||
<code>{{ content }}</code>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-alert>
|
||||
<gl-card class="gl-mt-4">
|
||||
<template #header>
|
||||
{{ $options.i18n.KEEP_HEADER_TEXT }}
|
||||
</template>
|
||||
<template #default>
|
||||
<div>
|
||||
<p>
|
||||
<gl-sprintf :message="$options.i18n.KEEP_INFO_TEXT">
|
||||
<template #strong="{ content }">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<expiration-dropdown
|
||||
:value="prefilledForm.keepN"
|
||||
:disabled="isFieldDisabled"
|
||||
:form-options="$options.formOptions.keepN"
|
||||
:label="$options.i18n.KEEP_N_LABEL"
|
||||
name="keep-n"
|
||||
data-testid="keep-n-dropdown"
|
||||
@input="onModelChange($event, 'keepN')"
|
||||
/>
|
||||
<expiration-input
|
||||
v-model="prefilledForm.nameRegexKeep"
|
||||
:error="apiErrors.nameRegexKeep"
|
||||
:disabled="isFieldDisabled"
|
||||
:label="$options.i18n.NAME_REGEX_KEEP_LABEL"
|
||||
:description="$options.i18n.NAME_REGEX_KEEP_DESCRIPTION"
|
||||
name="keep-regex"
|
||||
data-testid="keep-regex-input"
|
||||
@input="onModelChange($event, 'nameRegexKeep')"
|
||||
@validation="setLocalErrors($event, 'nameRegexKeep')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</gl-card>
|
||||
<gl-card class="gl-mt-7">
|
||||
<template #header>
|
||||
{{ $options.i18n.REMOVE_HEADER_TEXT }}
|
||||
</template>
|
||||
<template #default>
|
||||
<div>
|
||||
<p>
|
||||
<gl-sprintf :message="$options.i18n.REMOVE_INFO_TEXT">
|
||||
<template #strong="{ content }">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<expiration-dropdown
|
||||
:value="prefilledForm.olderThan"
|
||||
:disabled="isFieldDisabled"
|
||||
:form-options="$options.formOptions.olderThan"
|
||||
:label="$options.i18n.EXPIRATION_SCHEDULE_LABEL"
|
||||
name="older-than"
|
||||
data-testid="older-than-dropdown"
|
||||
@input="onModelChange($event, 'olderThan')"
|
||||
/>
|
||||
<expiration-input
|
||||
v-model="prefilledForm.nameRegex"
|
||||
:error="apiErrors.nameRegex"
|
||||
:disabled="isFieldDisabled"
|
||||
:label="$options.i18n.NAME_REGEX_LABEL"
|
||||
:description="$options.i18n.NAME_REGEX_DESCRIPTION"
|
||||
name="remove-regex"
|
||||
data-testid="remove-regex-input"
|
||||
@input="onModelChange($event, 'nameRegex')"
|
||||
@validation="setLocalErrors($event, 'nameRegex')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</gl-card>
|
||||
<div class="gl-mt-7 gl-display-flex gl-align-items-center">
|
||||
<crud-component :title="$options.i18n.KEEP_HEADER_TEXT" class="gl-mt-5">
|
||||
<p>
|
||||
<gl-sprintf :message="$options.i18n.KEEP_INFO_TEXT">
|
||||
<template #strong="{ content }">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<expiration-dropdown
|
||||
:value="prefilledForm.keepN"
|
||||
:disabled="isFieldDisabled"
|
||||
:form-options="$options.formOptions.keepN"
|
||||
:label="$options.i18n.KEEP_N_LABEL"
|
||||
name="keep-n"
|
||||
data-testid="keep-n-dropdown"
|
||||
@input="onModelChange($event, 'keepN')"
|
||||
/>
|
||||
<expiration-input
|
||||
v-model="prefilledForm.nameRegexKeep"
|
||||
:error="apiErrors.nameRegexKeep"
|
||||
:disabled="isFieldDisabled"
|
||||
:label="$options.i18n.NAME_REGEX_KEEP_LABEL"
|
||||
:description="$options.i18n.NAME_REGEX_KEEP_DESCRIPTION"
|
||||
name="keep-regex"
|
||||
data-testid="keep-regex-input"
|
||||
@input="onModelChange($event, 'nameRegexKeep')"
|
||||
@validation="setLocalErrors($event, 'nameRegexKeep')"
|
||||
/>
|
||||
</crud-component>
|
||||
<crud-component :title="$options.i18n.REMOVE_HEADER_TEXT" class="gl-mt-5">
|
||||
<div>
|
||||
<p>
|
||||
<gl-sprintf :message="$options.i18n.REMOVE_INFO_TEXT">
|
||||
<template #strong="{ content }">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
<expiration-dropdown
|
||||
:value="prefilledForm.olderThan"
|
||||
:disabled="isFieldDisabled"
|
||||
:form-options="$options.formOptions.olderThan"
|
||||
:label="$options.i18n.EXPIRATION_SCHEDULE_LABEL"
|
||||
name="older-than"
|
||||
data-testid="older-than-dropdown"
|
||||
@input="onModelChange($event, 'olderThan')"
|
||||
/>
|
||||
<expiration-input
|
||||
v-model="prefilledForm.nameRegex"
|
||||
:error="apiErrors.nameRegex"
|
||||
:disabled="isFieldDisabled"
|
||||
:label="$options.i18n.NAME_REGEX_LABEL"
|
||||
:description="$options.i18n.NAME_REGEX_DESCRIPTION"
|
||||
name="remove-regex"
|
||||
data-testid="remove-regex-input"
|
||||
@input="onModelChange($event, 'nameRegex')"
|
||||
@validation="setLocalErrors($event, 'nameRegex')"
|
||||
/>
|
||||
</div>
|
||||
</crud-component>
|
||||
<div class="settings-sticky-footer gl-mt-5 gl-flex gl-items-center">
|
||||
<gl-button
|
||||
data-testid="save-button"
|
||||
type="submit"
|
||||
|
|
|
|||
|
|
@ -45,21 +45,19 @@ export default {
|
|||
|
||||
<template>
|
||||
<gl-form-group id="expiration-policy-toggle-group" label-for="expiration-policy-toggle">
|
||||
<div class="gl-display-flex">
|
||||
<gl-toggle
|
||||
id="expiration-policy-toggle"
|
||||
v-model="enabled"
|
||||
:label="$options.i18n.toggleLabel"
|
||||
label-position="hidden"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
<span class="gl-ml-5 gl-leading-24" data-testid="description">
|
||||
<gl-sprintf :message="toggleText">
|
||||
<template #strong="{ content }">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
<gl-toggle
|
||||
id="expiration-policy-toggle"
|
||||
v-model="enabled"
|
||||
:label="$options.i18n.toggleLabel"
|
||||
label-position="left"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
<div class="gl-text-subtle gl-mt-2" data-testid="description">
|
||||
<gl-sprintf :message="toggleText">
|
||||
<template #strong="{ content }">
|
||||
{{ content }}
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
</gl-form-group>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -48,10 +48,10 @@ export const NAME_REGEX_DESCRIPTION = s__(
|
|||
);
|
||||
|
||||
export const ENABLED_TOGGLE_DESCRIPTION = s__(
|
||||
'ContainerRegistry|%{strongStart}Enabled%{strongEnd} - Tags that match the rules on this page are automatically scheduled for deletion.',
|
||||
'ContainerRegistry|Enabled - tags that match the rules on this page are automatically scheduled for deletion.',
|
||||
);
|
||||
export const DISABLED_TOGGLE_DESCRIPTION = s__(
|
||||
'ContainerRegistry|%{strongStart}Disabled%{strongEnd} - Tags will not be automatically deleted.',
|
||||
'ContainerRegistry|Disabled - tags will not be automatically deleted.',
|
||||
);
|
||||
|
||||
export const CADENCE_LABEL = s__('ContainerRegistry|Run cleanup:');
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
NEW_WORK_ITEM_GID,
|
||||
WIDGET_TYPE_LABELS,
|
||||
WIDGET_TYPE_ROLLEDUP_DATES,
|
||||
WIDGET_TYPE_CRM_CONTACTS,
|
||||
} from '../constants';
|
||||
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
|
||||
import namespaceWorkItemTypesQuery from '../graphql/namespace_work_item_types.query.graphql';
|
||||
|
|
@ -41,6 +42,7 @@ import WorkItemDescription from './work_item_description.vue';
|
|||
import WorkItemAssignees from './work_item_assignees.vue';
|
||||
import WorkItemLabels from './work_item_labels.vue';
|
||||
import WorkItemLoading from './work_item_loading.vue';
|
||||
import WorkItemCrmContacts from './work_item_crm_contacts.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -55,6 +57,7 @@ export default {
|
|||
WorkItemAssignees,
|
||||
WorkItemLabels,
|
||||
WorkItemLoading,
|
||||
WorkItemCrmContacts,
|
||||
WorkItemHealthStatus: () =>
|
||||
import('ee_component/work_items/components/work_item_health_status.vue'),
|
||||
WorkItemColor: () => import('ee_component/work_items/components/work_item_color.vue'),
|
||||
|
|
@ -165,6 +168,9 @@ export default {
|
|||
workItemColor() {
|
||||
return findWidget(WIDGET_TYPE_COLOR, this.workItem);
|
||||
},
|
||||
workItemCrmContacts() {
|
||||
return findWidget(WIDGET_TYPE_CRM_CONTACTS, this.workItem);
|
||||
},
|
||||
workItemTypesForSelect() {
|
||||
return this.workItemTypes
|
||||
? this.workItemTypes.map((node) => ({
|
||||
|
|
@ -219,6 +225,9 @@ export default {
|
|||
const labelsWidget = findWidget(WIDGET_TYPE_LABELS, this.workItem);
|
||||
return labelsWidget?.labels?.nodes?.map((label) => label.id) || [];
|
||||
},
|
||||
workItemCrmContactIds() {
|
||||
return this.workItemCrmContacts?.contacts?.nodes?.map((item) => item.id) || [];
|
||||
},
|
||||
workItemColorValue() {
|
||||
const colorWidget = findWidget(WIDGET_TYPE_COLOR, this.workItem);
|
||||
return colorWidget?.color || '';
|
||||
|
|
@ -338,6 +347,12 @@ export default {
|
|||
};
|
||||
}
|
||||
|
||||
if (this.isWidgetSupported(WIDGET_TYPE_CRM_CONTACTS)) {
|
||||
workItemCreateInput.crmContactsWidget = {
|
||||
contactIds: this.workItemCrmContactIds,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.$apollo.mutate({
|
||||
mutation: createWorkItemMutation,
|
||||
|
|
@ -464,6 +479,16 @@ export default {
|
|||
@error="$emit('error', $event)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="workItemCrmContacts">
|
||||
<work-item-crm-contacts
|
||||
class="gl-mb-5"
|
||||
:full-path="fullPath"
|
||||
:work-item-id="workItem.id"
|
||||
:work-item-iid="workItem.iid"
|
||||
:work-item-type="selectedWorkItemTypeName"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="workItemLabels">
|
||||
<work-item-labels
|
||||
class="gl-mb-5 js-labels"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { ASC } from '~/notes/constants';
|
|||
import { __ } from '~/locale';
|
||||
import { clearDraft } from '~/lib/utils/autosave';
|
||||
import DiscussionReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
|
||||
import ResolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue';
|
||||
import createNoteMutation from '../../graphql/notes/create_work_item_note.mutation.graphql';
|
||||
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
|
||||
import { TRACKING_CATEGORY_SHOW, i18n } from '../../constants';
|
||||
|
|
@ -21,6 +22,7 @@ export default {
|
|||
WorkItemNoteSignedOut,
|
||||
WorkItemCommentLocked,
|
||||
WorkItemCommentForm,
|
||||
ResolveDiscussionButton,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
props: {
|
||||
|
|
@ -89,6 +91,26 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isDiscussionResolved: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isDiscussionResolvable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isResolving: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
hasReplies: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -174,6 +196,9 @@ export default {
|
|||
'internal-note': this.isInternalThread,
|
||||
};
|
||||
},
|
||||
resolveDiscussionTitle() {
|
||||
return this.isDiscussionResolved ? __('Unresolve thread') : __('Resolve thread');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
autofocus: {
|
||||
|
|
@ -283,8 +308,12 @@ export default {
|
|||
:comment-button-text="commentButtonText"
|
||||
:is-discussion-locked="isDiscussionLocked"
|
||||
:is-work-item-confidential="isWorkItemConfidential"
|
||||
:is-discussion-resolved="isDiscussionResolved"
|
||||
:is-discussion-resolvable="isDiscussionResolvable"
|
||||
:full-path="fullPath"
|
||||
:has-replies="hasReplies"
|
||||
:work-item-iid="workItemIid"
|
||||
@toggleResolveDiscussion="$emit('resolve')"
|
||||
@submitForm="updateWorkItem"
|
||||
@cancelEditing="cancelEditing"
|
||||
@error="$emit('error', $event)"
|
||||
|
|
@ -294,6 +323,16 @@ export default {
|
|||
data-testid="note-reply-textarea"
|
||||
@focus="showReplyForm"
|
||||
/>
|
||||
|
||||
<div v-if="!isNewDiscussion && !isEditing" class="discussion-actions">
|
||||
<resolve-discussion-button
|
||||
v-if="isDiscussionResolvable"
|
||||
data-testid="resolve-discussion-button"
|
||||
:is-resolving="isResolving"
|
||||
:button-title="resolveDiscussionTitle"
|
||||
@onClick="$emit('resolve')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -121,12 +121,28 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
isDiscussionResolved: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isDiscussionResolvable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
hasReplies: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
commentText: getDraft(this.autosaveKey) || this.initialValue || '',
|
||||
updateInProgress: false,
|
||||
isNoteInternal: false,
|
||||
toggleResolveChecked: this.isDiscussionResolved,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -162,6 +178,12 @@ export default {
|
|||
workItemTypeKey() {
|
||||
return capitalizeFirstCharacter(this.workItemType).replace(' ', '');
|
||||
},
|
||||
showResolveDiscussionToggle() {
|
||||
return !this.isNewDiscussion && this.isDiscussionResolvable && this.hasReplies;
|
||||
},
|
||||
resolveCheckboxLabel() {
|
||||
return this.isDiscussionResolved ? __('Unresolve thread') : __('Resolve thread');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setCommentText(newText) {
|
||||
|
|
@ -194,6 +216,15 @@ export default {
|
|||
this.$emit('cancelEditing');
|
||||
clearDraft(this.autosaveKey);
|
||||
},
|
||||
submitForm() {
|
||||
if (this.toggleResolveChecked) {
|
||||
this.$emit('toggleResolveDiscussion');
|
||||
}
|
||||
this.$emit('submitForm', {
|
||||
commentText: this.commentText,
|
||||
isNoteInternal: this.isNoteInternal,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -218,12 +249,22 @@ export default {
|
|||
supports-quick-actions
|
||||
:autofocus="autofocus"
|
||||
@input="setCommentText"
|
||||
@keydown.meta.enter="$emit('submitForm', { commentText, isNoteInternal })"
|
||||
@keydown.ctrl.enter="$emit('submitForm', { commentText, isNoteInternal })"
|
||||
@keydown.meta.enter="submitForm"
|
||||
@keydown.ctrl.enter="submitForm"
|
||||
@keydown.esc.stop="cancelEditing"
|
||||
/>
|
||||
</comment-field-layout>
|
||||
<div class="note-form-actions" data-testid="work-item-comment-form-actions">
|
||||
<div v-if="showResolveDiscussionToggle">
|
||||
<label>
|
||||
<gl-form-checkbox
|
||||
v-model="toggleResolveChecked"
|
||||
data-testid="toggle-resolve-checkbox"
|
||||
>
|
||||
{{ resolveCheckboxLabel }}
|
||||
</gl-form-checkbox>
|
||||
</label>
|
||||
</div>
|
||||
<gl-form-checkbox
|
||||
v-if="isNewDiscussion"
|
||||
v-model="isNoteInternal"
|
||||
|
|
@ -245,7 +286,7 @@ export default {
|
|||
data-testid="confirm-button"
|
||||
:disabled="!commentText.length"
|
||||
:loading="isSubmitting"
|
||||
@click="$emit('submitForm', { commentText, isNoteInternal })"
|
||||
@click="submitForm"
|
||||
>{{ commentButtonTextComputed }}
|
||||
</gl-button>
|
||||
<work-item-state-toggle
|
||||
|
|
@ -258,7 +299,7 @@ export default {
|
|||
:full-path="fullPath"
|
||||
:has-comment="Boolean(commentText.length)"
|
||||
can-update
|
||||
@submit-comment="$emit('submitForm', { commentText, isNoteInternal })"
|
||||
@submit-comment="submitForm"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<gl-button
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { ASC } from '~/notes/constants';
|
||||
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
||||
import toggleWorkItemNoteResolveDiscussion from '~/work_items/graphql/notes/toggle_work_item_note_resolve_discussion.mutation.graphql';
|
||||
import DiscussionNotesRepliesWrapper from '~/notes/components/discussion_notes_replies_wrapper.vue';
|
||||
import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
|
||||
import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
|
||||
|
|
@ -77,14 +78,20 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isExpandedOnLoad: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isExpanded: true,
|
||||
isExpanded: this.isExpandedOnLoad,
|
||||
autofocus: false,
|
||||
isReplying: false,
|
||||
replyingText: '',
|
||||
showForm: false,
|
||||
isResolving: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -104,7 +111,7 @@ export default {
|
|||
return null;
|
||||
},
|
||||
discussionId() {
|
||||
return this.discussion[0]?.discussion?.id || '';
|
||||
return this.firstComment?.id || '';
|
||||
},
|
||||
shouldShowReplyForm() {
|
||||
return this.showForm || this.hasReplies;
|
||||
|
|
@ -112,6 +119,25 @@ export default {
|
|||
isOnlyCommentOfAThread() {
|
||||
return !this.hasReplies && !this.showForm;
|
||||
},
|
||||
firstComment() {
|
||||
return this.discussion[0]?.discussion;
|
||||
},
|
||||
isDiscussionResolved() {
|
||||
return this.firstComment?.resolved;
|
||||
},
|
||||
isDiscussionResolvable() {
|
||||
return this.firstComment?.resolvable;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
discussion: {
|
||||
handler(newDiscussion) {
|
||||
if (newDiscussion[0].discussion.resolved === false) {
|
||||
this.isExpanded = true;
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showReplyForm() {
|
||||
|
|
@ -139,6 +165,52 @@ export default {
|
|||
this.isReplying = true;
|
||||
this.replyingText = commentText;
|
||||
},
|
||||
getToggledDiscussion(resolved) {
|
||||
let resolvedBy = null;
|
||||
if (resolved) {
|
||||
resolvedBy = {
|
||||
id: gon?.current_user_id,
|
||||
name: gon?.current_user_fullname,
|
||||
__typename: 'UserCore',
|
||||
};
|
||||
}
|
||||
const toggledDiscussionNotes = [...this.discussion].map((note) => {
|
||||
return {
|
||||
...note,
|
||||
discussion: {
|
||||
...note.discussion,
|
||||
resolved,
|
||||
resolvedBy,
|
||||
},
|
||||
};
|
||||
});
|
||||
return {
|
||||
id: this.discussionId,
|
||||
notes: {
|
||||
nodes: [...toggledDiscussionNotes],
|
||||
},
|
||||
};
|
||||
},
|
||||
async resolveDiscussion() {
|
||||
this.isResolving = true;
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: toggleWorkItemNoteResolveDiscussion,
|
||||
variables: { id: this.discussionId, resolve: !this.isDiscussionResolved },
|
||||
optimisticResponse: {
|
||||
discussionToggleResolve: {
|
||||
errors: [],
|
||||
discussion: this.getToggledDiscussion(!this.isDiscussionResolved),
|
||||
__typename: 'DiscussionToggleResolvePayload',
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.$emit('error', error.message);
|
||||
} finally {
|
||||
this.isResolving = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -158,8 +230,12 @@ export default {
|
|||
:class="{ 'gl-mb-4': hasReplies }"
|
||||
:assignees="assignees"
|
||||
:can-set-work-item-metadata="canSetWorkItemMetadata"
|
||||
:is-discussion-resolved="isDiscussionResolved"
|
||||
:is-discussion-resolvable="isDiscussionResolvable"
|
||||
:work-item-id="workItemId"
|
||||
:work-item-iid="workItemIid"
|
||||
:is-resolving="isResolving"
|
||||
@resolve="resolveDiscussion"
|
||||
@startReplying="showReplyForm"
|
||||
@deleteNote="$emit('deleteNote', note)"
|
||||
@reportAbuse="$emit('reportAbuse', note)"
|
||||
|
|
@ -187,9 +263,13 @@ export default {
|
|||
:work-item-id="workItemId"
|
||||
:work-item-iid="workItemIid"
|
||||
:can-set-work-item-metadata="canSetWorkItemMetadata"
|
||||
:is-discussion-resolved="isDiscussionResolved"
|
||||
:is-discussion-resolvable="isDiscussionResolvable"
|
||||
:is-resolving="isResolving"
|
||||
@startReplying="showReplyForm"
|
||||
@deleteNote="$emit('deleteNote', note)"
|
||||
@reportAbuse="$emit('reportAbuse', note)"
|
||||
@resolve="resolveDiscussion"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
<discussion-notes-replies-wrapper>
|
||||
|
|
@ -214,6 +294,9 @@ export default {
|
|||
:work-item-id="workItemId"
|
||||
:work-item-iid="workItemIid"
|
||||
:can-set-work-item-metadata="canSetWorkItemMetadata"
|
||||
:is-discussion-resolved="isDiscussionResolved"
|
||||
:is-discussion-resolvable="isDiscussionResolvable"
|
||||
:is-resolving="isResolving"
|
||||
@startReplying="showReplyForm"
|
||||
@deleteNote="$emit('deleteNote', reply)"
|
||||
@reportAbuse="$emit('reportAbuse', reply)"
|
||||
|
|
@ -241,10 +324,15 @@ export default {
|
|||
:is-discussion-locked="isDiscussionLocked"
|
||||
:is-internal-thread="note.internal"
|
||||
:is-work-item-confidential="isWorkItemConfidential"
|
||||
:is-discussion-resolved="isDiscussionResolved"
|
||||
:is-discussion-resolvable="isDiscussionResolvable"
|
||||
:is-resolving="isResolving"
|
||||
:has-replies="hasReplies"
|
||||
@startReplying="showReplyForm"
|
||||
@cancelEditing="hideReplyForm"
|
||||
@replied="onReplied"
|
||||
@replying="onReplying"
|
||||
@resolve="resolveDiscussion"
|
||||
@error="$emit('error', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -91,6 +91,21 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isDiscussionResolved: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isDiscussionResolvable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isResolving: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -126,6 +141,9 @@ export default {
|
|||
showReply() {
|
||||
return this.note.userPermissions.createNote && this.isFirstNote;
|
||||
},
|
||||
canResolve() {
|
||||
return this.note.userPermissions.resolveNote && this.isFirstNote && this.hasReplies;
|
||||
},
|
||||
noteHeaderClass() {
|
||||
return {
|
||||
'note-header': true,
|
||||
|
|
@ -174,6 +192,9 @@ export default {
|
|||
isWorkItemConfidential() {
|
||||
return this.workItem.confidential;
|
||||
},
|
||||
discussionResolvedBy() {
|
||||
return this.note.discussion.resolvedBy;
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
workItem: {
|
||||
|
|
@ -326,9 +347,13 @@ export default {
|
|||
:work-item-id="workItemId"
|
||||
:autofocus="isEditing"
|
||||
:is-work-item-confidential="isWorkItemConfidential"
|
||||
:is-discussion-resolved="isDiscussionResolved"
|
||||
:is-discussion-resolvable="isDiscussionResolvable"
|
||||
:has-replies="hasReplies"
|
||||
:full-path="fullPath"
|
||||
class="gl-pl-3 gl-mt-3"
|
||||
@cancelEditing="isEditing = false"
|
||||
@toggleResolveDiscussion="$emit('resolve')"
|
||||
@submitForm="updateNote"
|
||||
/>
|
||||
<div v-else data-testid="note-wrapper">
|
||||
|
|
@ -360,8 +385,14 @@ export default {
|
|||
:is-author-contributor="note.authorIsContributor"
|
||||
:max-access-level-of-author="note.maxAccessLevelOfAuthor"
|
||||
:project-name="projectName"
|
||||
:can-resolve="canResolve"
|
||||
:resolvable="isDiscussionResolvable"
|
||||
:is-resolved="isDiscussionResolved"
|
||||
:is-resolving="isResolving"
|
||||
:resolved-by="discussionResolvedBy"
|
||||
@startReplying="showReplyForm"
|
||||
@startEditing="startEditing"
|
||||
@resolve="$emit('resolve')"
|
||||
@error="($event) => $emit('error', $event)"
|
||||
@notifyCopyDone="notifyCopyDone"
|
||||
@deleteNote="$emit('deleteNote')"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export default {
|
|||
assignUserText: __('Assign to commenting user'),
|
||||
unassignUserText: __('Unassign from commenting user'),
|
||||
reportAbuseText: __('Report abuse'),
|
||||
resolveThreadTitle: __('Resolve thread'),
|
||||
},
|
||||
components: {
|
||||
EmojiPicker: () => import('~/emoji/components/picker.vue'),
|
||||
|
|
@ -108,6 +109,31 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
canResolve: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
resolvable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isResolved: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isResolving: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
resolvedBy: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
assignUserActionText() {
|
||||
|
|
@ -131,8 +157,21 @@ export default {
|
|||
name: this.projectName,
|
||||
});
|
||||
},
|
||||
resolveIcon() {
|
||||
if (!this.isResolving) {
|
||||
return this.isResolved ? 'check-circle-filled' : 'check-circle';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
resolveVariant() {
|
||||
return this.isResolved ? 'success' : 'default';
|
||||
},
|
||||
resolveThreadTitle() {
|
||||
return this.isResolved
|
||||
? __('Resolved by ') + this.resolvedBy.name
|
||||
: this.$options.i18n.resolveThreadTitle;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
async setAwardEmoji(name) {
|
||||
const { mutation, mutationName, errorMessage } = getMutation({ note: this.note, name });
|
||||
|
|
@ -199,6 +238,20 @@ export default {
|
|||
>
|
||||
{{ __('Contributor') }}
|
||||
</user-access-role-badge>
|
||||
<gl-button
|
||||
v-if="canResolve"
|
||||
ref="resolveButton"
|
||||
v-gl-tooltip.hover
|
||||
category="tertiary"
|
||||
:variant="resolveVariant"
|
||||
:class="{ 'is-disabled': !resolvable, 'is-active': isResolved }"
|
||||
:title="resolveThreadTitle"
|
||||
:aria-label="resolveThreadTitle"
|
||||
:icon="resolveIcon"
|
||||
:loading="isResolving"
|
||||
class="line-resolve-btn note-action-button"
|
||||
@click="$emit('resolve')"
|
||||
/>
|
||||
<emoji-picker
|
||||
v-if="showAwardEmoji"
|
||||
toggle-class="add-reaction-button btn-default-tertiary"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
WIDGET_TYPE_WEIGHT,
|
||||
WIDGET_TYPE_COLOR,
|
||||
WIDGET_TYPE_DEVELOPMENT,
|
||||
WIDGET_TYPE_CRM_CONTACTS,
|
||||
WORK_ITEM_TYPE_VALUE_EPIC,
|
||||
} from '../constants';
|
||||
import WorkItemAssignees from './work_item_assignees.vue';
|
||||
|
|
@ -26,6 +27,7 @@ import WorkItemMilestone from './work_item_milestone.vue';
|
|||
import WorkItemParent from './work_item_parent.vue';
|
||||
import WorkItemTimeTracking from './work_item_time_tracking.vue';
|
||||
import WorkItemDevelopment from './work_item_development/work_item_development.vue';
|
||||
import WorkItemCrmContacts from './work_item_crm_contacts.vue';
|
||||
|
||||
export default {
|
||||
ListType,
|
||||
|
|
@ -38,6 +40,7 @@ export default {
|
|||
WorkItemParent,
|
||||
WorkItemTimeTracking,
|
||||
WorkItemDevelopment,
|
||||
WorkItemCrmContacts,
|
||||
WorkItemWeight: () => import('ee_component/work_items/components/work_item_weight.vue'),
|
||||
WorkItemProgress: () => import('ee_component/work_items/components/work_item_progress.vue'),
|
||||
WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
|
||||
|
|
@ -139,6 +142,9 @@ export default {
|
|||
hasParent() {
|
||||
return this.workItemHierarchy?.hasParent;
|
||||
},
|
||||
workItemCrmContacts() {
|
||||
return this.isWidgetPresent(WIDGET_TYPE_CRM_CONTACTS) && this.glFeatures.workItemsAlpha;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isWidgetPresent(type) {
|
||||
|
|
@ -168,6 +174,15 @@ export default {
|
|||
"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="workItemCrmContacts">
|
||||
<work-item-crm-contacts
|
||||
class="gl-mb-5"
|
||||
:full-path="fullPath"
|
||||
:work-item-id="workItem.id"
|
||||
:work-item-iid="workItem.iid"
|
||||
:work-item-type="workItemType"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="workItemLabels">
|
||||
<work-item-labels
|
||||
class="gl-mb-5 js-labels"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,313 @@
|
|||
<script>
|
||||
import { GlIcon, GlLink, GlPopover } from '@gitlab/ui';
|
||||
import { difference, groupBy, xor } from 'lodash';
|
||||
import { findWidget } from '~/issues/list/utils';
|
||||
import { __, n__, s__ } from '~/locale';
|
||||
import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue';
|
||||
import Tracking from '~/tracking';
|
||||
import getGroupContactsQuery from '~/crm/contacts/components/graphql/get_group_contacts.query.graphql';
|
||||
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
|
||||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||
import updateNewWorkItemMutation from '../graphql/update_new_work_item.mutation.graphql';
|
||||
import {
|
||||
i18n,
|
||||
TRACKING_CATEGORY_SHOW,
|
||||
I18N_WORK_ITEM_ERROR_FETCHING_CRM_CONTACTS,
|
||||
WIDGET_TYPE_CRM_CONTACTS,
|
||||
} from '../constants';
|
||||
import { newWorkItemFullPath, newWorkItemId } from '../utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlPopover,
|
||||
WorkItemSidebarDropdownWidget,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
props: {
|
||||
fullPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
workItemId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
workItemIid: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
workItemType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchTerm: '',
|
||||
searchStarted: false,
|
||||
updateInProgress: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
createFlow() {
|
||||
return this.workItemId === newWorkItemId(this.workItemType);
|
||||
},
|
||||
selectedItems() {
|
||||
return this.workItemCrmContacts?.contacts.nodes || [];
|
||||
},
|
||||
isLoading() {
|
||||
return this.$apollo.queries.searchItems.loading;
|
||||
},
|
||||
selectedItemIds() {
|
||||
return this.selectedItems.map(({ id }) => id);
|
||||
},
|
||||
listItems() {
|
||||
const contacts = this.searchItems || [];
|
||||
const organizations = this.groupByOrganization(contacts, true);
|
||||
return organizations.map(([key, values]) => {
|
||||
return {
|
||||
text: key,
|
||||
options: values.map((contact) => {
|
||||
return { value: contact.id, text: `${contact.firstName} ${contact.lastName}` };
|
||||
}),
|
||||
};
|
||||
});
|
||||
},
|
||||
tracking() {
|
||||
return {
|
||||
category: TRACKING_CATEGORY_SHOW,
|
||||
label: 'item_contact',
|
||||
property: `type_${this.workItemType}`,
|
||||
};
|
||||
},
|
||||
selectedOrganizations() {
|
||||
return this.groupByOrganization(this.selectedItems, false);
|
||||
},
|
||||
workItemCrmContacts() {
|
||||
return findWidget(WIDGET_TYPE_CRM_CONTACTS, this.workItem);
|
||||
},
|
||||
workItemFullPath() {
|
||||
return this.createFlow
|
||||
? newWorkItemFullPath(this.fullPath, this.workItemType)
|
||||
: this.fullPath;
|
||||
},
|
||||
canUpdate() {
|
||||
return this.workItem?.userPermissions?.updateWorkItem;
|
||||
},
|
||||
dropdownLabelText() {
|
||||
return n__('%d contact', '%d contacts', this.selectedItemIds.length);
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
searchItems: {
|
||||
query: getGroupContactsQuery,
|
||||
variables() {
|
||||
return {
|
||||
groupFullPath: this.fullPath.split('/')[0],
|
||||
searchTerm: this.searchTerm,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.searchStarted;
|
||||
},
|
||||
update(data) {
|
||||
return data.group?.contacts?.nodes;
|
||||
},
|
||||
error() {
|
||||
this.$emit('error', I18N_WORK_ITEM_ERROR_FETCHING_CRM_CONTACTS);
|
||||
},
|
||||
},
|
||||
workItem: {
|
||||
query: workItemByIidQuery,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.workItemFullPath,
|
||||
iid: this.workItemIid,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return data?.workspace?.workItem ?? {};
|
||||
},
|
||||
skip() {
|
||||
return !this.workItemIid;
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
groupByOrganization(unsortedContacts, separateSelectedContacts) {
|
||||
// Sort the contacts first
|
||||
const contacts = [...unsortedContacts].sort((a, b) => {
|
||||
if (a.firstName !== b.firstName) {
|
||||
return a.firstName.localeCompare(b.firstName);
|
||||
}
|
||||
return a.lastName.localeCompare(b.lastName);
|
||||
});
|
||||
|
||||
let groups = [];
|
||||
let remainingContacts;
|
||||
// Group the selected contacts first
|
||||
if (separateSelectedContacts && this.selectedItems.length) {
|
||||
remainingContacts = contacts.filter(
|
||||
(contact) => !this.selectedItemIds.includes(contact.id),
|
||||
);
|
||||
groups.push([
|
||||
__('Selected'),
|
||||
contacts.filter((contact) => this.selectedItemIds.includes(contact.id)),
|
||||
]);
|
||||
} else {
|
||||
remainingContacts = contacts;
|
||||
}
|
||||
|
||||
const orphanContacts = remainingContacts.filter(({ organization }) => !organization);
|
||||
// Display each organization and their contacts next
|
||||
remainingContacts = remainingContacts.filter(({ organization }) => organization);
|
||||
const organizationGroups = Object.entries(groupBy(remainingContacts, 'organization.name'));
|
||||
organizationGroups.sort((a, b) => a[0].localeCompare(b[0]));
|
||||
groups = groups.concat(organizationGroups);
|
||||
|
||||
// Then finally display contacts without an organization
|
||||
if (orphanContacts.length) {
|
||||
groups.push([s__('Crm|No organization'), orphanContacts]);
|
||||
}
|
||||
return groups;
|
||||
},
|
||||
search(searchTerm = '') {
|
||||
this.searchTerm = searchTerm;
|
||||
this.searchStarted = true;
|
||||
},
|
||||
async updateItems(newSelectedItemIds) {
|
||||
this.updateInProgress = true;
|
||||
let newSelectedItems;
|
||||
|
||||
const differingItems = xor(newSelectedItemIds, this.selectedItemIds);
|
||||
|
||||
if (differingItems === 0) return;
|
||||
if (newSelectedItemIds.length === 0) {
|
||||
newSelectedItems = [];
|
||||
} else {
|
||||
const removeIds = difference(this.selectedItemIds, newSelectedItemIds);
|
||||
if (removeIds.length > 0) {
|
||||
newSelectedItems = this.selectedItems.filter(({ id }) => !removeIds.includes(id));
|
||||
}
|
||||
|
||||
const addIds = difference(newSelectedItemIds, this.selectedItemIds);
|
||||
if (addIds.length > 0) {
|
||||
newSelectedItems = [
|
||||
...this.selectedItems,
|
||||
...this.searchItems.filter(({ id }) => addIds.includes(id)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (this.createFlow) {
|
||||
this.$apollo.mutate({
|
||||
mutation: updateNewWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
workItemType: this.workItemType,
|
||||
fullPath: this.fullPath,
|
||||
crmContacts: newSelectedItems,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.updateInProgress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
workItemUpdate: { errors },
|
||||
},
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: updateWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: this.workItemId,
|
||||
crmContactsWidget: {
|
||||
contactIds: newSelectedItemIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors[0].message);
|
||||
}
|
||||
|
||||
this.track('updated_contacts');
|
||||
} catch {
|
||||
this.$emit('error', i18n.updateError);
|
||||
} finally {
|
||||
this.searchTerm = '';
|
||||
this.updateInProgress = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<work-item-sidebar-dropdown-widget
|
||||
:dropdown-label="s__('Crm|Contacts')"
|
||||
:can-update="canUpdate"
|
||||
dropdown-name="crm-contacts"
|
||||
:loading="isLoading"
|
||||
:list-items="listItems"
|
||||
:item-value="selectedItemIds"
|
||||
:update-in-progress="updateInProgress"
|
||||
:toggle-dropdown-text="dropdownLabelText"
|
||||
:header-text="s__('Crm|Select contacts')"
|
||||
multi-select
|
||||
clear-search-on-item-select
|
||||
data-testid="work-item-crm-contacts"
|
||||
@dropdownShown="search"
|
||||
@searchStarted="search"
|
||||
@updateValue="updateItems"
|
||||
>
|
||||
<template #readonly>
|
||||
<div class="gl-gap-2 gl-mt-1">
|
||||
<div
|
||||
v-for="[organizationName, contacts] in selectedOrganizations"
|
||||
:key="organizationName"
|
||||
data-testid="organization"
|
||||
>
|
||||
<div class="gl-text-secondary gl-mt-3">{{ organizationName }}</div>
|
||||
<div
|
||||
v-for="contact in contacts"
|
||||
:id="`contact_container_${contact.id}`"
|
||||
:key="contact.id"
|
||||
data-testid="contact"
|
||||
>
|
||||
<gl-link :id="`contact_${contact.id}`" class="gl-text-inherit">
|
||||
{{ contact.firstName }} {{ contact.lastName }}
|
||||
</gl-link>
|
||||
<gl-popover
|
||||
:target="`contact_${contact.id}`"
|
||||
:container="`contact_container_${contact.id}`"
|
||||
triggers="hover focus"
|
||||
placement="top"
|
||||
>
|
||||
<div>{{ contact.firstName }} {{ contact.lastName }}</div>
|
||||
<div class="gl-text-secondary">
|
||||
<div>{{ contact.description }}</div>
|
||||
<div v-if="contact.email">
|
||||
<gl-icon name="mail" class="gl-mr-2" />{{ contact.email }}
|
||||
</div>
|
||||
<div v-if="contact.phone">
|
||||
<gl-icon name="mobile" class="gl-mr-2" />{{ contact.phone }}
|
||||
</div>
|
||||
<div v-if="organizationName !== s__('Crm|No organization')">
|
||||
<gl-icon name="building" class="gl-mr-2" />{{ organizationName }}
|
||||
</div>
|
||||
</div>
|
||||
</gl-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</work-item-sidebar-dropdown-widget>
|
||||
</template>
|
||||
|
|
@ -280,6 +280,9 @@ export default {
|
|||
reportAbuse(isOpen, reply = {}) {
|
||||
this.$emit('openReportAbuse', reply);
|
||||
},
|
||||
isDiscussionResolved(discussion) {
|
||||
return discussion.notes.nodes[0]?.discussion?.resolved;
|
||||
},
|
||||
async fetchMoreNotes() {
|
||||
this.isLoadingMore = true;
|
||||
await this.$apollo.queries.workItemNotes
|
||||
|
|
@ -393,6 +396,7 @@ export default {
|
|||
:can-set-work-item-metadata="canSetWorkItemMetadata"
|
||||
:is-discussion-locked="isDiscussionLocked"
|
||||
:is-work-item-confidential="isWorkItemConfidential"
|
||||
:is-expanded-on-load="!isDiscussionResolved(discussion)"
|
||||
@deleteNote="showDeleteNoteModal($event, discussion)"
|
||||
@reportAbuse="reportAbuse(true, $event)"
|
||||
@error="$emit('error', $event)"
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ export default {
|
|||
inputId: 'work-item-parent-listbox-value',
|
||||
noWorkItemId: 'no-work-item-id',
|
||||
i18n: {
|
||||
assignParentLabel: s__('WorkItem|Assign parent'),
|
||||
assignParentLabel: s__('WorkItem|Select parent'),
|
||||
parentLabel: s__('WorkItem|Parent'),
|
||||
none: s__('WorkItem|None'),
|
||||
unAssign: s__('WorkItem|Unassign'),
|
||||
unAssign: s__('WorkItem|Clear'),
|
||||
workItemsFetchError: s__(
|
||||
'WorkItem|Something went wrong while fetching items. Please try again.',
|
||||
),
|
||||
|
|
@ -232,7 +232,7 @@ export default {
|
|||
<gl-link
|
||||
v-if="localSelectedItem"
|
||||
data-testid="work-item-parent-link"
|
||||
class="gl-link gl-text-gray-900 gl-display-inline-block gl-max-w-full gl-whitespace-nowrap gl-text-overflow-ellipsis gl-overflow-hidden"
|
||||
class="gl-inline-block gl-align-top gl-text-gray-900 gl-max-w-full gl-whitespace-nowrap gl-text-overflow-ellipsis gl-overflow-hidden"
|
||||
:href="parentWebUrl"
|
||||
>{{ listboxText }}</gl-link
|
||||
>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export const WIDGET_TYPE_LINKED_ITEMS = 'LINKED_ITEMS';
|
|||
export const WIDGET_TYPE_COLOR = 'COLOR';
|
||||
export const WIDGET_TYPE_DESIGNS = 'DESIGNS';
|
||||
export const WIDGET_TYPE_DEVELOPMENT = 'DEVELOPMENT';
|
||||
export const WIDGET_TYPE_CRM_CONTACTS = 'CRM_CONTACTS';
|
||||
|
||||
export const WORK_ITEM_TYPE_ENUM_INCIDENT = 'INCIDENT';
|
||||
export const WORK_ITEM_TYPE_ENUM_ISSUE = 'ISSUE';
|
||||
|
|
@ -67,6 +68,9 @@ export const i18n = {
|
|||
export const I18N_WORK_ITEM_ERROR_FETCHING_LABELS = s__(
|
||||
'WorkItem|Something went wrong when fetching labels. Please try again.',
|
||||
);
|
||||
export const I18N_WORK_ITEM_ERROR_FETCHING_CRM_CONTACTS = s__(
|
||||
'WorkItem|Something went wrong when fetching CRM contacts. Please try again.',
|
||||
);
|
||||
export const I18N_WORK_ITEM_ERROR_FETCHING_TYPES = s__(
|
||||
'WorkItem|Something went wrong when fetching work item types. Please try again',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import {
|
|||
WIDGET_TYPE_ITERATION,
|
||||
WIDGET_TYPE_HEALTH_STATUS,
|
||||
WIDGET_TYPE_DESCRIPTION,
|
||||
WIDGET_TYPE_CRM_CONTACTS,
|
||||
NEW_WORK_ITEM_IID,
|
||||
} from '../constants';
|
||||
import workItemByIidQuery from './work_item_by_iid.query.graphql';
|
||||
|
|
@ -240,6 +241,7 @@ export const setNewWorkItemCache = async (
|
|||
WIDGET_TYPE_HIERARCHY,
|
||||
WIDGET_TYPE_TIME_TRACKING,
|
||||
WIDGET_TYPE_PARTICIPANTS,
|
||||
WIDGET_TYPE_CRM_CONTACTS,
|
||||
];
|
||||
|
||||
if (!widgetDefinitions) {
|
||||
|
|
@ -300,6 +302,17 @@ export const setNewWorkItemCache = async (
|
|||
});
|
||||
}
|
||||
|
||||
if (widgetName === WIDGET_TYPE_CRM_CONTACTS) {
|
||||
widgets.push({
|
||||
type: 'CRM_CONTACTS',
|
||||
contacts: {
|
||||
nodes: [],
|
||||
__typename: 'CustomerRelationsContactConnection',
|
||||
},
|
||||
__typename: 'WorkItemWidgetCrmContacts',
|
||||
});
|
||||
}
|
||||
|
||||
if (widgetName === WIDGET_TYPE_LABELS) {
|
||||
const labelsWidgetData = widgetDefinitions.find(
|
||||
(definition) => definition.type === WIDGET_TYPE_LABELS,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
#import "ee_else_ce/work_items/graphql/notes/work_item_note.fragment.graphql"
|
||||
|
||||
mutation toggleWorkItemNoteResolveDiscussion($id: DiscussionID!, $resolve: Boolean!) {
|
||||
discussionToggleResolve(input: { id: $id, resolve: $resolve }) {
|
||||
discussion {
|
||||
id
|
||||
notes {
|
||||
nodes {
|
||||
...WorkItemNote
|
||||
}
|
||||
}
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,12 @@ fragment WorkItemNote on Note {
|
|||
}
|
||||
discussion {
|
||||
id
|
||||
resolved
|
||||
resolvable
|
||||
resolvedBy {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
author {
|
||||
...User
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
WIDGET_TYPE_LABELS,
|
||||
WIDGET_TYPE_HEALTH_STATUS,
|
||||
WIDGET_TYPE_DESCRIPTION,
|
||||
WIDGET_TYPE_CRM_CONTACTS,
|
||||
NEW_WORK_ITEM_IID,
|
||||
CLEAR_VALUE,
|
||||
} from '../constants';
|
||||
|
|
@ -64,6 +65,7 @@ export const updateNewWorkItemCache = (input, cache) => {
|
|||
confidential,
|
||||
labels,
|
||||
rolledUpDates,
|
||||
crmContacts,
|
||||
} = input;
|
||||
|
||||
const query = workItemByIidQuery;
|
||||
|
|
@ -95,6 +97,11 @@ export const updateNewWorkItemCache = (input, cache) => {
|
|||
newData: description,
|
||||
nodePath: 'description',
|
||||
},
|
||||
{
|
||||
widgetType: WIDGET_TYPE_CRM_CONTACTS,
|
||||
newData: crmContacts,
|
||||
nodePath: 'contacts.nodes',
|
||||
},
|
||||
];
|
||||
|
||||
widgetUpdates.forEach(({ widgetType, newData, nodePath }) => {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,13 @@ input LocalLabelInput {
|
|||
title: String
|
||||
}
|
||||
|
||||
input LocalCrmContactsInput {
|
||||
firstName: String
|
||||
lastName: String
|
||||
organizationName: String
|
||||
id: ID!
|
||||
}
|
||||
|
||||
input LocalUpdateWorkItemInput {
|
||||
id: WorkItemID!
|
||||
assignees: [LocalUserInput!]
|
||||
|
|
@ -63,6 +70,7 @@ input LocalUpdateNewWorkItemInput {
|
|||
confidential: Boolean
|
||||
labels: [LocalLabelInput]
|
||||
rolledUpDates: [LocalRolledUpDatesInput]
|
||||
crmContacts: [LocalCrmContactsInput]
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
|
|
|
|||
|
|
@ -150,4 +150,20 @@ fragment WorkItemWidgets on WorkItemWidget {
|
|||
}
|
||||
}
|
||||
}
|
||||
... on WorkItemWidgetCrmContacts {
|
||||
contacts {
|
||||
nodes {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
phone
|
||||
description
|
||||
organization {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Projects
|
||||
module Prometheus
|
||||
module Metrics
|
||||
class BaseService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def initialize(metric, params = {})
|
||||
@metric = metric
|
||||
@params = params.dup
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :metric, :params
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Projects
|
||||
module Prometheus
|
||||
module Metrics
|
||||
class DestroyService < Metrics::BaseService
|
||||
def execute
|
||||
metric.destroy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
table_name: observability_metrics_issues_connections
|
||||
classes:
|
||||
- Observability::MetricsIssuesConnection
|
||||
feature_categories:
|
||||
- metrics
|
||||
description: Represents the join between an Issue and an Observability metric stored in Gitlab Observability Backend.
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/160713
|
||||
milestone: '17.3'
|
||||
gitlab_schema: gitlab_main_cell
|
||||
sharding_key:
|
||||
namespace_id: namespaces
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateMetricsIssuesConnections < Gitlab::Database::Migration[2.2]
|
||||
disable_ddl_transaction!
|
||||
milestone '17.3'
|
||||
|
||||
def up
|
||||
create_table :observability_metrics_issues_connections do |t|
|
||||
t.references :issue, null: false, foreign_key: { to_table: :issues, on_delete: :cascade }, index: false
|
||||
t.bigint :namespace_id, null: false, index: true
|
||||
t.timestamps_with_timezone null: false
|
||||
t.integer :metric_type, null: false, limit: 2
|
||||
t.text :metric_name, null: false, limit: 500
|
||||
|
||||
t.index [:issue_id, :metric_type, :metric_name],
|
||||
unique: true,
|
||||
name: 'idx_o11y_metric_issue_conn_on_issue_id_metric_type_name'
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :observability_metrics_issues_connections
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
|
||||
# for more information on how to write migrations for GitLab.
|
||||
|
||||
class AddNamespacesO11yMetricsFk < Gitlab::Database::Migration[2.2]
|
||||
disable_ddl_transaction!
|
||||
milestone '17.3'
|
||||
|
||||
def up
|
||||
add_concurrent_foreign_key :observability_metrics_issues_connections,
|
||||
:namespaces,
|
||||
column: :namespace_id,
|
||||
on_delete: :cascade
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_foreign_key :observability_metrics_issues_connections, column: :namespace_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
e7b09f3a8bf7795ebcc1db9322786bd72e08911ef9706c5bc886624ec2e59e4f
|
||||
|
|
@ -0,0 +1 @@
|
|||
57742714f77431808f51983d2f803280276c167e55dc05094bbcca8b8b0e58f9
|
||||
|
|
@ -13797,6 +13797,26 @@ CREATE SEQUENCE oauth_openid_requests_id_seq
|
|||
|
||||
ALTER SEQUENCE oauth_openid_requests_id_seq OWNED BY oauth_openid_requests.id;
|
||||
|
||||
CREATE TABLE observability_metrics_issues_connections (
|
||||
id bigint NOT NULL,
|
||||
issue_id bigint NOT NULL,
|
||||
namespace_id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
metric_type smallint NOT NULL,
|
||||
metric_name text NOT NULL,
|
||||
CONSTRAINT check_3c743c1262 CHECK ((char_length(metric_name) <= 500))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE observability_metrics_issues_connections_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE observability_metrics_issues_connections_id_seq OWNED BY observability_metrics_issues_connections.id;
|
||||
|
||||
CREATE TABLE onboarding_progresses (
|
||||
id bigint NOT NULL,
|
||||
namespace_id bigint NOT NULL,
|
||||
|
|
@ -21371,6 +21391,8 @@ ALTER TABLE ONLY oauth_device_grants ALTER COLUMN id SET DEFAULT nextval('oauth_
|
|||
|
||||
ALTER TABLE ONLY oauth_openid_requests ALTER COLUMN id SET DEFAULT nextval('oauth_openid_requests_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY observability_metrics_issues_connections ALTER COLUMN id SET DEFAULT nextval('observability_metrics_issues_connections_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY onboarding_progresses ALTER COLUMN id SET DEFAULT nextval('onboarding_progresses_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY operations_feature_flag_scopes ALTER COLUMN id SET DEFAULT nextval('operations_feature_flag_scopes_id_seq'::regclass);
|
||||
|
|
@ -23734,6 +23756,9 @@ ALTER TABLE ONLY oauth_device_grants
|
|||
ALTER TABLE ONLY oauth_openid_requests
|
||||
ADD CONSTRAINT oauth_openid_requests_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY observability_metrics_issues_connections
|
||||
ADD CONSTRAINT observability_metrics_issues_connections_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY onboarding_progresses
|
||||
ADD CONSTRAINT onboarding_progresses_pkey PRIMARY KEY (id);
|
||||
|
||||
|
|
@ -26038,6 +26063,8 @@ CREATE UNIQUE INDEX idx_namespace_settings_on_default_compliance_framework_id ON
|
|||
|
||||
CREATE INDEX idx_namespace_settings_on_last_dormant_member_review_at ON namespace_settings USING btree (last_dormant_member_review_at) WHERE (remove_dormant_members IS TRUE);
|
||||
|
||||
CREATE UNIQUE INDEX idx_o11y_metric_issue_conn_on_issue_id_metric_type_name ON observability_metrics_issues_connections USING btree (issue_id, metric_type, metric_name);
|
||||
|
||||
CREATE UNIQUE INDEX idx_on_approval_group_rules_any_approver_type ON approval_group_rules USING btree (group_id, rule_type) WHERE (rule_type = 4);
|
||||
|
||||
CREATE UNIQUE INDEX idx_on_approval_group_rules_group_id_type_name ON approval_group_rules USING btree (group_id, rule_type, name);
|
||||
|
|
@ -28462,6 +28489,8 @@ CREATE UNIQUE INDEX index_oauth_device_grants_on_user_code ON oauth_device_grant
|
|||
|
||||
CREATE INDEX index_oauth_openid_requests_on_access_grant_id ON oauth_openid_requests USING btree (access_grant_id);
|
||||
|
||||
CREATE INDEX index_observability_metrics_issues_connections_on_namespace_id ON observability_metrics_issues_connections USING btree (namespace_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_on_deploy_keys_id_and_type_and_public ON keys USING btree (id, type) WHERE (public = true);
|
||||
|
||||
CREATE INDEX index_on_dingtalk_tracker_data_corpid ON dingtalk_tracker_data USING btree (corpid) WHERE (corpid IS NOT NULL);
|
||||
|
|
@ -33606,6 +33635,9 @@ ALTER TABLE ONLY boards
|
|||
ALTER TABLE ONLY epic_user_mentions
|
||||
ADD CONSTRAINT fk_f1ab52883e FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY observability_metrics_issues_connections
|
||||
ADD CONSTRAINT fk_f218d84a14 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE p_ci_pipeline_variables
|
||||
ADD CONSTRAINT fk_f29c5f4380_p FOREIGN KEY (partition_id, pipeline_id) REFERENCES ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;
|
||||
|
||||
|
|
@ -34314,6 +34346,9 @@ ALTER TABLE ONLY ml_models
|
|||
ALTER TABLE ONLY elastic_group_index_statuses
|
||||
ADD CONSTRAINT fk_rails_52b9969b12 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY observability_metrics_issues_connections
|
||||
ADD CONSTRAINT fk_rails_533fe605e3 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY bulk_import_configurations
|
||||
ADD CONSTRAINT fk_rails_536b96bff1 FOREIGN KEY (bulk_import_id) REFERENCES bulk_imports(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
|||
|
|
@ -39,8 +39,13 @@ averaged.
|
|||
|
||||
#### Add test coverage results using `coverage` keyword
|
||||
|
||||
To add test coverage results to a merge request using the project's `.gitlab-ci.yml` file, provide a regular expression
|
||||
using the [`coverage`](../yaml/index.md#coverage) keyword.
|
||||
You can display test coverage results in a merge request by adding the
|
||||
[`coverage`](../yaml/index.md#coverage) keyword to your project's `.gitlab-ci.yml` file.
|
||||
|
||||
To aggregate multiple test coverage values:
|
||||
|
||||
- For each job you want to include in the aggregate value,
|
||||
add the `coverage` keyword followed by a regular expression.
|
||||
|
||||
#### Test coverage examples
|
||||
|
||||
|
|
@ -114,7 +119,7 @@ You can require specific users or a group to approve merge requests that would r
|
|||
|
||||
To add a `Coverage-Check` approval rule:
|
||||
|
||||
1. Set up a [`coverage`](../yaml/index.md#coverage) regular expression for all jobs you want to include in the overall coverage value.
|
||||
1. [Add test coverage results to a merge request](#add-test-coverage-results-using-coverage-keyword).
|
||||
1. Go to your project and select **Settings > Merge requests**.
|
||||
1. Under **Merge request approvals**, select **Enable** next to the `Coverage-Check` approval rule.
|
||||
1. Select the **Target branch**.
|
||||
|
|
|
|||
|
|
@ -2053,7 +2053,7 @@ In this example:
|
|||
|
||||
**Additional details**:
|
||||
|
||||
- You can find parse examples in [Code Coverage](../testing/code_coverage.md#test-coverage-examples).
|
||||
- You can find regex examples in [Code Coverage](../testing/code_coverage.md#test-coverage-examples).
|
||||
- If there is more than one matched line in the job output, the last line is used
|
||||
(the first result of reverse search).
|
||||
- If there are multiple matches in a single line, the last match is searched
|
||||
|
|
|
|||
|
|
@ -277,7 +277,8 @@ pages:
|
|||
Some other examples of mixing [variables](../../../ci/variables/index.md) with strings for dynamic prefixes:
|
||||
|
||||
- `pages.path_prefix: 'mr-$CI_COMMIT_REF_SLUG'`: Branch or tag name prefixed with `mr-`, like `mr-branch-name`.
|
||||
- `pages.path_prefix: '-${CI_MERGE_REQUEST_IID}-'`: Merge request number prefixed and suffixed with `-`, like `-123-`.
|
||||
- `pages.path_prefix: '_${CI_MERGE_REQUEST_IID}_'`: Merge request number
|
||||
prefixed ans suffixed with `_`, like `_123_`.
|
||||
|
||||
### Use multiple deployments to create pages environments
|
||||
|
||||
|
|
@ -302,7 +303,7 @@ pages:
|
|||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "staging" # ensure to run on master (with default PAGES_PREFIX)
|
||||
variables:
|
||||
PAGES_PREFIX: '-stg' # prefix with _stg for the staging branch
|
||||
PAGES_PREFIX: '_stg' # prefix with _stg for the staging branch
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event" # conditionally change the prefix on Merge Requests
|
||||
when: manual # run pages manually on Merge Requests
|
||||
variables:
|
||||
|
|
|
|||
|
|
@ -235,6 +235,11 @@ msgid_plural "%d completed issues"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d contact"
|
||||
msgid_plural "%d contacts"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d contribution"
|
||||
msgid_plural "%d contributions"
|
||||
msgstr[0] ""
|
||||
|
|
@ -14241,12 +14246,6 @@ msgid_plural "ContainerRegistry|%{count} tags"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "ContainerRegistry|%{strongStart}Disabled%{strongEnd} - Tags will not be automatically deleted."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|%{strongStart}Enabled%{strongEnd} - Tags that match the rules on this page are automatically scheduled for deletion."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -14382,6 +14381,9 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Digest: %{imageId}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Disabled - tags will not be automatically deleted."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Docker connection error"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -14391,6 +14393,9 @@ msgstr ""
|
|||
msgid "ContainerRegistry|Enable cleanup policy"
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|Enabled - tags that match the rules on this page are automatically scheduled for deletion."
|
||||
msgstr ""
|
||||
|
||||
msgid "ContainerRegistry|GitLab is unable to validate this signature automatically. Validate the signature manually before trusting it."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -16019,6 +16024,9 @@ msgstr ""
|
|||
msgid "Crm|Organizations"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Select contacts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cron time zone"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -39305,6 +39313,9 @@ msgstr ""
|
|||
msgid "Pipelines|Validating GitLab CI configuration…"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|View merge train details"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Visualize"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -44930,6 +44941,9 @@ msgstr ""
|
|||
msgid "Resolved by"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resolved by "
|
||||
msgstr ""
|
||||
|
||||
msgid "Resolved by %{name}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -52378,6 +52392,9 @@ msgstr ""
|
|||
msgid "Table of contents"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tables containing block elements (like multiple paragraphs, lists or blockquotes) are not supported in Markdown and will be converted to HTML."
|
||||
msgstr ""
|
||||
|
||||
msgid "Tag"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -53111,9 +53128,6 @@ msgstr ""
|
|||
msgid "The contact does not belong to the issue group's root ancestor"
|
||||
msgstr ""
|
||||
|
||||
msgid "The content editor may change the markdown formatting style of the document, which may not match your original markdown style."
|
||||
msgstr ""
|
||||
|
||||
msgid "The content for this wiki page failed to load. To fix this error, reload the page."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -60255,9 +60269,6 @@ msgstr ""
|
|||
msgid "WorkItem|Are you sure you want to delete the %{workItemType}? This action cannot be reversed."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Assign parent"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Blocked by"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -60279,6 +60290,9 @@ msgstr ""
|
|||
msgid "WorkItem|Child removed"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Clear"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Close %{workItemType}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -60495,6 +60509,9 @@ msgstr ""
|
|||
msgid "WorkItem|Select a project"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Select parent"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Select type"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -60516,6 +60533,9 @@ msgstr ""
|
|||
msgid "WorkItem|Something went wrong when deleting the task. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong when fetching CRM contacts. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Something went wrong when fetching child items. Please refresh this page."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -60639,9 +60659,6 @@ msgstr ""
|
|||
msgid "WorkItem|Turn on confidentiality"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Unassign"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Undo"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@
|
|||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/fonts": "^1.3.0",
|
||||
"@gitlab/svgs": "3.109.0",
|
||||
"@gitlab/ui": "87.4.0",
|
||||
"@gitlab/ui": "87.6.1",
|
||||
"@gitlab/web-ide": "^0.0.1-dev-20240613133550",
|
||||
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
|
||||
"@rails/actioncable": "7.0.8-4",
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ RSpec.describe 'Project > Settings > Packages and registries > Container registr
|
|||
|
||||
within_testid 'container-expiration-policy-project-settings' do
|
||||
expect(find_by_testid('enable-toggle'))
|
||||
.to have_content('Disabled - Tags will not be automatically deleted.')
|
||||
.to have_content('Disabled - tags will not be automatically deleted.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { builders } from 'prosemirror-test-builder';
|
||||
import createEventHub from '~/helpers/event_hub_factory';
|
||||
import Bold from '~/content_editor/extensions/bold';
|
||||
import BulletList from '~/content_editor/extensions/bullet_list';
|
||||
import ListItem from '~/content_editor/extensions/list_item';
|
||||
|
|
@ -8,6 +9,8 @@ import TableRow from '~/content_editor/extensions/table_row';
|
|||
import TableHeader from '~/content_editor/extensions/table_header';
|
||||
import { createTestEditor } from '../test_utils';
|
||||
|
||||
const eventHub = createEventHub();
|
||||
|
||||
describe('content_editor/extensions/table', () => {
|
||||
let tiptapEditor;
|
||||
let doc;
|
||||
|
|
@ -21,7 +24,15 @@ describe('content_editor/extensions/table', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
tiptapEditor = createTestEditor({
|
||||
extensions: [Table, TableCell, TableRow, TableHeader, BulletList, Bold, ListItem],
|
||||
extensions: [
|
||||
Table.configure({ eventHub }),
|
||||
TableCell,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
BulletList,
|
||||
Bold,
|
||||
ListItem,
|
||||
],
|
||||
});
|
||||
|
||||
({
|
||||
|
|
@ -47,13 +58,16 @@ describe('content_editor/extensions/table', () => {
|
|||
it('triggers a warning (just once) if the table is markdown, but the changes in the document will render an HTML table instead', () => {
|
||||
tiptapEditor.commands.setContent(initialDoc.toJSON());
|
||||
|
||||
tiptapEditor.on('alert', mockAlert);
|
||||
eventHub.$on('alert', mockAlert);
|
||||
|
||||
tiptapEditor.commands.setTextSelection({ from: 20, to: 22 });
|
||||
tiptapEditor.commands.toggleBulletList();
|
||||
|
||||
jest.advanceTimersByTime(1001);
|
||||
expect(mockAlert).toHaveBeenCalled();
|
||||
expect(mockAlert).toHaveBeenCalledWith({
|
||||
message: expect.any(String),
|
||||
variant: 'warning',
|
||||
});
|
||||
|
||||
mockAlert.mockReset();
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
UNAVAILABLE_FEATURE_INTRO_TEXT,
|
||||
UNAVAILABLE_USER_FEATURE_TEXT,
|
||||
} from '~/packages_and_registries/settings/project/constants';
|
||||
import SettingsSection from '~/vue_shared/components/settings/settings_section.vue';
|
||||
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
|
||||
|
||||
import {
|
||||
|
|
@ -37,13 +38,14 @@ describe('Cleanup image tags project settings', () => {
|
|||
|
||||
const findFormComponent = () => wrapper.findComponent(ContainerExpirationPolicyForm);
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findTitle = () => wrapper.findByTestId('title');
|
||||
const findDescription = () => wrapper.findByTestId('description');
|
||||
const findSettingsSectionComponent = () => wrapper.findComponent(SettingsSection);
|
||||
const findDescription = () => wrapper.findByTestId('settings-section-description');
|
||||
|
||||
const mountComponent = (provide = defaultProvidedValues, config) => {
|
||||
wrapper = shallowMountExtended(component, {
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
SettingsSection,
|
||||
},
|
||||
provide,
|
||||
...config,
|
||||
|
|
@ -91,7 +93,7 @@ describe('Cleanup image tags project settings', () => {
|
|||
await waitForPromises();
|
||||
|
||||
expect(findFormComponent().exists()).toBe(true);
|
||||
expect(findTitle().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_TITLE);
|
||||
expect(findSettingsSectionComponent().props('heading')).toBe(CONTAINER_CLEANUP_POLICY_TITLE);
|
||||
expect(findDescription().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_DESCRIPTION);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import Vue, { nextTick } from 'vue';
|
|||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { GlCard, GlLoadingIcon } from 'jest/packages_and_registries/shared/stubs';
|
||||
import { GlLoadingIcon } from 'jest/packages_and_registries/shared/stubs';
|
||||
import component from '~/packages_and_registries/settings/project/components/container_expiration_policy_form.vue';
|
||||
import { UPDATE_SETTINGS_ERROR_MESSAGE } from '~/packages_and_registries/settings/project/constants';
|
||||
import updateContainerExpirationPolicyMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_container_expiration_policy.mutation.graphql';
|
||||
|
|
@ -62,7 +62,6 @@ describe('Container Expiration Policy Settings Form', () => {
|
|||
} = {}) => {
|
||||
wrapper = shallowMount(component, {
|
||||
stubs: {
|
||||
GlCard,
|
||||
GlLoadingIcon,
|
||||
GlSprintf,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import WorkItemTitle from '~/work_items/components/work_item_title.vue';
|
|||
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
|
||||
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
|
||||
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
|
||||
import WorkItemCrmContacts from '~/work_items/components/work_item_crm_contacts.vue';
|
||||
import { WORK_ITEM_TYPE_ENUM_EPIC } from '~/work_items/constants';
|
||||
import namespaceWorkItemTypesQuery from '~/work_items/graphql/namespace_work_item_types.query.graphql';
|
||||
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
|
||||
|
|
@ -51,6 +52,7 @@ describe('Create work item component', () => {
|
|||
const findDescriptionWidget = () => wrapper.findComponent(WorkItemDescription);
|
||||
const findAssigneesWidget = () => wrapper.findComponent(WorkItemAssignees);
|
||||
const findLabelsWidget = () => wrapper.findComponent(WorkItemLabels);
|
||||
const findCrmContactsWidget = () => wrapper.findComponent(WorkItemCrmContacts);
|
||||
const findSelect = () => wrapper.findComponent(GlFormSelect);
|
||||
const findConfidentialCheckbox = () => wrapper.find('[data-testid="confidential-checkbox"]');
|
||||
const findCreateWorkItemView = () => wrapper.find('[data-testid="create-work-item-view"]');
|
||||
|
|
@ -296,5 +298,9 @@ describe('Create work item component', () => {
|
|||
it('renders the work item labels widget', () => {
|
||||
expect(findLabelsWidget().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the work item CRM contacts widget', () => {
|
||||
expect(findCrmContactsWidget().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import createNoteMutation from '~/work_items/graphql/notes/create_work_item_note
|
|||
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
|
||||
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
|
||||
import DiscussionReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
|
||||
import ResolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue';
|
||||
import {
|
||||
createWorkItemNoteResponse,
|
||||
workItemByIidResponseFactory,
|
||||
|
|
@ -34,6 +35,7 @@ describe('Work item add note', () => {
|
|||
const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
|
||||
const findReplyPlaceholder = () => wrapper.findComponent(DiscussionReplyPlaceholder);
|
||||
const findWorkItemLockedComponent = () => wrapper.findComponent(WorkItemCommentLocked);
|
||||
const findResolveDiscussionButton = () => wrapper.findComponent(ResolveDiscussionButton);
|
||||
|
||||
const createComponent = async ({
|
||||
mutationHandler = mutationSuccessHandler,
|
||||
|
|
@ -46,6 +48,11 @@ describe('Work item add note', () => {
|
|||
isGroup = false,
|
||||
workItemType = 'Task',
|
||||
isInternalThread = false,
|
||||
isNewDiscussion = false,
|
||||
isDiscussionResolved = false,
|
||||
isDiscussionResolvable = false,
|
||||
isResolving = false,
|
||||
hasReplies = false,
|
||||
} = {}) => {
|
||||
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
|
||||
if (signedIn) {
|
||||
|
|
@ -70,6 +77,11 @@ describe('Work item add note', () => {
|
|||
markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
|
||||
autocompleteDataSources: {},
|
||||
isInternalThread,
|
||||
isNewDiscussion,
|
||||
isDiscussionResolved,
|
||||
isDiscussionResolvable,
|
||||
isResolving,
|
||||
hasReplies,
|
||||
},
|
||||
stubs: {
|
||||
WorkItemCommentLocked,
|
||||
|
|
@ -297,4 +309,58 @@ describe('Work item add note', () => {
|
|||
expect(findCommentForm().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Resolve Discussion button', () => {
|
||||
it('renders resolve discussion button when discussion is resolvable', async () => {
|
||||
await createComponent({
|
||||
isDiscussionResolvable: true,
|
||||
isEditing: false,
|
||||
});
|
||||
|
||||
expect(findResolveDiscussionButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render resolve discussion button when discussion is not resolvable', async () => {
|
||||
await createComponent({
|
||||
isDiscussionResolvable: false,
|
||||
isEditing: false,
|
||||
});
|
||||
|
||||
expect(findResolveDiscussionButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render resolve discussion button when it is a new discussion', async () => {
|
||||
await createComponent({
|
||||
isDiscussionResolvable: false,
|
||||
isEditing: false,
|
||||
isNewDiscussion: true,
|
||||
});
|
||||
|
||||
expect(findResolveDiscussionButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('emits `resolve` event when resolve discussion button is clicked', async () => {
|
||||
await createComponent({
|
||||
isDiscussionResolvable: true,
|
||||
isEditing: false,
|
||||
});
|
||||
|
||||
findResolveDiscussionButton().vm.$emit('onClick');
|
||||
|
||||
expect(wrapper.emitted('resolve')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('passes correct props to resolve discussion button', async () => {
|
||||
await createComponent({
|
||||
isDiscussionResolvable: true,
|
||||
isDiscussionResolved: false,
|
||||
isResolving: true,
|
||||
isEditing: false,
|
||||
});
|
||||
|
||||
const resolveButton = findResolveDiscussionButton();
|
||||
expect(resolveButton.props('isResolving')).toBe(true);
|
||||
expect(resolveButton.props('buttonTitle')).toBe('Resolve thread');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { GlFormCheckbox, GlIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { createMockDirective } from 'helpers/vue_mock_directive';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import * as autosave from '~/lib/utils/autosave';
|
||||
|
|
@ -35,11 +35,12 @@ describe('Work item comment form component', () => {
|
|||
|
||||
const findCommentFieldLayout = () => wrapper.findComponent(CommentFieldLayout);
|
||||
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
|
||||
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
|
||||
const findConfirmButton = () => wrapper.find('[data-testid="confirm-button"]');
|
||||
const findCancelButton = () => wrapper.findByTestId('cancel-button');
|
||||
const findConfirmButton = () => wrapper.findByTestId('confirm-button');
|
||||
const findInternalNoteCheckbox = () => wrapper.findComponent(GlFormCheckbox);
|
||||
const findInternalNoteTooltipIcon = () => wrapper.findComponent(GlIcon);
|
||||
const findWorkItemToggleStateButton = () => wrapper.findComponent(WorkItemStateToggle);
|
||||
const findToggleResolveCheckbox = () => wrapper.findByTestId('toggle-resolve-checkbox');
|
||||
|
||||
const createComponent = ({
|
||||
isSubmitting = false,
|
||||
|
|
@ -48,8 +49,11 @@ describe('Work item comment form component', () => {
|
|||
workItemState = STATE_OPEN,
|
||||
workItemType = 'Task',
|
||||
isGroup = false,
|
||||
hasReplies = false,
|
||||
isDiscussionResolved = false,
|
||||
isDiscussionResolvable = false,
|
||||
} = {}) => {
|
||||
wrapper = shallowMount(WorkItemCommentForm, {
|
||||
wrapper = shallowMountExtended(WorkItemCommentForm, {
|
||||
provide: {
|
||||
isGroup,
|
||||
},
|
||||
|
|
@ -66,6 +70,9 @@ describe('Work item comment form component', () => {
|
|||
markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
|
||||
autocompleteDataSources: {},
|
||||
isNewDiscussion,
|
||||
isDiscussionResolvable,
|
||||
isDiscussionResolved,
|
||||
hasReplies,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: createMockDirective('gl-tooltip'),
|
||||
|
|
@ -288,4 +295,42 @@ describe('Work item comment form component', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Toggle Resolve checkbox', () => {
|
||||
it('does not render when used as a top level comment/discussion', () => {
|
||||
createComponent({ isNewDiscussion: true });
|
||||
|
||||
expect(findToggleResolveCheckbox().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders when used as a reply and the discussion is resolvable', () => {
|
||||
createComponent({
|
||||
hasReplies: true,
|
||||
isDiscussionResolvable: true,
|
||||
});
|
||||
|
||||
expect(findToggleResolveCheckbox().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('emits the `toggleResolve` event on submitForm when resolved', async () => {
|
||||
createComponent({
|
||||
hasReplies: true,
|
||||
isDiscussionResolvable: true,
|
||||
isDiscussionResolved: false,
|
||||
});
|
||||
|
||||
findToggleResolveCheckbox().vm.$emit('input', true);
|
||||
findMarkdownEditor().vm.$emit('input', 'new comment');
|
||||
|
||||
findConfirmButton().vm.$emit('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('submitForm')).toEqual([
|
||||
[{ commentText: 'new comment', isNoteInternal: false }],
|
||||
]);
|
||||
|
||||
expect(wrapper.emitted('toggleResolveDiscussion')).toEqual([[]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
|
||||
import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
|
||||
import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
|
||||
import WorkItemNoteReplying from '~/work_items/components/notes/work_item_note_replying.vue';
|
||||
import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue';
|
||||
import toggleWorkItemNoteResolveDiscussion from '~/work_items/graphql/notes/toggle_work_item_note_resolve_discussion.mutation.graphql';
|
||||
import {
|
||||
mockWorkItemCommentNote,
|
||||
mockToggleResolveDiscussionResponse,
|
||||
mockWorkItemNotesResponseWithComments,
|
||||
} from 'jest/work_items/mock_data';
|
||||
import { WIDGET_TYPE_NOTES } from '~/work_items/constants';
|
||||
|
|
@ -17,6 +21,8 @@ const mockWorkItemNotesWidgetResponseWithComments =
|
|||
);
|
||||
|
||||
describe('Work Item Discussion', () => {
|
||||
Vue.use(VueApollo);
|
||||
|
||||
let wrapper;
|
||||
const mockWorkItemId = 'gid://gitlab/WorkItem/625';
|
||||
|
||||
|
|
@ -26,12 +32,20 @@ describe('Work Item Discussion', () => {
|
|||
const findWorkItemAddNote = () => wrapper.findComponent(WorkItemAddNote);
|
||||
const findWorkItemNoteReplying = () => wrapper.findComponent(WorkItemNoteReplying);
|
||||
|
||||
const toggleWorkItemResolveDiscussionHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockToggleResolveDiscussionResponse);
|
||||
|
||||
const createComponent = ({
|
||||
discussion = [mockWorkItemCommentNote],
|
||||
workItemId = mockWorkItemId,
|
||||
workItemType = 'Task',
|
||||
isExpandedOnLoad = true,
|
||||
} = {}) => {
|
||||
wrapper = shallowMount(WorkItemDiscussion, {
|
||||
apolloProvider: createMockApollo([
|
||||
[toggleWorkItemNoteResolveDiscussion, toggleWorkItemResolveDiscussionHandler],
|
||||
]),
|
||||
propsData: {
|
||||
fullPath: 'gitlab-org',
|
||||
discussion,
|
||||
|
|
@ -40,6 +54,7 @@ describe('Work Item Discussion', () => {
|
|||
workItemType,
|
||||
markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
|
||||
autocompleteDataSources: {},
|
||||
isExpandedOnLoad,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -148,4 +163,58 @@ describe('Work Item Discussion', () => {
|
|||
|
||||
expect(wrapper.emitted('error')).toEqual([[mockErrorText]]);
|
||||
});
|
||||
|
||||
describe('Resolve discussion', () => {
|
||||
beforeEach(() => {
|
||||
window.gon.current_user_id = 'gid://gitlab/User/1';
|
||||
window.gon.current_user_fullname = 'Administrator';
|
||||
});
|
||||
const resolvedDiscussionList =
|
||||
mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes.slice();
|
||||
resolvedDiscussionList.forEach((note) => {
|
||||
return {
|
||||
...note,
|
||||
discussion: {
|
||||
id: note.discussion.id,
|
||||
resolved: true,
|
||||
resolvable: true,
|
||||
resolvedBy: {
|
||||
id: 'gid://gitlab/User/1',
|
||||
name: 'Administrator',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('Resolved discussion is not expanded on default', () => {
|
||||
createComponent({
|
||||
discussion: resolvedDiscussionList,
|
||||
isExpandedOnLoad: false,
|
||||
});
|
||||
|
||||
expect(findToggleRepliesWidget().props('collapsed')).toBe(true);
|
||||
});
|
||||
|
||||
it('toggles resolved status when toggle icon is clicked from note header', async () => {
|
||||
createComponent({
|
||||
discussion:
|
||||
mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes.slice(),
|
||||
isExpandedOnLoad: true,
|
||||
});
|
||||
|
||||
const mainComment = findThreadAtIndex(0);
|
||||
|
||||
expect(findToggleRepliesWidget().props('collapsed')).toBe(false);
|
||||
|
||||
mainComment.vm.$emit('resolve');
|
||||
|
||||
expect(toggleWorkItemResolveDiscussionHandler).toHaveBeenCalled();
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findToggleRepliesWidget().props('collapsed')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import removeAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_
|
|||
import groupWorkItemNotesByIidQuery from '~/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql';
|
||||
import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
|
||||
import {
|
||||
mockWorkItemNotesResponseWithComments,
|
||||
mockAwardEmojiThumbsUp,
|
||||
mockWorkItemNotesResponseWithComments,
|
||||
} from 'jest/work_items/mock_data';
|
||||
import { EMOJI_THUMBSUP, EMOJI_THUMBSDOWN } from '~/work_items/constants';
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
|
|||
import WorkItemParent from '~/work_items/components/work_item_parent.vue';
|
||||
import WorkItemTimeTracking from '~/work_items/components/work_item_time_tracking.vue';
|
||||
import WorkItemDevelopment from '~/work_items/components/work_item_development/work_item_development.vue';
|
||||
import WorkItemCrmContacts from '~/work_items/components/work_item_crm_contacts.vue';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import WorkItemAttributesWrapper from '~/work_items/components/work_item_attributes_wrapper.vue';
|
||||
import { workItemResponseFactory } from '../mock_data';
|
||||
|
|
@ -25,6 +26,7 @@ describe('WorkItemAttributesWrapper component', () => {
|
|||
const findWorkItemTimeTracking = () => wrapper.findComponent(WorkItemTimeTracking);
|
||||
const findWorkItemParticipants = () => wrapper.findComponent(Participants);
|
||||
const findWorkItemDevelopment = () => wrapper.findComponent(WorkItemDevelopment);
|
||||
const findWorkItemCrmContacts = () => wrapper.findComponent(WorkItemCrmContacts);
|
||||
|
||||
const createComponent = ({
|
||||
workItem = workItemQueryResponse.data.workItem,
|
||||
|
|
@ -184,6 +186,34 @@ describe('WorkItemAttributesWrapper component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('CRM contacts widget', () => {
|
||||
describe('when workItemsAlpha FF is disabled', () => {
|
||||
it.each`
|
||||
description | crmContactsWidgetPresent | exists
|
||||
${'renders when widget is returned from API'} | ${true} | ${false}
|
||||
${'does not render when widget is not returned from API'} | ${false} | ${false}
|
||||
`('$description', ({ crmContactsWidgetPresent, exists }) => {
|
||||
const response = workItemResponseFactory({ crmContactsWidgetPresent });
|
||||
createComponent({ workItem: response.data.workItem });
|
||||
|
||||
expect(findWorkItemCrmContacts().exists()).toBe(exists);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when workItemsAlpha FF is enabled', () => {
|
||||
it.each`
|
||||
description | crmContactsWidgetPresent | exists
|
||||
${'renders when widget is returned from API'} | ${true} | ${true}
|
||||
${'does not render when widget is not returned from API'} | ${false} | ${false}
|
||||
`('$description', ({ crmContactsWidgetPresent, exists }) => {
|
||||
const response = workItemResponseFactory({ crmContactsWidgetPresent });
|
||||
createComponent({ workItem: response.data.workItem, workItemsAlpha: true });
|
||||
|
||||
expect(findWorkItemCrmContacts().exists()).toBe(exists);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('participants widget', () => {
|
||||
it.each`
|
||||
description | participantsWidgetPresent | exists
|
||||
|
|
|
|||
|
|
@ -0,0 +1,250 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
|
||||
import {
|
||||
TRACKING_CATEGORY_SHOW,
|
||||
I18N_WORK_ITEM_ERROR_FETCHING_CRM_CONTACTS,
|
||||
} from '~/work_items/constants';
|
||||
import searchQuery from '~/crm/contacts/components/graphql/get_group_contacts.query.graphql';
|
||||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
import WorkItemCrmContacts from '~/work_items/components/work_item_crm_contacts.vue';
|
||||
import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue';
|
||||
import {
|
||||
getGroupCrmContactsResponse,
|
||||
mockCrmContacts,
|
||||
updateWorkItemMutationResponseFactory,
|
||||
updateWorkItemMutationErrorResponse,
|
||||
workItemByIidResponseFactory,
|
||||
} from '../mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const workItemId = 'gid://gitlab/WorkItem/10';
|
||||
const mockItems = mockCrmContacts;
|
||||
|
||||
describe('WorkItemCrmContacts component', () => {
|
||||
/** @type {import('helpers/vue_test_utils_helper').ExtendedWrapper} */
|
||||
let wrapper;
|
||||
|
||||
const item1Id = mockItems[0].id;
|
||||
const item3Id = mockItems[2].id;
|
||||
|
||||
const searchQuerySuccessHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(getGroupCrmContactsResponse(mockItems));
|
||||
const errorHandler = jest.fn().mockRejectedValue('Error');
|
||||
const successUpdateWorkItemMutationHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(updateWorkItemMutationResponseFactory({ crmContacts: [mockItems[0]] }));
|
||||
|
||||
const createComponent = ({
|
||||
searchQueryHandler = searchQuerySuccessHandler,
|
||||
updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler,
|
||||
workItemIid = '1',
|
||||
items = [],
|
||||
} = {}) => {
|
||||
const workItemQueryResponse = workItemByIidResponseFactory({
|
||||
canUpdate: true,
|
||||
crmContacts: items,
|
||||
});
|
||||
const workItemQueryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
|
||||
|
||||
wrapper = shallowMountExtended(WorkItemCrmContacts, {
|
||||
apolloProvider: createMockApollo([
|
||||
[searchQuery, searchQueryHandler],
|
||||
[updateWorkItemMutation, updateWorkItemMutationHandler],
|
||||
[workItemByIidQuery, workItemQueryHandler],
|
||||
]),
|
||||
propsData: {
|
||||
workItemId,
|
||||
workItemIid,
|
||||
fullPath: 'test-project-path',
|
||||
workItemType: 'Task',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findWorkItemSidebarDropdownWidget = () =>
|
||||
wrapper.findComponent(WorkItemSidebarDropdownWidget);
|
||||
const findAllItems = () => wrapper.findAllByTestId('contact');
|
||||
const findAllGroups = () => wrapper.findAllByTestId('organization');
|
||||
const findItem = () => findAllItems().at(0);
|
||||
|
||||
const showDropdown = () => {
|
||||
findWorkItemSidebarDropdownWidget().vm.$emit('dropdownShown');
|
||||
};
|
||||
|
||||
const updateItems = async (items) => {
|
||||
findWorkItemSidebarDropdownWidget().vm.$emit('searchStarted');
|
||||
await waitForPromises();
|
||||
|
||||
findWorkItemSidebarDropdownWidget().vm.$emit('updateValue', items);
|
||||
};
|
||||
|
||||
const getMutationInput = (contactIds) => {
|
||||
return {
|
||||
input: {
|
||||
id: workItemId,
|
||||
crmContactsWidget: {
|
||||
contactIds,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
it('renders the work item sidebar dropdown widget with default props', async () => {
|
||||
createComponent();
|
||||
await waitForPromises();
|
||||
|
||||
expect(findWorkItemSidebarDropdownWidget().props()).toMatchObject({
|
||||
dropdownLabel: 'Contacts',
|
||||
canUpdate: true,
|
||||
dropdownName: 'crm-contacts',
|
||||
updateInProgress: false,
|
||||
toggleDropdownText: '0 contacts',
|
||||
headerText: 'Select contacts',
|
||||
multiSelect: true,
|
||||
itemValue: [],
|
||||
});
|
||||
expect(findAllItems()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders the items when they are already present', async () => {
|
||||
createComponent({ items: mockItems });
|
||||
await waitForPromises();
|
||||
|
||||
expect(findWorkItemSidebarDropdownWidget().props('itemValue')).toStrictEqual(
|
||||
mockItems.map(({ id }) => id),
|
||||
);
|
||||
expect(findAllItems()).toHaveLength(3);
|
||||
expect(findAllGroups()).toHaveLength(2);
|
||||
expect(findItem().text()).toContain("Jenee O'Reilly");
|
||||
expect(findItem().text()).toContain("Jenee.O'Reilly-12@example.org");
|
||||
expect(findItem().text()).toContain('Anderson LLC-4');
|
||||
});
|
||||
|
||||
it.each`
|
||||
expectedAssertion | searchTerm | handler | result
|
||||
${'when dropdown is shown'} | ${''} | ${searchQuerySuccessHandler} | ${3}
|
||||
${'when correct input is entered'} | ${'Item 1'} | ${jest.fn().mockResolvedValue(getGroupCrmContactsResponse([mockItems[0]]))} | ${1}
|
||||
${'and shows no matching results when incorrect input is entered'} | ${'Item 2'} | ${jest.fn().mockResolvedValue(getGroupCrmContactsResponse([]))} | ${0}
|
||||
`('calls search label query $expectedAssertion', async ({ searchTerm, result, handler }) => {
|
||||
createComponent({
|
||||
searchQueryHandler: handler,
|
||||
});
|
||||
|
||||
showDropdown();
|
||||
await findWorkItemSidebarDropdownWidget().vm.$emit('searchStarted', searchTerm);
|
||||
|
||||
expect(findWorkItemSidebarDropdownWidget().props('loading')).toBe(true);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findWorkItemSidebarDropdownWidget().props('loading')).toBe(false);
|
||||
|
||||
expect(
|
||||
findWorkItemSidebarDropdownWidget()
|
||||
.props('listItems')
|
||||
.flatMap(({ options }) => options),
|
||||
).toHaveLength(result);
|
||||
expect(handler).toHaveBeenCalledWith({
|
||||
groupFullPath: 'test-project-path',
|
||||
searchTerm,
|
||||
nextPageCursor: '',
|
||||
prevPageCursor: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('emits error event if search query fails', async () => {
|
||||
createComponent({ searchQueryHandler: errorHandler });
|
||||
showDropdown();
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('error')).toEqual([[I18N_WORK_ITEM_ERROR_FETCHING_CRM_CONTACTS]]);
|
||||
});
|
||||
|
||||
it('update items when items are updated', async () => {
|
||||
createComponent();
|
||||
showDropdown();
|
||||
updateItems([item1Id]);
|
||||
await waitForPromises();
|
||||
|
||||
expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith(getMutationInput([item1Id]));
|
||||
});
|
||||
|
||||
it('clears all items when updateValue has no items', async () => {
|
||||
createComponent();
|
||||
findWorkItemSidebarDropdownWidget().vm.$emit('updateValue', []);
|
||||
await waitForPromises();
|
||||
|
||||
expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith(getMutationInput([]));
|
||||
});
|
||||
|
||||
it('shows selected items, then organizations then orphans', async () => {
|
||||
createComponent();
|
||||
updateItems([item1Id, item3Id]);
|
||||
await waitForPromises();
|
||||
showDropdown();
|
||||
|
||||
const [item1, item2, item3] = mockItems;
|
||||
|
||||
const selected = [{ text: `${item1.firstName} ${item1.lastName}`, value: item1.id }];
|
||||
const unselected = [{ text: `${item2.firstName} ${item2.lastName}`, value: item2.id }];
|
||||
const orphans = [{ text: `${item3.firstName} ${item3.lastName}`, value: item3.id }];
|
||||
|
||||
expect(findWorkItemSidebarDropdownWidget().props('listItems')).toEqual([
|
||||
{ options: selected, text: 'Selected' },
|
||||
{ options: unselected, text: 'Anderson LLC-4' },
|
||||
{ options: orphans, text: 'No organization' },
|
||||
]);
|
||||
});
|
||||
|
||||
describe('tracking', () => {
|
||||
let trackingSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
trackingSpy = null;
|
||||
});
|
||||
|
||||
it('tracks editing the items on dropdown widget updateValue', async () => {
|
||||
showDropdown();
|
||||
updateItems([item1Id]);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_contacts', {
|
||||
category: TRACKING_CATEGORY_SHOW,
|
||||
label: 'item_contact',
|
||||
property: 'type_Task',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.each`
|
||||
errorType | expectedErrorMessage | failureHandler
|
||||
${'graphql error'} | ${'Something went wrong while updating the work item. Please try again.'} | ${jest.fn().mockResolvedValue(updateWorkItemMutationErrorResponse)}
|
||||
${'network error'} | ${'Something went wrong while updating the work item. Please try again.'} | ${jest.fn().mockRejectedValue(new Error())}
|
||||
`(
|
||||
'emits an error when there is a $errorType',
|
||||
async ({ expectedErrorMessage, failureHandler }) => {
|
||||
createComponent({
|
||||
updateWorkItemMutationHandler: failureHandler,
|
||||
});
|
||||
|
||||
updateItems([item1Id]);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('error')).toEqual([[expectedErrorMessage]]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -24,10 +24,10 @@ import {
|
|||
workItemQueryResponse,
|
||||
mockWorkItemNotesByIidResponse,
|
||||
mockMoreWorkItemNotesResponse,
|
||||
mockWorkItemNotesResponseWithComments,
|
||||
workItemNotesCreateSubscriptionResponse,
|
||||
workItemNotesUpdateSubscriptionResponse,
|
||||
workItemNotesDeleteSubscriptionResponse,
|
||||
mockWorkItemNotesResponseWithComments,
|
||||
} from '../mock_data';
|
||||
|
||||
const mockWorkItemId = workItemQueryResponse.data.workItem.id;
|
||||
|
|
|
|||
|
|
@ -187,11 +187,11 @@ describe('WorkItemParent component', () => {
|
|||
expect(findSidebarDropdownWidget().exists()).toBe(true);
|
||||
expect(findSidebarDropdownWidget().props()).toMatchObject({
|
||||
listItems: [],
|
||||
headerText: 'Assign parent',
|
||||
headerText: 'Select parent',
|
||||
loading: false,
|
||||
searchable: true,
|
||||
infiniteScroll: false,
|
||||
resetButtonLabel: 'Unassign',
|
||||
resetButtonLabel: 'Clear',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,50 @@ export const mockLabels = [
|
|||
},
|
||||
];
|
||||
|
||||
export const mockCrmContacts = [
|
||||
{
|
||||
__typename: 'CustomerRelationsContact',
|
||||
id: 'gid://gitlab/CustomerRelations::Contact/213',
|
||||
firstName: 'Jenee',
|
||||
lastName: "O'Reilly",
|
||||
email: "Jenee.O'Reilly-12@example.org",
|
||||
phone: null,
|
||||
description: null,
|
||||
active: true,
|
||||
organization: {
|
||||
__typename: 'CustomerRelationsOrganization',
|
||||
id: 'gid://gitlab/CustomerRelations::Organization/55',
|
||||
name: 'Anderson LLC-4',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'CustomerRelationsContact',
|
||||
id: 'gid://gitlab/CustomerRelations::Contact/216',
|
||||
firstName: 'Kassie',
|
||||
lastName: 'Oberbrunner',
|
||||
email: 'Kassie.Oberbrunner-15@example.org',
|
||||
phone: null,
|
||||
description: null,
|
||||
active: true,
|
||||
organization: {
|
||||
__typename: 'CustomerRelationsOrganization',
|
||||
id: 'gid://gitlab/CustomerRelations::Organization/55',
|
||||
name: 'Anderson LLC-4',
|
||||
},
|
||||
},
|
||||
{
|
||||
__typename: 'CustomerRelationsContact',
|
||||
id: 'gid://gitlab/CustomerRelations::Contact/232',
|
||||
firstName: 'Liza',
|
||||
lastName: 'Osinski',
|
||||
email: 'Liza.Osinski-31@example.org',
|
||||
phone: null,
|
||||
description: null,
|
||||
active: true,
|
||||
organization: null,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockMilestone = {
|
||||
__typename: 'Milestone',
|
||||
id: 'gid://gitlab/Milestone/30',
|
||||
|
|
@ -917,8 +961,10 @@ export const workItemResponseFactory = ({
|
|||
labelsWidgetPresent = true,
|
||||
hierarchyWidgetPresent = true,
|
||||
linkedItemsWidgetPresent = true,
|
||||
crmContactsWidgetPresent = true,
|
||||
colorWidgetPresent = true,
|
||||
labels = mockLabels,
|
||||
crmContacts = mockCrmContacts,
|
||||
allowsScopedLabels = false,
|
||||
lastEditedAt = null,
|
||||
lastEditedBy = null,
|
||||
|
|
@ -1248,6 +1294,18 @@ export const workItemResponseFactory = ({
|
|||
: {
|
||||
type: 'MOCK TYPE',
|
||||
},
|
||||
crmContactsWidgetPresent
|
||||
? {
|
||||
__typename: 'WorkItemWidgetCrmContacts',
|
||||
type: 'CRM_CONTACTS',
|
||||
contacts: {
|
||||
nodes: crmContacts,
|
||||
__typename: 'CustomerRelationsContactConnection',
|
||||
},
|
||||
}
|
||||
: {
|
||||
type: 'MOCK TYPE',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -2634,6 +2692,23 @@ export const getProjectLabelsResponse = (labels) => ({
|
|||
},
|
||||
});
|
||||
|
||||
export const getGroupCrmContactsResponse = (contacts) => ({
|
||||
data: {
|
||||
group: {
|
||||
id: '1',
|
||||
contacts: {
|
||||
nodes: contacts,
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
endCursor: null,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const mockIterationWidgetResponse = {
|
||||
description: 'Iteration description',
|
||||
dueDate: '2022-07-19',
|
||||
|
|
@ -2841,6 +2916,10 @@ export const mockWorkItemNotesResponse = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723561234',
|
||||
resolved: false,
|
||||
resolvable: false,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
userPermissions: {
|
||||
adminNote: false,
|
||||
|
|
@ -2942,6 +3021,10 @@ export const mockWorkItemNotesResponse = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
|
||||
resolved: false,
|
||||
resolvable: false,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
userPermissions: {
|
||||
adminNote: false,
|
||||
|
|
@ -3053,6 +3136,10 @@ export const mockWorkItemNotesByIidResponse = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723561234',
|
||||
resolved: false,
|
||||
resolvable: false,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
userPermissions: {
|
||||
adminNote: false,
|
||||
|
|
@ -3107,6 +3194,10 @@ export const mockWorkItemNotesByIidResponse = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723568765',
|
||||
resolved: false,
|
||||
resolvable: false,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
userPermissions: {
|
||||
adminNote: false,
|
||||
|
|
@ -3161,6 +3252,10 @@ export const mockWorkItemNotesByIidResponse = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
|
||||
resolved: false,
|
||||
resolvable: false,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
userPermissions: {
|
||||
adminNote: false,
|
||||
|
|
@ -3271,6 +3366,10 @@ export const mockMoreWorkItemNotesResponse = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da1112356a59e',
|
||||
resolved: false,
|
||||
resolvable: false,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
userPermissions: {
|
||||
adminNote: false,
|
||||
|
|
@ -3325,6 +3424,10 @@ export const mockMoreWorkItemNotesResponse = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da1272356a59e',
|
||||
resolved: false,
|
||||
resolvable: false,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
userPermissions: {
|
||||
adminNote: false,
|
||||
|
|
@ -3378,6 +3481,10 @@ export const mockMoreWorkItemNotesResponse = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
|
||||
resolved: false,
|
||||
resolvable: false,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
userPermissions: {
|
||||
adminNote: false,
|
||||
|
|
@ -3449,6 +3556,9 @@ export const createWorkItemNoteResponse = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
|
||||
resolved: false,
|
||||
resolvable: true,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
author: {
|
||||
|
|
@ -3506,6 +3616,10 @@ export const mockWorkItemCommentNote = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
|
||||
resolved: false,
|
||||
resolvable: true,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
userPermissions: {
|
||||
adminNote: false,
|
||||
|
|
@ -3603,6 +3717,9 @@ export const mockWorkItemNotesResponseWithComments = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
|
||||
resolved: false,
|
||||
resolvable: true,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
author: {
|
||||
|
|
@ -3645,6 +3762,9 @@ export const mockWorkItemNotesResponseWithComments = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
|
||||
resolved: false,
|
||||
resolvable: true,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
author: {
|
||||
|
|
@ -3696,6 +3816,10 @@ export const mockWorkItemNotesResponseWithComments = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
|
||||
resolved: false,
|
||||
resolvable: true,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
userPermissions: {
|
||||
adminNote: false,
|
||||
|
|
@ -3771,6 +3895,10 @@ export const workItemNotesCreateSubscriptionResponse = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
|
||||
resolved: false,
|
||||
resolvable: false,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
userPermissions: {
|
||||
adminNote: false,
|
||||
|
|
@ -3851,6 +3979,10 @@ export const workItemNotesUpdateSubscriptionResponse = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
|
||||
resolved: false,
|
||||
resolvable: false,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
userPermissions: {
|
||||
adminNote: false,
|
||||
|
|
@ -3908,6 +4040,9 @@ export const workItemSystemNoteWithMetadata = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/7d4a46ea0525e2eeed451f7b718b0ebe73205374',
|
||||
resolved: false,
|
||||
resolvable: false,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
author: {
|
||||
|
|
@ -4001,6 +4136,9 @@ export const workItemNotesWithSystemNotesWithChangedDescription = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/aa72f4c2f3eef66afa6d79a805178801ce4bd89f',
|
||||
resolved: false,
|
||||
resolvable: false,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
author: {
|
||||
|
|
@ -4066,6 +4204,9 @@ export const workItemNotesWithSystemNotesWithChangedDescription = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/a7d3cf7bd72f7a98f802845f538af65cb11a02cc',
|
||||
resolved: false,
|
||||
resolvable: false,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
author: {
|
||||
|
|
@ -4131,6 +4272,9 @@ export const workItemNotesWithSystemNotesWithChangedDescription = {
|
|||
authorIsContributor: false,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/391eed1ee0a258cc966a51dde900424f3b51b95d',
|
||||
resolved: false,
|
||||
resolvable: false,
|
||||
resolvedBy: null,
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
author: {
|
||||
|
|
@ -4672,6 +4816,14 @@ export const createWorkItemQueryResponse = {
|
|||
totalTimeSpent: 0,
|
||||
__typename: 'WorkItemWidgetTimeTracking',
|
||||
},
|
||||
{
|
||||
type: 'CRM_CONTACTS',
|
||||
contacts: {
|
||||
nodes: [],
|
||||
__typename: 'CustomerRelationsContactConnection',
|
||||
},
|
||||
__typename: 'WorkItemWidgetCrmContacts',
|
||||
},
|
||||
],
|
||||
__typename: 'WorkItem',
|
||||
},
|
||||
|
|
@ -4679,3 +4831,121 @@ export const createWorkItemQueryResponse = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockToggleResolveDiscussionResponse = {
|
||||
data: {
|
||||
discussionToggleResolve: {
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/c4be5bec43a737e0966dbc4c040b1517e7febfa9',
|
||||
notes: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/DiscussionNote/2506',
|
||||
body: 'test3',
|
||||
bodyHtml: '<p data-sourcepos="1:1-1:5" dir="auto">test3</p>',
|
||||
system: false,
|
||||
internal: false,
|
||||
systemNoteIconName: null,
|
||||
createdAt: '2024-07-19T05:52:01Z',
|
||||
lastEditedAt: '2024-07-26T10:06:02Z',
|
||||
url: 'http://127.0.0.1:3000/flightjs/Flight/-/issues/134#note_2506',
|
||||
authorIsContributor: false,
|
||||
maxAccessLevelOfAuthor: 'Owner',
|
||||
lastEditedBy: null,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/c4be5bec43a737e0966dbc4c040b1517e7febfa9',
|
||||
resolved: true,
|
||||
resolvable: true,
|
||||
resolvedBy: {
|
||||
id: 'gid://gitlab/User/1',
|
||||
name: 'Administrator',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
author: {
|
||||
id: 'gid://gitlab/User/1',
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/258d8dc916db8cea2cafb6c3cd0cb0246efe061421dbd83ec3a350428cabda4f?s=80&d=identicon',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webUrl: 'http://127.0.0.1:3000/root',
|
||||
webPath: '/root',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
awardEmoji: {
|
||||
nodes: [],
|
||||
__typename: 'AwardEmojiConnection',
|
||||
},
|
||||
userPermissions: {
|
||||
adminNote: true,
|
||||
awardEmoji: true,
|
||||
readNote: true,
|
||||
createNote: true,
|
||||
resolveNote: true,
|
||||
repositionNote: true,
|
||||
__typename: 'NotePermissions',
|
||||
},
|
||||
systemNoteMetadata: null,
|
||||
__typename: 'Note',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/DiscussionNote/2539',
|
||||
body: 'comment',
|
||||
bodyHtml: '<p data-sourcepos="1:1-1:7" dir="auto">comment</p>',
|
||||
system: false,
|
||||
internal: false,
|
||||
systemNoteIconName: null,
|
||||
createdAt: '2024-07-23T05:07:46Z',
|
||||
lastEditedAt: '2024-07-26T10:06:02Z',
|
||||
url: 'http://127.0.0.1:3000/flightjs/Flight/-/issues/134#note_2539',
|
||||
authorIsContributor: false,
|
||||
maxAccessLevelOfAuthor: 'Owner',
|
||||
lastEditedBy: null,
|
||||
discussion: {
|
||||
id: 'gid://gitlab/Discussion/c4be5bec43a737e0966dbc4c040b1517e7febfa9',
|
||||
resolved: true,
|
||||
resolvable: true,
|
||||
resolvedBy: {
|
||||
id: 'gid://gitlab/User/1',
|
||||
name: 'Administrator',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
author: {
|
||||
id: 'gid://gitlab/User/1',
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/258d8dc916db8cea2cafb6c3cd0cb0246efe061421dbd83ec3a350428cabda4f?s=80&d=identicon',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webUrl: 'http://127.0.0.1:3000/root',
|
||||
webPath: '/root',
|
||||
__typename: 'UserCore',
|
||||
},
|
||||
awardEmoji: {
|
||||
nodes: [],
|
||||
__typename: 'AwardEmojiConnection',
|
||||
},
|
||||
userPermissions: {
|
||||
adminNote: true,
|
||||
awardEmoji: true,
|
||||
readNote: true,
|
||||
createNote: true,
|
||||
resolveNote: true,
|
||||
repositionNote: true,
|
||||
__typename: 'NotePermissions',
|
||||
},
|
||||
systemNoteMetadata: null,
|
||||
__typename: 'Note',
|
||||
},
|
||||
],
|
||||
__typename: 'NoteConnection',
|
||||
},
|
||||
__typename: 'Discussion',
|
||||
},
|
||||
errors: [],
|
||||
__typename: 'DiscussionToggleResolvePayload',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_
|
|||
import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
|
||||
import removeAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql';
|
||||
import {
|
||||
mockWorkItemNotesResponseWithComments,
|
||||
mockAwardEmojiThumbsUp,
|
||||
mockAwardEmojiThumbsDown,
|
||||
mockWorkItemNotesResponseWithComments,
|
||||
} from '../mock_data';
|
||||
|
||||
function getWorkItem(data) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ RSpec.describe Projects::PipelineHelper do
|
|||
end
|
||||
|
||||
describe '#js_pipeline_header_data' do
|
||||
before do
|
||||
allow(helper).to receive(:current_user).and_return(user)
|
||||
end
|
||||
|
||||
subject(:pipeline_header_data) { helper.js_pipeline_header_data(project, pipeline) }
|
||||
|
||||
it 'returns pipeline header data' do
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ issues:
|
|||
- email
|
||||
- issuable_resource_links
|
||||
- synced_epic
|
||||
- observability_metrics
|
||||
work_item_type:
|
||||
- issues
|
||||
- namespace
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Projects::Prometheus::Metrics::DestroyService, feature_category: :metrics do
|
||||
let(:metric) { create(:prometheus_metric) }
|
||||
|
||||
subject { described_class.new(metric) }
|
||||
|
||||
it 'destroys metric' do
|
||||
subject.execute
|
||||
|
||||
expect(PrometheusMetric.find_by(id: metric.id)).to be_nil
|
||||
end
|
||||
end
|
||||
|
|
@ -34,7 +34,7 @@ require 'rspec-parameterized'
|
|||
require 'shoulda/matchers'
|
||||
require 'test_prof/recipes/rspec/let_it_be'
|
||||
require 'test_prof/factory_default'
|
||||
require 'test_prof/factory_prof/nate_heckler'
|
||||
require 'test_prof/factory_prof/nate_heckler' if ENV.fetch('ENABLE_FACTORY_PROF', 'true') == 'true'
|
||||
require 'parslet/rig/rspec'
|
||||
require 'axe-rspec'
|
||||
|
||||
|
|
|
|||
|
|
@ -8075,7 +8075,6 @@
|
|||
- './spec/services/projects/overwrite_project_service_spec.rb'
|
||||
- './spec/services/projects/participants_service_spec.rb'
|
||||
- './spec/services/projects/prometheus/alerts/notify_service_spec.rb'
|
||||
- './spec/services/projects/prometheus/metrics/destroy_service_spec.rb'
|
||||
- './spec/services/projects/protect_default_branch_service_spec.rb'
|
||||
- './spec/services/projects/readme_renderer_service_spec.rb'
|
||||
- './spec/services/projects/record_target_platforms_service_spec.rb'
|
||||
|
|
|
|||
|
|
@ -49,4 +49,43 @@ RSpec.shared_examples 'rich text editor - common' do
|
|||
expect(page).not_to have_text('An error occurred')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'block content is added to a table' do
|
||||
it 'converts a markdown table to HTML and shows a warning for it' do
|
||||
click_on 'Add a table'
|
||||
|
||||
switch_to_content_editor
|
||||
type_in_content_editor '* list item'
|
||||
|
||||
expect(page).to have_text(
|
||||
"Tables containing block elements (like multiple paragraphs, lists or blockquotes) are not \
|
||||
supported in Markdown and will be converted to HTML."
|
||||
)
|
||||
|
||||
switch_to_markdown_editor
|
||||
expect(page.find('textarea').value).to include '<table>
|
||||
<tr>
|
||||
<th>header</th>
|
||||
<th>header</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
* list item
|
||||
</td>
|
||||
</tr>
|
||||
</table>'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ RSpec.shared_examples 'work items rolled up dates' do
|
|||
context 'when updating child work item dates' do
|
||||
it 'rolled up child dates' do
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/473408
|
||||
allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(102)
|
||||
allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(105)
|
||||
|
||||
child_title = 'A child issue'
|
||||
add_new_child(title: child_title, start_date: '2020-12-01', due_date: '2020-12-02')
|
||||
|
|
@ -192,7 +192,7 @@ RSpec.shared_examples 'work items rolled up dates' do
|
|||
context 'when removing all children' do
|
||||
it 'rolled up child dates' do
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/473408
|
||||
allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(108)
|
||||
allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(114)
|
||||
|
||||
add_new_child(title: 'child issue 1', start_date: '2020-11-01', due_date: '2020-12-02')
|
||||
add_new_child(title: 'child issue 2', start_date: '2020-12-01', due_date: '2021-01-02')
|
||||
|
|
@ -249,6 +249,9 @@ RSpec.shared_examples 'work items rolled up dates' do
|
|||
end
|
||||
|
||||
it 'rolled up child dates' do
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/473408
|
||||
allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(101)
|
||||
|
||||
add_existing_child(child_work_item)
|
||||
|
||||
within work_item_rolledup_dates_selector do
|
||||
|
|
|
|||
|
|
@ -524,7 +524,7 @@ RSpec.shared_examples 'work items parent' do |type|
|
|||
|
||||
find_and_click_edit(work_item_parent_selector)
|
||||
|
||||
find_and_click_clear(work_item_parent_selector, 'Unassign')
|
||||
find_and_click_clear(work_item_parent_selector, 'Clear')
|
||||
|
||||
expect(find(work_item_parent_selector)).to have_content('None')
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ require (
|
|||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.0
|
||||
github.com/BurntSushi/toml v1.4.0
|
||||
github.com/alecthomas/chroma/v2 v2.14.0
|
||||
github.com/aws/aws-sdk-go v1.54.18
|
||||
github.com/aws/aws-sdk-go v1.55.2
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dlclark/regexp2 v1.11.2
|
||||
github.com/getsentry/raven-go v0.2.0
|
||||
|
|
@ -20,12 +20,12 @@ require (
|
|||
github.com/jpillora/backoff v1.0.0
|
||||
github.com/mitchellh/copystructure v1.2.0
|
||||
github.com/prometheus/client_golang v1.19.1-0.20240328134234-93cf5d4f5f78
|
||||
github.com/redis/go-redis/v9 v9.5.3
|
||||
github.com/redis/go-redis/v9 v9.6.1
|
||||
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/smartystreets/goconvey v1.8.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
gitlab.com/gitlab-org/gitaly/v16 v16.11.6
|
||||
gitlab.com/gitlab-org/gitaly/v16 v16.11.7
|
||||
gitlab.com/gitlab-org/labkit v1.21.0
|
||||
go.uber.org/goleak v1.3.0
|
||||
gocloud.dev v0.37.0
|
||||
|
|
|
|||
|
|
@ -96,8 +96,8 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc
|
|||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/aws/aws-sdk-go v1.44.256/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||
github.com/aws/aws-sdk-go v1.54.18 h1:t8DGtN8A2wEiazoJxeDbfPsbxCKtjoRLuO7jBSgJzo4=
|
||||
github.com/aws/aws-sdk-go v1.54.18/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go v1.55.2 h1:/2OFM8uFfK9e+cqHTw9YPrvTzIXT2XkFGXRM7WbJb7E=
|
||||
github.com/aws/aws-sdk-go v1.55.2/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.25.3 h1:xYiLpZTQs1mzvz5PaI6uR0Wh57ippuEthxS4iK5v0n0=
|
||||
github.com/aws/aws-sdk-go-v2 v1.25.3/go.mod h1:35hUlJVYd+M++iLI3ALmVwMOyRYMmRqUXpTtRGW+K9I=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 h1:gTK2uhtAPtFcdRRJilZPx8uJLL2J85xK11nKtWL0wfU=
|
||||
|
|
@ -417,8 +417,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
|
|||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/prometheus/prometheus v0.50.1 h1:N2L+DYrxqPh4WZStU+o1p/gQlBaqFbcLBTjlp3vpdXw=
|
||||
github.com/prometheus/prometheus v0.50.1/go.mod h1:FvE8dtQ1Ww63IlyKBn1V4s+zMwF9kHkVNkQBR1pM4CU=
|
||||
github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU=
|
||||
github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
|
||||
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
|
|
@ -482,8 +482,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
|
|||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
|
||||
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
gitlab.com/gitlab-org/gitaly/v16 v16.11.6 h1:4oOHqKO7/fCVrdyuONg4E6ZlX6vu3H7+YLdHQMfb1a8=
|
||||
gitlab.com/gitlab-org/gitaly/v16 v16.11.6/go.mod h1:lJizRUtXRd1SBHjNbbbL9OsGN4TiugvfRBd8bIsdWI0=
|
||||
gitlab.com/gitlab-org/gitaly/v16 v16.11.7 h1:csL3bPTHIEE0147aCo1HFgNLMgmHwlwr1Ns65OlcMuE=
|
||||
gitlab.com/gitlab-org/gitaly/v16 v16.11.7/go.mod h1:lJizRUtXRd1SBHjNbbbL9OsGN4TiugvfRBd8bIsdWI0=
|
||||
gitlab.com/gitlab-org/labkit v1.21.0 h1:hLmdBDtXjD1yOmZ+uJOac3a5Tlo83QaezwhES4IYik4=
|
||||
gitlab.com/gitlab-org/labkit v1.21.0/go.mod h1:zeATDAaSBelPcPLbTTq8J3ZJEHyPTLVBM1q3nva+/W4=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
|
|
|
|||
|
|
@ -1359,10 +1359,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.109.0.tgz#af953d8114768343034f1f02bc8e2d93eb613c65"
|
||||
integrity sha512-MmBTsco2LIh/l16iJQy6R98YDOlE3C++AE0Z1+KCpAX/3+fLAmULx2sWp+JnmM0ws8J0LaeLN6+vWiPaEWA16Q==
|
||||
|
||||
"@gitlab/ui@87.4.0":
|
||||
version "87.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-87.4.0.tgz#6261602c944801e0fda84d8a85199862ec327320"
|
||||
integrity sha512-Ap6i17oNrV0pgjz+AFvsStZslizCZmLyUnyxAW5JGBKA1QpvR+2pgfnBjKyrpgoHy5kmVDQoOKy84PsErT0U1g==
|
||||
"@gitlab/ui@87.6.1":
|
||||
version "87.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-87.6.1.tgz#f6d9fad3689890d430fe179e3d58f8e75f00369e"
|
||||
integrity sha512-5Vfl64zrJaXAeJDrPNtpv5yFygGqceucJb4Fe8RvMEiY9fbSwnwlU22BAv45b5mWEiY/C3wsURLDwNw16Xt2pg==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "1.4.3"
|
||||
echarts "^5.3.2"
|
||||
|
|
|
|||
Loading…
Reference in New Issue