Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
8d9b19289a
commit
1f92a1f626
|
@ -3,7 +3,6 @@
|
|||
*/
|
||||
export default {
|
||||
files: [
|
||||
'app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue',
|
||||
'app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_comment_form.vue',
|
||||
'app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_edit_note.vue',
|
||||
'app/assets/javascripts/admin/statistics_panel/components/app.vue',
|
||||
|
|
|
@ -382,6 +382,9 @@ Dangerfile
|
|||
/ee/app/models/ee/merge_request.rb
|
||||
/ee/app/services/merge_requests/
|
||||
/ee/app/services/ee/merge_requests
|
||||
!/ee/app/services/merge_requests/merge_audit_event_service.rb
|
||||
!/ee/app/services/merge_requests/create_from_vulnerability_data_service.rb
|
||||
!/ee/app/services/merge_requests/policy_violations_detected_audit_event_service.rb
|
||||
/ee/app/workers/merge_requests/
|
||||
/ee/app/workers/ee/merge_requests
|
||||
/ee/app/workers/merge_request_reset_approvals_worker.rb
|
||||
|
|
|
@ -38,7 +38,6 @@ Gitlab/NoFindInWorkers:
|
|||
- 'app/workers/issuable_export_csv_worker.rb'
|
||||
- 'app/workers/issues/placement_worker.rb'
|
||||
- 'app/workers/members_destroyer/unassign_issuables_worker.rb'
|
||||
- 'app/workers/merge_requests/handle_assignees_change_worker.rb'
|
||||
- 'app/workers/merge_worker.rb'
|
||||
- 'app/workers/namespaces/root_statistics_worker.rb'
|
||||
- 'app/workers/namespaces/schedule_aggregation_worker.rb'
|
||||
|
|
|
@ -3702,7 +3702,6 @@ Layout/LineLength:
|
|||
- 'spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb'
|
||||
- 'spec/services/clusters/management/validate_management_project_permissions_service_spec.rb'
|
||||
- 'spec/services/clusters/update_service_spec.rb'
|
||||
- 'spec/services/commits/cherry_pick_service_spec.rb'
|
||||
- 'spec/services/concerns/exclusive_lease_guard_spec.rb'
|
||||
- 'spec/services/concerns/merge_requests/assigns_merge_params_spec.rb'
|
||||
- 'spec/services/concerns/rate_limited_service_spec.rb'
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.23.1
|
||||
0.24.0
|
||||
|
|
|
@ -365,7 +365,7 @@
|
|||
{"name":"kubeclient","version":"4.11.0","platform":"ruby","checksum":"4985fcd749fb8c364a668a8350a49821647f03aa52d9ee6cbc582beb8e883fcc"},
|
||||
{"name":"language_server-protocol","version":"3.17.0.3","platform":"ruby","checksum":"3d5c58c02f44a20d972957a9febe386d7e7468ab3900ce6bd2b563dd910c6b3f"},
|
||||
{"name":"launchy","version":"2.5.2","platform":"ruby","checksum":"8aa0441655aec5514008e1d04892c2de3ba57bd337afb984568da091121a241b"},
|
||||
{"name":"lefthook","version":"1.11.13","platform":"ruby","checksum":"64eee33daf516f27b8948c9734e495d740ba9aa211aadcf34c35cfb1008fbaa2"},
|
||||
{"name":"lefthook","version":"1.11.14","platform":"ruby","checksum":"c11c55f5096f5d38068b66be8a33143899b7095f28a8145c9adf0b3eb611c098"},
|
||||
{"name":"letter_opener","version":"1.10.0","platform":"ruby","checksum":"2ff33f2e3b5c3c26d1959be54b395c086ca6d44826e8bf41a14ff96fdf1bdbb2"},
|
||||
{"name":"letter_opener_web","version":"3.0.0","platform":"ruby","checksum":"3f391efe0e8b9b24becfab5537dfb17a5cf5eb532038f947daab58cb4b749860"},
|
||||
{"name":"libyajl2","version":"2.1.0","platform":"ruby","checksum":"aa5df6c725776fc050c8418450de0f7c129cb7200b811907c4c0b3b5c0aea0ef"},
|
||||
|
@ -464,8 +464,8 @@
|
|||
{"name":"open4","version":"1.3.4","platform":"ruby","checksum":"a1df037310624ecc1ea1d81264b11c83e96d0c3c1c6043108d37d396dcd0f4b1"},
|
||||
{"name":"openid_connect","version":"2.3.1","platform":"ruby","checksum":"5d808380cff80d78e3d3d54cfaebe2d6461d835c674faa29e2314a402c1b2182"},
|
||||
{"name":"opensearch-ruby","version":"3.4.0","platform":"ruby","checksum":"0a8621686bed3c59b4c23e08cbaef873685a3fe4568e9d2703155ca92b8ca05d"},
|
||||
{"name":"openssl","version":"3.2.0","platform":"java","checksum":"9a1c870b4175ee90bcd233b5041a5ca8072f5f5f06d404ab3c786aa31daffa02"},
|
||||
{"name":"openssl","version":"3.2.0","platform":"ruby","checksum":"3c4bb8760977b4becd2819c6c2569bcf5c6f48b32b9f7a4ce1fd37f996378d14"},
|
||||
{"name":"openssl","version":"3.3.0","platform":"java","checksum":"1755479b8f17a507f0d01020365b4ba96484c033ae88aef410f69d3240261657"},
|
||||
{"name":"openssl","version":"3.3.0","platform":"ruby","checksum":"ff3a573fc97ab30f69483fddc80029f91669bf36532859bd182d1836f45aee79"},
|
||||
{"name":"openssl-signature_algorithm","version":"1.3.0","platform":"ruby","checksum":"a3b40b5e8276162d4a6e50c7c97cdaf1446f9b2c3946a6fa2c14628e0c957e80"},
|
||||
{"name":"opentelemetry-api","version":"1.2.5","platform":"ruby","checksum":"ab3d9a0566cd2ee068ade40e840bc973383ab8568e693c0c5712f0c789122cc9"},
|
||||
{"name":"opentelemetry-common","version":"0.21.0","platform":"ruby","checksum":"fe891a44583a20bc3217b324aec76d066504494951682d391cfd57d40cd01c98"},
|
||||
|
|
|
@ -1116,7 +1116,7 @@ GEM
|
|||
language_server-protocol (3.17.0.3)
|
||||
launchy (2.5.2)
|
||||
addressable (~> 2.8)
|
||||
lefthook (1.11.13)
|
||||
lefthook (1.11.14)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
letter_opener_web (3.0.0)
|
||||
|
@ -1324,7 +1324,7 @@ GEM
|
|||
opensearch-ruby (3.4.0)
|
||||
faraday (>= 1.0, < 3)
|
||||
multi_json (>= 1.0)
|
||||
openssl (3.2.0)
|
||||
openssl (3.3.0)
|
||||
openssl-signature_algorithm (1.3.0)
|
||||
openssl (> 2.0)
|
||||
opentelemetry-api (1.2.5)
|
||||
|
|
|
@ -365,7 +365,7 @@
|
|||
{"name":"kubeclient","version":"4.11.0","platform":"ruby","checksum":"4985fcd749fb8c364a668a8350a49821647f03aa52d9ee6cbc582beb8e883fcc"},
|
||||
{"name":"language_server-protocol","version":"3.17.0.3","platform":"ruby","checksum":"3d5c58c02f44a20d972957a9febe386d7e7468ab3900ce6bd2b563dd910c6b3f"},
|
||||
{"name":"launchy","version":"2.5.2","platform":"ruby","checksum":"8aa0441655aec5514008e1d04892c2de3ba57bd337afb984568da091121a241b"},
|
||||
{"name":"lefthook","version":"1.11.13","platform":"ruby","checksum":"64eee33daf516f27b8948c9734e495d740ba9aa211aadcf34c35cfb1008fbaa2"},
|
||||
{"name":"lefthook","version":"1.11.14","platform":"ruby","checksum":"c11c55f5096f5d38068b66be8a33143899b7095f28a8145c9adf0b3eb611c098"},
|
||||
{"name":"letter_opener","version":"1.10.0","platform":"ruby","checksum":"2ff33f2e3b5c3c26d1959be54b395c086ca6d44826e8bf41a14ff96fdf1bdbb2"},
|
||||
{"name":"letter_opener_web","version":"3.0.0","platform":"ruby","checksum":"3f391efe0e8b9b24becfab5537dfb17a5cf5eb532038f947daab58cb4b749860"},
|
||||
{"name":"libyajl2","version":"2.1.0","platform":"ruby","checksum":"aa5df6c725776fc050c8418450de0f7c129cb7200b811907c4c0b3b5c0aea0ef"},
|
||||
|
@ -464,8 +464,8 @@
|
|||
{"name":"open4","version":"1.3.4","platform":"ruby","checksum":"a1df037310624ecc1ea1d81264b11c83e96d0c3c1c6043108d37d396dcd0f4b1"},
|
||||
{"name":"openid_connect","version":"2.3.1","platform":"ruby","checksum":"5d808380cff80d78e3d3d54cfaebe2d6461d835c674faa29e2314a402c1b2182"},
|
||||
{"name":"opensearch-ruby","version":"3.4.0","platform":"ruby","checksum":"0a8621686bed3c59b4c23e08cbaef873685a3fe4568e9d2703155ca92b8ca05d"},
|
||||
{"name":"openssl","version":"3.2.0","platform":"java","checksum":"9a1c870b4175ee90bcd233b5041a5ca8072f5f5f06d404ab3c786aa31daffa02"},
|
||||
{"name":"openssl","version":"3.2.0","platform":"ruby","checksum":"3c4bb8760977b4becd2819c6c2569bcf5c6f48b32b9f7a4ce1fd37f996378d14"},
|
||||
{"name":"openssl","version":"3.3.0","platform":"java","checksum":"1755479b8f17a507f0d01020365b4ba96484c033ae88aef410f69d3240261657"},
|
||||
{"name":"openssl","version":"3.3.0","platform":"ruby","checksum":"ff3a573fc97ab30f69483fddc80029f91669bf36532859bd182d1836f45aee79"},
|
||||
{"name":"openssl-signature_algorithm","version":"1.3.0","platform":"ruby","checksum":"a3b40b5e8276162d4a6e50c7c97cdaf1446f9b2c3946a6fa2c14628e0c957e80"},
|
||||
{"name":"opentelemetry-api","version":"1.2.5","platform":"ruby","checksum":"ab3d9a0566cd2ee068ade40e840bc973383ab8568e693c0c5712f0c789122cc9"},
|
||||
{"name":"opentelemetry-common","version":"0.21.0","platform":"ruby","checksum":"fe891a44583a20bc3217b324aec76d066504494951682d391cfd57d40cd01c98"},
|
||||
|
|
|
@ -1110,7 +1110,7 @@ GEM
|
|||
language_server-protocol (3.17.0.3)
|
||||
launchy (2.5.2)
|
||||
addressable (~> 2.8)
|
||||
lefthook (1.11.13)
|
||||
lefthook (1.11.14)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
letter_opener_web (3.0.0)
|
||||
|
@ -1318,7 +1318,7 @@ GEM
|
|||
opensearch-ruby (3.4.0)
|
||||
faraday (>= 1.0, < 3)
|
||||
multi_json (>= 1.0)
|
||||
openssl (3.2.0)
|
||||
openssl (3.3.0)
|
||||
openssl-signature_algorithm (1.3.0)
|
||||
openssl (> 2.0)
|
||||
opentelemetry-api (1.2.5)
|
||||
|
|
|
@ -105,6 +105,7 @@ export default {
|
|||
'contextCommits',
|
||||
'contextCommitsLoadingError',
|
||||
'selectedCommits',
|
||||
// eslint-disable-next-line vue/no-unused-properties -- searchText is mapped from Vuex and used in handleSearchCommits(),
|
||||
'searchText',
|
||||
'toRemoveCommits',
|
||||
]),
|
||||
|
@ -281,9 +282,6 @@ export default {
|
|||
handleModalHide() {
|
||||
this.resetModalState();
|
||||
},
|
||||
shouldShowInputDateFormat(value) {
|
||||
return ['Committed-before', 'Committed-after'].indexOf(value) !== -1;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
<script>
|
||||
import { GlSprintf, GlIcon, GlButton } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mapState } from 'pinia';
|
||||
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
|
@ -11,6 +9,7 @@ import {
|
|||
getLineClasses,
|
||||
} from '~/notes/components/multiline_comment_utils';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import { useNotes } from '~/notes/store/legacy_notes';
|
||||
import resolvedStatusMixin from '../mixins/resolved_status';
|
||||
|
||||
export default {
|
||||
|
@ -28,7 +27,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapState(useLegacyDiffs, ['getDiffFileByHash']),
|
||||
...mapGetters(['getDiscussion']),
|
||||
...mapState(useNotes, ['getDiscussion']),
|
||||
iconName() {
|
||||
return this.isDiffDiscussion || this.draft.line_code ? 'doc-text' : 'comment';
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mapState } from 'pinia';
|
||||
import { sprintf, s__, __ } from '~/locale';
|
||||
import { useNotes } from '~/notes/store/legacy_notes';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
|
@ -21,7 +21,7 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isDiscussionResolved']),
|
||||
...mapState(useNotes, ['isDiscussionResolved']),
|
||||
resolvedStatusMessage() {
|
||||
let message;
|
||||
const discussionResolved = this.isDiscussionResolved(
|
||||
|
|
|
@ -26,14 +26,18 @@ export const TAB_NAMES = Object.freeze({
|
|||
|
||||
export default {
|
||||
components: {
|
||||
AlertDetailsTable,
|
||||
DescriptionComponent,
|
||||
GlTab,
|
||||
GlTabs,
|
||||
HighlightBar,
|
||||
...(gon.features?.hideIncidentManagementFeatures ? {} : { TimelineTab }),
|
||||
...(gon.features?.hideIncidentManagementFeatures
|
||||
? {}
|
||||
: {
|
||||
AlertDetailsTable,
|
||||
TimelineTab,
|
||||
IncidentMetricTab: () =>
|
||||
import('ee_component/issues/show/components/incidents/incident_metric_tab.vue'),
|
||||
}),
|
||||
},
|
||||
inject: ['fullPath', 'iid', 'hasLinkedAlerts', 'uploadMetricsFeatureAvailable'],
|
||||
i18n: incidentTabsI18n,
|
||||
|
@ -75,10 +79,10 @@ export default {
|
|||
tabMapping() {
|
||||
const availableTabs = [TAB_NAMES.SUMMARY];
|
||||
|
||||
if (this.uploadMetricsFeatureAvailable) {
|
||||
if (this.uploadMetricsFeatureAvailable && this.showIncidentManagementFeatures) {
|
||||
availableTabs.push(TAB_NAMES.METRICS);
|
||||
}
|
||||
if (this.hasLinkedAlerts) {
|
||||
if (this.hasLinkedAlerts && this.showIncidentManagementFeatures) {
|
||||
availableTabs.push(TAB_NAMES.ALERTS);
|
||||
}
|
||||
|
||||
|
@ -158,18 +162,20 @@ export default {
|
|||
<description-component v-bind="$attrs" v-on="$listeners" />
|
||||
</gl-tab>
|
||||
<gl-tab
|
||||
v-if="uploadMetricsFeatureAvailable"
|
||||
v-if="uploadMetricsFeatureAvailable && showIncidentManagementFeatures"
|
||||
:title="$options.i18n.metricsTitle"
|
||||
data-testid="metrics-tab"
|
||||
>
|
||||
<!-- eslint-disable-next-line vue/no-undef-components -->
|
||||
<incident-metric-tab />
|
||||
</gl-tab>
|
||||
<gl-tab
|
||||
v-if="hasLinkedAlerts"
|
||||
v-if="hasLinkedAlerts && showIncidentManagementFeatures"
|
||||
class="alert-management-details"
|
||||
:title="$options.i18n.alertsTitle"
|
||||
data-testid="alert-details-tab"
|
||||
>
|
||||
<!-- eslint-disable-next-line vue/no-undef-components -->
|
||||
<alert-details-table :alert="alert" :loading="loading" />
|
||||
</gl-tab>
|
||||
<gl-tab
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<script>
|
||||
import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
|
||||
import { escape } from 'lodash';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions } from 'vuex';
|
||||
import { mapActions } from 'pinia';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import { truncateSha } from '~/lib/utils/text_utility';
|
||||
import { s__, __, sprintf } from '~/locale';
|
||||
import { FILE_DIFF_POSITION_TYPE } from '~/diffs/constants';
|
||||
import { useNotes } from '~/notes/store/legacy_notes';
|
||||
import NoteEditedText from './note_edited_text.vue';
|
||||
import NoteHeader from './note_header.vue';
|
||||
import ToggleRepliesWidget from './toggle_replies_widget.vue';
|
||||
|
@ -102,7 +102,7 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleDiscussion']),
|
||||
...mapActions(useNotes, ['toggleDiscussion']),
|
||||
toggleDiscussionHandler() {
|
||||
this.toggleDiscussion({ discussionId: this.discussion.id });
|
||||
},
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<script>
|
||||
import { GlButton, GlSkeletonLoader } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions } from 'vuex';
|
||||
import { mapState } from 'pinia';
|
||||
import { mapState, mapActions } from 'pinia';
|
||||
import { mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
|
||||
|
@ -13,6 +11,7 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
|
|||
import { isCollapsed } from '~/diffs/utils/diff_file';
|
||||
import { FILE_DIFF_POSITION_TYPE, IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import { useNotes } from '~/notes/store/legacy_notes';
|
||||
|
||||
const FIRST_CHAR_REGEX = /^(\+|-| )/;
|
||||
|
||||
|
@ -117,7 +116,7 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchDiscussionDiffLines']),
|
||||
...mapActions(useNotes, ['fetchDiscussionDiffLines']),
|
||||
fetchDiff() {
|
||||
this.error = false;
|
||||
this.fetchDiscussionDiffLines(this.discussion)
|
||||
|
|
|
@ -7,8 +7,7 @@ import {
|
|||
GlButtonGroup,
|
||||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import { mapActions, mapState } from 'pinia';
|
||||
import { InternalEvents } from '~/tracking';
|
||||
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
|
||||
import { __ } from '~/locale';
|
||||
|
@ -19,6 +18,7 @@ import {
|
|||
MR_FILTER_TRACKING_USER_COMMENTS,
|
||||
MR_FILTER_TRACKING_BOT_COMMENTS,
|
||||
} from '~/notes/constants';
|
||||
import { useNotes } from '~/notes/store/legacy_notes';
|
||||
|
||||
const filterOptionToTrackingEventMap = {
|
||||
comments: MR_FILTER_TRACKING_USER_COMMENTS,
|
||||
|
@ -46,10 +46,7 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
mergeRequestFilters: (state) => state.notes.mergeRequestFilters,
|
||||
discussionSortOrder: (state) => state.notes.discussionSortOrder,
|
||||
}),
|
||||
...mapState(useNotes, ['mergeRequestFilters', 'discussionSortOrder']),
|
||||
selectedFilterText() {
|
||||
const { length } = this.mergeRequestFilters;
|
||||
|
||||
|
@ -80,7 +77,7 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateMergeRequestFilters', 'setDiscussionSortDirection']),
|
||||
...mapActions(useNotes, ['updateMergeRequestFilters', 'setDiscussionSortDirection']),
|
||||
updateSortDirection() {
|
||||
this.setDiscussionSortDirection({
|
||||
direction: this.isSortAsc ? 'desc' : 'asc',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { GlFormSelect, GlSprintf } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions } from 'vuex';
|
||||
import { mapActions } from 'pinia';
|
||||
import { useNotes } from '~/notes/store/legacy_notes';
|
||||
import { getSymbol, getLineClasses } from './multiline_comment_utils';
|
||||
|
||||
export default {
|
||||
|
@ -48,7 +48,7 @@ export default {
|
|||
this.setSelectedCommentPosition();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setSelectedCommentPosition']),
|
||||
...mapActions(useNotes, ['setSelectedCommentPosition']),
|
||||
getSymbol({ type }) {
|
||||
return getSymbol(type);
|
||||
},
|
||||
|
|
|
@ -6,8 +6,7 @@ import {
|
|||
GlDisclosureDropdownItem,
|
||||
GlDisclosureDropdownGroup,
|
||||
} from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { mapActions, mapState } from 'pinia';
|
||||
import Api from '~/api';
|
||||
import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
|
||||
import { createAlert } from '~/alert';
|
||||
|
@ -16,6 +15,7 @@ import { __, sprintf } from '~/locale';
|
|||
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
|
||||
import { splitCamelCase } from '~/lib/utils/text_utility';
|
||||
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
|
||||
import { useNotes } from '~/notes/store/legacy_notes';
|
||||
import ReplyButton from './note_actions/reply_button.vue';
|
||||
import TimelineEventButton from './note_actions/timeline_event_button.vue';
|
||||
|
||||
|
@ -146,8 +146,12 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['isPromoteCommentToTimelineEventInProgress']),
|
||||
...mapGetters(['getUserDataByProp', 'getNoteableData', 'canUserAddIncidentTimelineEvents']),
|
||||
...mapState(useNotes, [
|
||||
'isPromoteCommentToTimelineEventInProgress',
|
||||
'getUserDataByProp',
|
||||
'getNoteableData',
|
||||
'canUserAddIncidentTimelineEvents',
|
||||
]),
|
||||
shouldShowActionsDropdown() {
|
||||
return this.currentUserId;
|
||||
},
|
||||
|
@ -204,7 +208,7 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleAwardRequest', 'promoteCommentToTimelineEvent']),
|
||||
...mapActions(useNotes, ['toggleAwardRequest', 'promoteCommentToTimelineEvent']),
|
||||
onEdit() {
|
||||
this.$emit('handleEdit');
|
||||
},
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<script>
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { mapActions, mapState } from 'pinia';
|
||||
import { createAlert } from '~/alert';
|
||||
import { __ } from '~/locale';
|
||||
import AwardsList from '~/vue_shared/components/awards_list.vue';
|
||||
import { useNotes } from '~/notes/store/legacy_notes';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -37,13 +37,13 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['getUserData']),
|
||||
...mapState(useNotes, ['getUserData']),
|
||||
isAuthoredByMe() {
|
||||
return this.noteAuthorId === this.getUserData.id;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleAwardRequest']),
|
||||
...mapActions(useNotes, ['toggleAwardRequest']),
|
||||
handleAward(awardName) {
|
||||
const data = {
|
||||
endpoint: this.toggleAwardPath,
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
<script>
|
||||
import { escape } from 'lodash';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
mapActions as mapVuexActions,
|
||||
mapGetters as mapVuexGetters,
|
||||
mapState as mapVuexState,
|
||||
} from 'vuex';
|
||||
import { mapState } from 'pinia';
|
||||
import { mapState, mapActions } from 'pinia';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
|
||||
import { renderGFM } from '~/behaviors/markdown/render_gfm';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import { useMrNotes } from '~/mr_notes/store/legacy_mr_notes';
|
||||
import { useNotes } from '~/notes/store/legacy_notes';
|
||||
import NoteAttachment from './note_attachment.vue';
|
||||
import NoteAwardsList from './note_awards_list.vue';
|
||||
import NoteEditedText from './note_edited_text.vue';
|
||||
|
@ -72,12 +67,14 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapVuexGetters(['getDiscussion', 'suggestionsCount', 'getSuggestionsFilePaths']),
|
||||
...mapState(useLegacyDiffs, ['suggestionCommitMessage']),
|
||||
...mapState(useMrNotes, ['failedToLoadMetadata']),
|
||||
...mapVuexState({
|
||||
batchSuggestionsInfo: (state) => state.notes.batchSuggestionsInfo,
|
||||
}),
|
||||
...mapState(useNotes, [
|
||||
'batchSuggestionsInfo',
|
||||
'getDiscussion',
|
||||
'suggestionsCount',
|
||||
'getSuggestionsFilePaths',
|
||||
]),
|
||||
discussion() {
|
||||
if (!this.note.isDraft) return {};
|
||||
|
||||
|
@ -157,7 +154,7 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapVuexActions([
|
||||
...mapActions(useNotes, [
|
||||
'submitSuggestion',
|
||||
'submitSuggestionBatch',
|
||||
'addSuggestionInfoToBatch',
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import { GlBadge, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { mapActions } from 'vuex';
|
||||
import { getActivePinia } from 'pinia';
|
||||
import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { TYPE_ACTIVITY, TYPE_COMMENT } from '~/import/constants';
|
||||
import { s__ } from '~/locale';
|
||||
|
@ -129,11 +128,10 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setTargetNoteHash']),
|
||||
updateTargetNoteHash() {
|
||||
if (this.$store) {
|
||||
this.setTargetNoteHash(this.noteTimestampLink);
|
||||
}
|
||||
async updateTargetNoteHash() {
|
||||
if (!getActivePinia()) return;
|
||||
const { useNotes } = await import('~/notes/store/legacy_notes');
|
||||
useNotes().setTargetNoteHash(this.noteTimestampLink);
|
||||
},
|
||||
handleUsernameMouseEnter() {
|
||||
this.$refs.authorNameLink.dispatchEvent(new Event('mouseenter'));
|
||||
|
|
|
@ -31,9 +31,14 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
tagCountText() {
|
||||
if (isEmpty(this.pageInfo)) {
|
||||
if (
|
||||
isEmpty(this.pageInfo) ||
|
||||
!Number.isInteger(this.pageInfo.total) ||
|
||||
this.pageInfo.total === 0
|
||||
) {
|
||||
return EMPTY_TAG_LABEL;
|
||||
}
|
||||
|
||||
return tagsCountText(this.pageInfo.total);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -146,7 +146,7 @@ export const fetchSidebarCount = ({ commit, state }, skipBlobs) => {
|
|||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
export const setQuery = async ({ state, commit, getters }, { key, value }) => {
|
||||
export const setQuery = async ({ state, commit, getters, dispatch }, { key, value }) => {
|
||||
commit(types.SET_QUERY, { key, value });
|
||||
|
||||
if (SIDEBAR_PARAMS.includes(key)) {
|
||||
|
@ -157,13 +157,32 @@ export const setQuery = async ({ state, commit, getters }, { key, value }) => {
|
|||
setDataToLS(LS_REGEX_HANDLE, value);
|
||||
}
|
||||
|
||||
if (state.searchType === SEARCH_TYPE_ZOEKT && getters.currentScope === SCOPE_BLOB) {
|
||||
const newUrl = setUrlParams({ ...state.query }, window.location.href, false, true);
|
||||
const isZoektSearch =
|
||||
state.searchType === SEARCH_TYPE_ZOEKT && getters.currentScope === SCOPE_BLOB;
|
||||
|
||||
if (isZoektSearch && key === 'search') {
|
||||
const shouldResetPage = state.query?.page > 1 || state.urlQuery?.page > 1;
|
||||
const query = shouldResetPage ? { ...state.query, page: 1 } : { ...state.query };
|
||||
const newUrl = setUrlParams(query, window.location.href, true, true);
|
||||
document.title = buildDocumentTitle(state.query.search);
|
||||
updateHistory({ state: state.query, title: state.query.search, url: newUrl, replace: false });
|
||||
|
||||
updateHistory({ state: query, title: state.query.search, url: newUrl, replace: true });
|
||||
|
||||
if (shouldResetPage) {
|
||||
commit(types.SET_QUERY, { key: 'page', value: 1 });
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
fetchSidebarCount({ state, commit });
|
||||
dispatch('fetchSidebarCount');
|
||||
}
|
||||
|
||||
if (isZoektSearch && key === 'page') {
|
||||
updateHistory({
|
||||
state: state.query,
|
||||
title: state.query.search,
|
||||
url: setUrlParams({ ...state.query }, window.location.href, true, true),
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlButton, GlTooltipDirective, GlAnimatedChevronLgDownUpIcon } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import { STATUS_CLOSED, STATUS_MERGED } from '~/issues/constants';
|
||||
import StatusIcon from './mr_widget_status_icon.vue';
|
||||
|
@ -8,6 +8,7 @@ import Actions from './action_buttons.vue';
|
|||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlAnimatedChevronLgDownUpIcon,
|
||||
StatusIcon,
|
||||
Actions,
|
||||
},
|
||||
|
@ -120,13 +121,14 @@ export default {
|
|||
:title="collapsed ? expandDetailsTooltip : collapseDetailsTooltip"
|
||||
:aria-label="collapsed ? expandDetailsTooltip : collapseDetailsTooltip"
|
||||
:aria-expanded="collapsed ? 'false' : 'true'"
|
||||
:icon="collapsed ? 'chevron-lg-down' : 'chevron-lg-up'"
|
||||
category="tertiary"
|
||||
size="small"
|
||||
class="gl-align-top"
|
||||
class="btn-icon"
|
||||
data-testid="widget-toggle"
|
||||
@click="() => $emit('toggle')"
|
||||
/>
|
||||
>
|
||||
<gl-animated-chevron-lg-down-up-icon :is-on="!collapsed" />
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
<!-- eslint-disable vue/multi-word-component-names -->
|
||||
<script>
|
||||
import { GlButton, GlLink, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
|
||||
import {
|
||||
GlButton,
|
||||
GlLink,
|
||||
GlTooltipDirective,
|
||||
GlLoadingIcon,
|
||||
GlAnimatedChevronLgDownUpIcon,
|
||||
} from '@gitlab/ui';
|
||||
import { kebabCase } from 'lodash';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { normalizeHeaders } from '~/lib/utils/common_utils';
|
||||
|
@ -40,6 +46,7 @@ export default {
|
|||
GlLink,
|
||||
GlButton,
|
||||
GlLoadingIcon,
|
||||
GlAnimatedChevronLgDownUpIcon,
|
||||
ContentRow,
|
||||
DynamicContent,
|
||||
DynamicScroller,
|
||||
|
@ -443,12 +450,14 @@ export default {
|
|||
:title="collapseButtonLabel"
|
||||
:aria-expanded="`${!isCollapsed}`"
|
||||
:aria-label="collapseButtonLabel"
|
||||
:icon="isCollapsed ? 'chevron-lg-down' : 'chevron-lg-up'"
|
||||
category="tertiary"
|
||||
data-testid="toggle-button"
|
||||
size="small"
|
||||
class="btn-icon"
|
||||
@click="toggleCollapsed"
|
||||
/>
|
||||
>
|
||||
<gl-animated-chevron-lg-down-up-icon :is-on="!isCollapsed" />
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -399,17 +399,24 @@ export default {
|
|||
</template>
|
||||
</gl-filtered-search>
|
||||
</div>
|
||||
<div
|
||||
:class="{
|
||||
'gl-flex gl-items-center gl-justify-between gl-gap-3': $scopedSlots['user-preference'],
|
||||
}"
|
||||
>
|
||||
<slot name="user-preference"></slot>
|
||||
<gl-sorting
|
||||
v-if="selectedSortOption"
|
||||
:sort-options="transformedSortOptions"
|
||||
:sort-by="sortById"
|
||||
:is-ascending="sortDirectionAscending"
|
||||
class="sort-dropdown-container gl-w-full sm:!gl-m-0 sm:gl-w-auto"
|
||||
dropdown-class="gl-grow"
|
||||
dropdown-toggle-class="gl-grow"
|
||||
dropdown-class="gl-grow"
|
||||
sort-direction-toggle-class="!gl-shrink !gl-grow-0"
|
||||
@sortByChange="handleSortByChange"
|
||||
@sortDirectionChange="handleSortDirectionChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -355,7 +355,11 @@ export default {
|
|||
@checked-input="handleAllIssuablesCheckedInput"
|
||||
@onFilter="$emit('filter', $event)"
|
||||
@onSort="$emit('sort', $event)"
|
||||
/>
|
||||
>
|
||||
<template #user-preference>
|
||||
<slot name="user-preference"></slot>
|
||||
</template>
|
||||
</filtered-search-bar>
|
||||
<gl-alert
|
||||
v-if="error"
|
||||
variant="danger"
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
<script>
|
||||
import {
|
||||
GlDisclosureDropdown,
|
||||
GlDisclosureDropdownItem,
|
||||
GlToggle,
|
||||
GlTooltipDirective,
|
||||
} from '@gitlab/ui';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { createAlert } from '~/alert';
|
||||
|
||||
import updateWorkItemsDisplaySettings from '../../graphql/update_user_preferences.mutation.graphql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDisclosureDropdown,
|
||||
GlDisclosureDropdownItem,
|
||||
GlToggle,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
inject: {
|
||||
isSignedIn: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
displayOptions: s__('WorkItems|Display options'),
|
||||
yourPreferences: s__('WorkItems|Your preferences'),
|
||||
openItemsInSidePanel: s__('WorkItems|Open items in side panel'),
|
||||
},
|
||||
props: {
|
||||
displaySettings: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isDropdownVisible: false,
|
||||
isLoading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tooltipText() {
|
||||
return !this.isDropdownVisible ? this.$options.i18n.displayOptions : '';
|
||||
},
|
||||
shouldOpenItemsInSidePanel() {
|
||||
return this.displaySettings?.shouldOpenItemsInSidePanel ?? true;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showDropdown() {
|
||||
this.isDropdownVisible = true;
|
||||
},
|
||||
hideDropdown() {
|
||||
this.isDropdownVisible = false;
|
||||
},
|
||||
async toggleSidePanelPreference() {
|
||||
const newDisplaySettings = {
|
||||
...this.displaySettings,
|
||||
shouldOpenItemsInSidePanel: !this.shouldOpenItemsInSidePanel,
|
||||
};
|
||||
|
||||
const input = {
|
||||
workItemsDisplaySettings: newDisplaySettings,
|
||||
};
|
||||
this.isLoading = true;
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: updateWorkItemsDisplaySettings,
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
});
|
||||
|
||||
this.$emit('displaySettingsChanged', newDisplaySettings);
|
||||
} catch (error) {
|
||||
createAlert({
|
||||
message: __('Something went wrong while saving the preference.'),
|
||||
captureError: true,
|
||||
error,
|
||||
});
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-disclosure-dropdown
|
||||
v-if="isSignedIn"
|
||||
v-gl-tooltip="tooltipText"
|
||||
icon="preferences"
|
||||
text-sr-only
|
||||
:toggle-text="$options.i18n.displayOptions"
|
||||
category="primary"
|
||||
no-caret
|
||||
placement="bottom-end"
|
||||
:auto-close="false"
|
||||
class="gl-mt-[10px] sm:gl-mt-0"
|
||||
@shown="showDropdown"
|
||||
@hidden="hideDropdown"
|
||||
>
|
||||
<div class="gl-mt-2">
|
||||
<span class="gl-pl-4 gl-text-sm gl-font-bold">{{ $options.i18n.yourPreferences }}</span>
|
||||
<gl-disclosure-dropdown-item
|
||||
class="work-item-dropdown-toggle"
|
||||
@action="toggleSidePanelPreference"
|
||||
>
|
||||
<template #list-item>
|
||||
<gl-toggle
|
||||
:value="shouldOpenItemsInSidePanel"
|
||||
:label="$options.i18n.openItemsInSidePanel"
|
||||
class="gl-justify-between"
|
||||
label-position="left"
|
||||
:is-loading="isLoading"
|
||||
/>
|
||||
</template>
|
||||
</gl-disclosure-dropdown-item>
|
||||
</div>
|
||||
</gl-disclosure-dropdown>
|
||||
</template>
|
|
@ -0,0 +1,8 @@
|
|||
query getUserWorkItemsDisplaySettingsPreferences {
|
||||
currentUser {
|
||||
id
|
||||
userPreferences {
|
||||
workItemsDisplaySettings
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
mutation updateWorkItemsDisplaySettings($input: UserPreferencesUpdateInput!) {
|
||||
userPreferencesUpdate(input: $input) {
|
||||
userPreferences {
|
||||
workItemsDisplaySettings
|
||||
}
|
||||
}
|
||||
}
|
|
@ -91,6 +91,7 @@ import CreateWorkItemModal from '../components/create_work_item_modal.vue';
|
|||
import WorkItemHealthStatus from '../components/work_item_health_status.vue';
|
||||
import WorkItemDrawer from '../components/work_item_drawer.vue';
|
||||
import WorkItemListHeading from '../components/work_item_list_heading.vue';
|
||||
import WorkItemUserPreferences from '../components/shared/work_item_list_preferences.vue';
|
||||
import {
|
||||
BASE_ALLOWED_CREATE_TYPES,
|
||||
DETAIL_VIEW_QUERY_PARAM_NAME,
|
||||
|
@ -102,6 +103,7 @@ import {
|
|||
WORK_ITEM_TYPE_NAME_KEY_RESULT,
|
||||
WORK_ITEM_TYPE_NAME_OBJECTIVE,
|
||||
} from '../constants';
|
||||
import getUserWorkItemsDisplaySettingsPreferences from '../graphql/get_user_preferences.query.graphql';
|
||||
|
||||
const EmojiToken = () =>
|
||||
import('~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue');
|
||||
|
@ -135,6 +137,7 @@ export default {
|
|||
CreateWorkItemModal,
|
||||
LocalBoard,
|
||||
WorkItemListHeading,
|
||||
WorkItemUserPreferences,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
inject: [
|
||||
|
@ -204,9 +207,27 @@ export default {
|
|||
initialLoadWasFiltered: false,
|
||||
showLocalBoard: false,
|
||||
namespaceId: null,
|
||||
displaySettings: {},
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
displaySettings: {
|
||||
query: getUserWorkItemsDisplaySettingsPreferences,
|
||||
update(data) {
|
||||
return (
|
||||
data?.currentUser?.userPreferences?.workItemsDisplaySettings ?? {
|
||||
shouldOpenItemsInSidePanel: true,
|
||||
}
|
||||
);
|
||||
},
|
||||
skip() {
|
||||
return !this.isSignedIn;
|
||||
},
|
||||
error(error) {
|
||||
this.error = __('An error occurred while getting work item user preference.');
|
||||
Sentry.captureException(error);
|
||||
},
|
||||
},
|
||||
workItemsFull: {
|
||||
query() {
|
||||
return getWorkItemsQuery;
|
||||
|
@ -335,6 +356,8 @@ export default {
|
|||
});
|
||||
},
|
||||
workItemDrawerEnabled() {
|
||||
const shouldOpenItemsInSidePanel = this.displaySettings?.shouldOpenItemsInSidePanel ?? true;
|
||||
if (!shouldOpenItemsInSidePanel) return false;
|
||||
if (this.glFeatures.workItemViewForIssues) {
|
||||
return true;
|
||||
}
|
||||
|
@ -950,6 +973,9 @@ export default {
|
|||
this.isInitialLoadComplete = false;
|
||||
this.refetchItems({ refetchCounts: true });
|
||||
},
|
||||
handleDisplaySettingsChanged(newDisplaySettings) {
|
||||
this.displaySettings = newDisplaySettings;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -1009,7 +1035,14 @@ export default {
|
|||
@previous-page="handlePreviousPage"
|
||||
@sort="handleSort"
|
||||
@select-issuable="handleToggle"
|
||||
@displaySettingsChanged="handleDisplaySettingsChanged"
|
||||
>
|
||||
<template #user-preference>
|
||||
<work-item-user-preferences
|
||||
:display-settings="displaySettings"
|
||||
@displaySettingsChanged="handleDisplaySettingsChanged"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="!isPlanningViewsEnabled" #nav-actions>
|
||||
<div class="gl-flex gl-gap-3">
|
||||
<gl-button
|
||||
|
|
|
@ -442,6 +442,7 @@ class ProjectPolicy < BasePolicy
|
|||
|
||||
rule { can?(:reporter_access) }.policy do
|
||||
enable :admin_issue_board
|
||||
enable :read_code
|
||||
enable :download_code
|
||||
enable :read_statistics
|
||||
enable :daily_statistics
|
||||
|
@ -896,6 +897,7 @@ class ProjectPolicy < BasePolicy
|
|||
rule { repository_disabled }.policy do
|
||||
prevent :build_push_code
|
||||
prevent :push_code
|
||||
prevent :read_code
|
||||
prevent :download_code
|
||||
prevent :build_download_code
|
||||
prevent :fork_project
|
||||
|
@ -996,6 +998,7 @@ class ProjectPolicy < BasePolicy
|
|||
enable :read_deployment
|
||||
enable :read_commit_status
|
||||
enable :read_container_image
|
||||
enable :read_code
|
||||
enable :download_code
|
||||
enable :read_release
|
||||
enable :download_wiki_code
|
||||
|
@ -1093,6 +1096,7 @@ class ProjectPolicy < BasePolicy
|
|||
|
||||
rule { download_code_deploy_key }.policy do
|
||||
enable :download_code
|
||||
enable :read_code
|
||||
end
|
||||
|
||||
rule { push_code_deploy_key }.policy do
|
||||
|
@ -1207,10 +1211,6 @@ class ProjectPolicy < BasePolicy
|
|||
enable :read_project_metadata
|
||||
end
|
||||
|
||||
rule { can?(:download_code) }.policy do
|
||||
enable :read_code
|
||||
end
|
||||
|
||||
rule { can?(:developer_access) & namespace_catalog_available }.policy do
|
||||
enable :read_namespace_catalog
|
||||
end
|
||||
|
|
|
@ -22,7 +22,7 @@ module Integrations
|
|||
end
|
||||
|
||||
expose :tags do |item|
|
||||
item['tags'].map { |tag| strip_tags(tag['name']) }
|
||||
item['tags'].blank? ? [] : item['tags'].map { |tag| strip_tags(tag['name']) }
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -13,14 +13,15 @@ class MergeRequests::HandleAssigneesChangeWorker
|
|||
idempotent!
|
||||
|
||||
def perform(merge_request_id, user_id, old_assignee_ids, options = {})
|
||||
merge_request = MergeRequest.find(merge_request_id)
|
||||
user = User.find(user_id)
|
||||
merge_request = MergeRequest.find_by_id(merge_request_id)
|
||||
user = User.find_by_id(user_id)
|
||||
|
||||
return unless merge_request && user
|
||||
|
||||
old_assignees = User.id_in(old_assignee_ids)
|
||||
|
||||
::MergeRequests::HandleAssigneesChangeService
|
||||
.new(project: merge_request.target_project, current_user: user)
|
||||
.execute(merge_request, old_assignees, options)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
end
|
||||
end
|
||||
|
|
|
@ -29,6 +29,8 @@
|
|||
- 1
|
||||
- - admin_emails
|
||||
- 1
|
||||
- - ai_active_context_code_repository_index
|
||||
- 1
|
||||
- - ai_active_context_code_saas_initial_indexing_event
|
||||
- 1
|
||||
- - ai_knowledge_graph_indexing_task
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChangeWorkItemCustomLifecycleNameLimit < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.2'
|
||||
disable_ddl_transaction!
|
||||
|
||||
CONSTRAINT_NAME = 'check_1feff2de99'
|
||||
|
||||
def up
|
||||
remove_text_limit :work_item_custom_lifecycles, :name, constraint_name: CONSTRAINT_NAME
|
||||
add_text_limit :work_item_custom_lifecycles, :name, 64, constraint_name: CONSTRAINT_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_text_limit :work_item_custom_lifecycles, :name, constraint_name: CONSTRAINT_NAME
|
||||
add_text_limit :work_item_custom_lifecycles, :name, 255, constraint_name: CONSTRAINT_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChangeWorkItemCustomStatusDescriptionLimit < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.2'
|
||||
disable_ddl_transaction!
|
||||
|
||||
CONSTRAINT_NAME = 'check_8ea8b3c991'
|
||||
|
||||
def up
|
||||
remove_text_limit :work_item_custom_statuses, :description, constraint_name: CONSTRAINT_NAME
|
||||
add_text_limit :work_item_custom_statuses, :description, 128, constraint_name: CONSTRAINT_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_text_limit :work_item_custom_statuses, :description, constraint_name: CONSTRAINT_NAME
|
||||
add_text_limit :work_item_custom_statuses, :description, 255, constraint_name: CONSTRAINT_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
443d16f76f1a3b90e71b697a443466779146c6f2a20946b58801dda73fd30d06
|
|
@ -0,0 +1 @@
|
|||
c14b80337285a053b5b2d7009adca6515caad99d477cc8c106765f3751938da2
|
|
@ -26122,7 +26122,7 @@ CREATE TABLE work_item_custom_lifecycles (
|
|||
name text NOT NULL,
|
||||
created_by_id bigint,
|
||||
updated_by_id bigint,
|
||||
CONSTRAINT check_1feff2de99 CHECK ((char_length(name) <= 255))
|
||||
CONSTRAINT check_1feff2de99 CHECK ((char_length(name) <= 64))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE work_item_custom_lifecycles_id_seq
|
||||
|
@ -26148,7 +26148,7 @@ CREATE TABLE work_item_custom_statuses (
|
|||
converted_from_system_defined_status_identifier smallint,
|
||||
CONSTRAINT check_4789467800 CHECK ((char_length(color) <= 7)),
|
||||
CONSTRAINT check_720a7c4d24 CHECK ((char_length(name) <= 32)),
|
||||
CONSTRAINT check_8ea8b3c991 CHECK ((char_length(description) <= 255)),
|
||||
CONSTRAINT check_8ea8b3c991 CHECK ((char_length(description) <= 128)),
|
||||
CONSTRAINT check_ff2bac1606 CHECK ((category > 0))
|
||||
);
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ The namespace is a user or group in GitLab, such as `gitlab.com/sidney-jones` or
|
|||
|
||||
Using the GitLab UI, the GitHub importer always imports from the
|
||||
`github.com` domain. If you are importing from a self-hosted GitHub Enterprise Server domain, use the
|
||||
[GitLab Import API](#use-the-api) GitHub endpoint.
|
||||
[GitLab Import API](#use-the-api) GitHub endpoint with a GitLab access token with the `api` scope.
|
||||
|
||||
You can change the target namespace and target repository name before you import.
|
||||
|
||||
|
|
|
@ -7252,6 +7252,9 @@ msgstr ""
|
|||
msgid "An error occurred while getting merge request counts"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while getting work item user preference."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while initializing path locks"
|
||||
msgstr ""
|
||||
|
||||
|
@ -59713,6 +59716,9 @@ msgstr ""
|
|||
msgid "Something went wrong while resolving this discussion. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while saving the preference."
|
||||
msgstr ""
|
||||
|
||||
msgid "Something went wrong while setting %{issuableType} %{dateType} date."
|
||||
msgstr ""
|
||||
|
||||
|
@ -70208,6 +70214,15 @@ msgstr ""
|
|||
msgid "WorkItems|Ancestors not available"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItems|Display options"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItems|Open items in side panel"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItems|Your preferences"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem| %{workItemType}s closed"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -326,5 +326,6 @@
|
|||
"engines": {
|
||||
"node": ">=12.22.1",
|
||||
"yarn": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
|
|
@ -1,19 +1,27 @@
|
|||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import Vue from 'vue';
|
||||
import { PiniaVuePlugin } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import { useNotes } from '~/notes/store/legacy_notes';
|
||||
import PreviewItem from '~/batch_comments/components/preview_item.vue';
|
||||
import store from '~/mr_notes/stores';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { globalAccessorPlugin } from '~/pinia/plugins';
|
||||
import { createDraft } from '../mock_data';
|
||||
|
||||
jest.mock('~/behaviors/markdown/render_gfm');
|
||||
jest.mock('~/mr_notes/stores', () => jest.requireActual('helpers/mocks/mr_notes/stores'));
|
||||
|
||||
Vue.use(PiniaVuePlugin);
|
||||
|
||||
describe('Batch comments draft preview item component', () => {
|
||||
let wrapper;
|
||||
let pinia;
|
||||
let draft;
|
||||
|
||||
beforeEach(() => {
|
||||
store.reset();
|
||||
|
||||
store.getters.getDiscussion = jest.fn(() => null);
|
||||
pinia = createTestingPinia({ plugins: [globalAccessorPlugin] });
|
||||
useLegacyDiffs();
|
||||
useNotes();
|
||||
});
|
||||
|
||||
function createComponent(extra = {}, improvedReviewExperience = false) {
|
||||
|
@ -23,9 +31,7 @@ describe('Batch comments draft preview item component', () => {
|
|||
};
|
||||
|
||||
wrapper = mountExtended(PreviewItem, {
|
||||
mocks: {
|
||||
$store: store,
|
||||
},
|
||||
pinia,
|
||||
propsData: { draft },
|
||||
provide: {
|
||||
glFeatures: { improvedReviewExperience },
|
||||
|
@ -93,7 +99,8 @@ describe('Batch comments draft preview item component', () => {
|
|||
|
||||
describe('for thread', () => {
|
||||
beforeEach(() => {
|
||||
store.getters.getDiscussion.mockReturnValue({
|
||||
useNotes().discussions = [
|
||||
{
|
||||
id: '1',
|
||||
notes: [
|
||||
{
|
||||
|
@ -102,8 +109,8 @@ describe('Batch comments draft preview item component', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
store.getters.isDiscussionResolved = jest.fn().mockReturnValue(false);
|
||||
},
|
||||
];
|
||||
|
||||
createComponent({ discussion_id: '1', resolve_discussion: true });
|
||||
});
|
||||
|
|
|
@ -91,6 +91,7 @@ describe('Incident Tabs component', () => {
|
|||
const findSummaryTab = () => wrapper.findByTestId('summary-tab');
|
||||
const findTimelineTab = () => wrapper.findByTestId('timeline-tab');
|
||||
const findAlertDetailsTab = () => wrapper.findByTestId('alert-details-tab');
|
||||
const findMetricsTab = () => wrapper.findByTestId('metrics-tab');
|
||||
const findAlertDetailsComponent = () => wrapper.findComponent(AlertDetailsTable);
|
||||
const findDescriptionComponent = () => wrapper.findComponent(DescriptionComponent);
|
||||
const findHighlightBarComponent = () => wrapper.findComponent(HighlightBar);
|
||||
|
@ -227,5 +228,14 @@ describe('Incident Tabs component', () => {
|
|||
it('does not render the timeline tab', () => {
|
||||
expect(findTimelineTab().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render the alert details tab', () => {
|
||||
mountComponent({ hasLinkedAlerts: true });
|
||||
expect(findAlertDetailsTab().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render the metrics tab', () => {
|
||||
expect(findMetricsTab().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
import { nextTick } from 'vue';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { PiniaVuePlugin } from 'pinia';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import diffDiscussionHeader from '~/notes/components/diff_discussion_header.vue';
|
||||
import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
|
||||
import createStore from '~/notes/stores';
|
||||
|
||||
import mockDiffFile from 'jest/diffs/mock_data/diff_discussions';
|
||||
import { useNotes } from '~/notes/store/legacy_notes';
|
||||
import { globalAccessorPlugin } from '~/pinia/plugins';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import { discussionMock } from '../mock_data';
|
||||
|
||||
Vue.use(PiniaVuePlugin);
|
||||
|
||||
describe('diff_discussion_header component', () => {
|
||||
let store;
|
||||
let pinia;
|
||||
let wrapper;
|
||||
|
||||
const createComponent = ({ propsData = {} } = {}) => {
|
||||
wrapper = shallowMountExtended(diffDiscussionHeader, {
|
||||
store,
|
||||
pinia,
|
||||
propsData: {
|
||||
discussion: discussionMock,
|
||||
...propsData,
|
||||
|
@ -24,8 +29,9 @@ describe('diff_discussion_header component', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
window.mrTabs = {};
|
||||
store = createStore();
|
||||
|
||||
pinia = createTestingPinia({ plugins: [globalAccessorPlugin] });
|
||||
useLegacyDiffs();
|
||||
useNotes();
|
||||
createComponent({ propsData: { discussion: discussionMock } });
|
||||
});
|
||||
|
||||
|
@ -60,10 +66,6 @@ describe('diff_discussion_header component', () => {
|
|||
let commitElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
store.state.diffs = {
|
||||
projectPath: 'something',
|
||||
};
|
||||
|
||||
createComponent({
|
||||
propsData: {
|
||||
discussion: {
|
||||
|
@ -202,5 +204,18 @@ describe('diff_discussion_header component', () => {
|
|||
});
|
||||
expect(wrapper.findComponent(ToggleRepliesWidget).props('collapsed')).toBe(true);
|
||||
});
|
||||
|
||||
it('toggles discussion', () => {
|
||||
createComponent({
|
||||
propsData: {
|
||||
discussion: {
|
||||
...discussionMock,
|
||||
expanded: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
wrapper.findComponent(ToggleRepliesWidget).vm.$emit('toggle');
|
||||
expect(useNotes().toggleDiscussion).toHaveBeenCalledWith({ discussionId: discussionMock.id });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,17 +5,16 @@ import { PiniaVuePlugin } from 'pinia';
|
|||
import { createTestingPinia } from '@pinia/testing';
|
||||
import discussionFixture from 'test_fixtures/merge_requests/diff_discussion.json';
|
||||
import imageDiscussionFixture from 'test_fixtures/merge_requests/image_diff_discussion.json';
|
||||
import { createStore } from '~/mr_notes/stores';
|
||||
import DiffWithNote from '~/notes/components/diff_with_note.vue';
|
||||
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
|
||||
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
|
||||
import { globalAccessorPlugin } from '~/pinia/plugins';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import { useNotes } from '~/notes/store/legacy_notes';
|
||||
|
||||
Vue.use(PiniaVuePlugin);
|
||||
|
||||
describe('diff_with_note', () => {
|
||||
let store;
|
||||
let pinia;
|
||||
let wrapper;
|
||||
|
||||
|
@ -37,7 +36,6 @@ describe('diff_with_note', () => {
|
|||
const createComponent = (propsData) => {
|
||||
wrapper = shallowMount(DiffWithNote, {
|
||||
propsData,
|
||||
store,
|
||||
pinia,
|
||||
});
|
||||
};
|
||||
|
@ -45,15 +43,7 @@ describe('diff_with_note', () => {
|
|||
beforeEach(() => {
|
||||
pinia = createTestingPinia({ plugins: [globalAccessorPlugin] });
|
||||
useLegacyDiffs();
|
||||
store = createStore();
|
||||
store.replaceState({
|
||||
...store.state,
|
||||
notes: {
|
||||
noteableData: {
|
||||
current_user: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
useNotes().noteableData = { current_user: {} };
|
||||
});
|
||||
|
||||
describe('text diff', () => {
|
||||
|
@ -199,7 +189,7 @@ describe('diff_with_note', () => {
|
|||
beforeEach(() => {
|
||||
wrapper = shallowMount(DiffWithNote, {
|
||||
propsData: { discussion: fileDiscussion, diffFile: {} },
|
||||
store,
|
||||
pinia,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -218,7 +208,7 @@ describe('diff_with_note', () => {
|
|||
|
||||
wrapper = shallowMount(DiffWithNote, {
|
||||
propsData: { discussion: fileDiscussion, diffFile: {} },
|
||||
store,
|
||||
pinia,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { getByRole } from '@testing-library/dom';
|
||||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { PiniaVuePlugin } from 'pinia';
|
||||
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
|
||||
import NoteableNote from '~/notes/components/noteable_note.vue';
|
||||
import { SYSTEM_NOTE } from '~/notes/constants';
|
||||
|
@ -8,6 +10,9 @@ import createStore from '~/notes/stores';
|
|||
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
|
||||
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
|
||||
import SystemNote from '~/vue_shared/components/notes/system_note.vue';
|
||||
import { globalAccessorPlugin } from '~/pinia/plugins';
|
||||
import { useNotes } from '~/notes/store/legacy_notes';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
|
||||
|
||||
jest.mock('~/behaviors/markdown/render_gfm');
|
||||
|
@ -20,14 +25,18 @@ const DISCUSSION_WITH_LINE_RANGE = {
|
|||
},
|
||||
};
|
||||
|
||||
Vue.use(PiniaVuePlugin);
|
||||
|
||||
describe('DiscussionNotes', () => {
|
||||
let store;
|
||||
let pinia;
|
||||
let wrapper;
|
||||
|
||||
const getList = () => getByRole(wrapper.element, 'list');
|
||||
const createComponent = (props, mountingMethod = shallowMount) => {
|
||||
wrapper = mountingMethod(DiscussionNotes, {
|
||||
store,
|
||||
pinia,
|
||||
propsData: {
|
||||
discussion: discussionMock,
|
||||
isExpanded: false,
|
||||
|
@ -48,6 +57,9 @@ describe('DiscussionNotes', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
pinia = createTestingPinia({ plugins: [globalAccessorPlugin] });
|
||||
useLegacyDiffs();
|
||||
useNotes();
|
||||
store = createStore();
|
||||
store.dispatch('setNoteableData', noteableDataMock);
|
||||
store.dispatch('setNotesData', notesDataMock);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { GlCollapsibleListbox, GlListboxItem, GlButton } from '@gitlab/ui';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import Vuex from 'vuex';
|
||||
import { PiniaVuePlugin } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import DiscussionFilter from '~/notes/components/mr_discussion_filter.vue';
|
||||
|
@ -11,36 +11,19 @@ import {
|
|||
MR_FILTER_TRACKING_USER_COMMENTS,
|
||||
MR_FILTER_TRACKING_BOT_COMMENTS,
|
||||
} from '~/notes/constants';
|
||||
import { globalAccessorPlugin } from '~/pinia/plugins';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import { useNotes } from '~/notes/store/legacy_notes';
|
||||
|
||||
Vue.use(Vuex);
|
||||
Vue.use(PiniaVuePlugin);
|
||||
|
||||
describe('Merge request discussion filter component', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
let updateMergeRequestFilters;
|
||||
let setDiscussionSortDirection;
|
||||
|
||||
function createComponent(mergeRequestFilters = MR_FILTER_OPTIONS.map((f) => f.value)) {
|
||||
updateMergeRequestFilters = jest.fn();
|
||||
setDiscussionSortDirection = jest.fn();
|
||||
|
||||
store = new Vuex.Store({
|
||||
modules: {
|
||||
notes: {
|
||||
state: {
|
||||
mergeRequestFilters,
|
||||
discussionSortOrder: 'asc',
|
||||
},
|
||||
actions: {
|
||||
updateMergeRequestFilters,
|
||||
setDiscussionSortDirection,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
let pinia;
|
||||
|
||||
function createComponent() {
|
||||
wrapper = mountExtended(DiscussionFilter, {
|
||||
store,
|
||||
pinia,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -48,6 +31,13 @@ describe('Merge request discussion filter component', () => {
|
|||
return wrapper.findComponentByTestId(`listbox-item-${value}`);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
pinia = createTestingPinia({ plugins: [globalAccessorPlugin] });
|
||||
useLegacyDiffs();
|
||||
useNotes().mergeRequestFilters = MR_FILTER_OPTIONS.map((f) => f.value);
|
||||
useNotes().discussionSortOrder = 'asc';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.removeItem('mr_activity_filters');
|
||||
localStorage.removeItem('sort_direction_merge_request');
|
||||
|
@ -59,7 +49,7 @@ describe('Merge request discussion filter component', () => {
|
|||
|
||||
createComponent();
|
||||
|
||||
expect(setDiscussionSortDirection).toHaveBeenCalledWith(expect.anything(), {
|
||||
expect(useNotes().setDiscussionSortDirection).toHaveBeenCalledWith({
|
||||
direction: 'desc',
|
||||
});
|
||||
});
|
||||
|
@ -71,7 +61,7 @@ describe('Merge request discussion filter component', () => {
|
|||
|
||||
createComponent();
|
||||
|
||||
expect(updateMergeRequestFilters).toHaveBeenCalledWith(expect.anything(), ['comments']);
|
||||
expect(useNotes().updateMergeRequestFilters).toHaveBeenCalledWith(['comments']);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -90,7 +80,7 @@ describe('Merge request discussion filter component', () => {
|
|||
|
||||
wrapper.findComponent(GlCollapsibleListbox).vm.$emit('hidden');
|
||||
|
||||
expect(updateMergeRequestFilters).toHaveBeenCalledWith(expect.anything(), [
|
||||
expect(useNotes().updateMergeRequestFilters).toHaveBeenCalledWith([
|
||||
'assignees_reviewers',
|
||||
'bot_comments',
|
||||
'comments',
|
||||
|
@ -113,7 +103,7 @@ describe('Merge request discussion filter component', () => {
|
|||
`('updates toggle text to $expectedText with $state', async ({ state, expectedText }) => {
|
||||
createComponent();
|
||||
|
||||
store.state.notes.mergeRequestFilters = state;
|
||||
useNotes().mergeRequestFilters = state;
|
||||
|
||||
await nextTick();
|
||||
|
||||
|
@ -193,7 +183,7 @@ describe('Merge request discussion filter component', () => {
|
|||
`(
|
||||
'has the correct attributes and props when the sort-order is "$sortOrder"',
|
||||
async ({ sortOrder, expectedTitle, expectedIcon }) => {
|
||||
store.state.notes.discussionSortOrder = sortOrder;
|
||||
useNotes().discussionSortOrder = sortOrder;
|
||||
await nextTick();
|
||||
|
||||
const sortDirectionButton = wrapper.findByTestId('mr-discussion-sort-direction');
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
import { GlFormSelect } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { GlFormSelect, GlSprintf } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import Vuex from 'vuex';
|
||||
import { PiniaVuePlugin } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import MultilineCommentForm from '~/notes/components/multiline_comment_form.vue';
|
||||
import notesModule from '~/notes/stores/modules';
|
||||
import { globalAccessorPlugin } from '~/pinia/plugins';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import { useNotes } from '~/notes/store/legacy_notes';
|
||||
|
||||
Vue.use(PiniaVuePlugin);
|
||||
|
||||
describe('MultilineCommentForm', () => {
|
||||
Vue.use(Vuex);
|
||||
const setSelectedCommentPosition = jest.fn();
|
||||
let wrapper;
|
||||
let pinia;
|
||||
|
||||
const testLine = {
|
||||
line_code: 'test',
|
||||
type: 'test',
|
||||
|
@ -16,63 +21,64 @@ describe('MultilineCommentForm', () => {
|
|||
new_line: 'test',
|
||||
};
|
||||
|
||||
const createWrapper = (props = {}, state) => {
|
||||
setSelectedCommentPosition.mockReset();
|
||||
|
||||
const store = new Vuex.Store({
|
||||
modules: { notes: notesModule() },
|
||||
actions: { setSelectedCommentPosition },
|
||||
});
|
||||
if (state) store.replaceState({ ...store.state, ...state });
|
||||
|
||||
const createWrapper = (props = {}) => {
|
||||
const propsData = {
|
||||
line: { ...testLine },
|
||||
commentLineOptions: [{ text: '1' }],
|
||||
...props,
|
||||
};
|
||||
return mount(MultilineCommentForm, { propsData, store });
|
||||
wrapper = shallowMount(MultilineCommentForm, { propsData, pinia, stubs: { GlSprintf } });
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
pinia = createTestingPinia({ plugins: [globalAccessorPlugin] });
|
||||
useLegacyDiffs();
|
||||
useNotes();
|
||||
});
|
||||
|
||||
describe('created', () => {
|
||||
it('sets commentLineStart to line', () => {
|
||||
const line = { ...testLine };
|
||||
const wrapper = createWrapper({ line });
|
||||
createWrapper({ line });
|
||||
|
||||
expect(wrapper.vm.commentLineStart).toEqual(line);
|
||||
expect(setSelectedCommentPosition).toHaveBeenCalled();
|
||||
// we can't check for .attributes() because of GlFormSelect design
|
||||
// all the attributes get converted to a string, so the line object becomes [object Object]
|
||||
// we can test for the component internals instead which is as reliable as VTUs checks
|
||||
expect(wrapper.findComponent(GlFormSelect).vm.$attrs.value).toEqual(line);
|
||||
expect(useNotes().setSelectedCommentPosition).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets commentLineStart to lineRange', () => {
|
||||
const lineRange = {
|
||||
start: { ...testLine },
|
||||
};
|
||||
const wrapper = createWrapper({ lineRange });
|
||||
createWrapper({ lineRange });
|
||||
|
||||
expect(wrapper.vm.commentLineStart).toEqual(lineRange.start);
|
||||
expect(setSelectedCommentPosition).toHaveBeenCalled();
|
||||
expect(wrapper.findComponent(GlFormSelect).vm.$attrs.value).toEqual(lineRange.start);
|
||||
expect(useNotes().setSelectedCommentPosition).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroyed', () => {
|
||||
it('calls setSelectedCommentPosition', () => {
|
||||
const wrapper = createWrapper();
|
||||
createWrapper();
|
||||
wrapper.destroy();
|
||||
|
||||
// Once during created, once during destroyed
|
||||
expect(setSelectedCommentPosition).toHaveBeenCalledTimes(2);
|
||||
expect(useNotes().setSelectedCommentPosition).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles changing the start line', () => {
|
||||
const line = { ...testLine };
|
||||
const wrapper = createWrapper({ line });
|
||||
createWrapper({ line });
|
||||
const glSelect = wrapper.findComponent(GlFormSelect);
|
||||
|
||||
glSelect.vm.$emit('change', { ...testLine });
|
||||
|
||||
expect(wrapper.vm.commentLineStart).toEqual(line);
|
||||
expect(wrapper.findComponent(GlFormSelect).vm.$attrs.value).toEqual(line);
|
||||
expect(wrapper.emitted('input')).toHaveLength(1);
|
||||
// Once during created, once during updateCommentLineStart
|
||||
expect(setSelectedCommentPosition).toHaveBeenCalledTimes(2);
|
||||
expect(useNotes().setSelectedCommentPosition).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,9 @@ import {
|
|||
} from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import AxiosMockAdapter from 'axios-mock-adapter';
|
||||
import { nextTick } from 'vue';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import { PiniaVuePlugin } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { stubComponent } from 'helpers/stub_component';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
@ -13,13 +15,17 @@ import noteActions from '~/notes/components/note_actions.vue';
|
|||
import { NOTEABLE_TYPE_MAPPING } from '~/notes/constants';
|
||||
import TimelineEventButton from '~/notes/components/note_actions/timeline_event_button.vue';
|
||||
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
|
||||
import createStore from '~/notes/stores';
|
||||
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import { useNotes } from '~/notes/store/legacy_notes';
|
||||
import { globalAccessorPlugin } from '~/pinia/plugins';
|
||||
import { userDataMock } from '../mock_data';
|
||||
|
||||
Vue.use(PiniaVuePlugin);
|
||||
|
||||
describe('noteActions', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
let pinia;
|
||||
let props;
|
||||
let actions;
|
||||
let axiosMock;
|
||||
|
@ -37,20 +43,20 @@ describe('noteActions', () => {
|
|||
noteableType,
|
||||
isPromotionInProgress = true,
|
||||
}) => {
|
||||
store.dispatch('setUserData', {
|
||||
useNotes().setUserData({
|
||||
...userDataMock,
|
||||
can_add_timeline_events: userCanAdd,
|
||||
});
|
||||
store.state.noteableData = {
|
||||
...store.state.noteableData,
|
||||
useNotes().noteableData = {
|
||||
...useNotes().noteableData,
|
||||
type: noteableType,
|
||||
};
|
||||
store.state.isPromoteCommentToTimelineEventInProgress = isPromotionInProgress;
|
||||
useNotes().isPromoteCommentToTimelineEventInProgress = isPromotionInProgress;
|
||||
};
|
||||
|
||||
const mountNoteActions = (propsData) => {
|
||||
return shallowMount(noteActions, {
|
||||
store,
|
||||
pinia,
|
||||
propsData,
|
||||
stubs: {
|
||||
GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, {
|
||||
|
@ -65,7 +71,10 @@ describe('noteActions', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
pinia = createTestingPinia({ plugins: [globalAccessorPlugin], stubActions: false });
|
||||
useLegacyDiffs();
|
||||
useNotes().toggleAwardRequest.mockResolvedValue();
|
||||
useNotes().promoteCommentToTimelineEvent.mockResolvedValue();
|
||||
|
||||
props = {
|
||||
accessLevel: 'Maintainer',
|
||||
|
@ -98,7 +107,7 @@ describe('noteActions', () => {
|
|||
|
||||
describe('user is logged in', () => {
|
||||
beforeEach(() => {
|
||||
store.dispatch('setUserData', userDataMock);
|
||||
useNotes().setUserData(userDataMock);
|
||||
|
||||
wrapper = mountNoteActions(props);
|
||||
});
|
||||
|
@ -222,13 +231,13 @@ describe('noteActions', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
wrapper = mountNoteActions(props);
|
||||
store.state.noteableData = {
|
||||
useNotes().noteableData = {
|
||||
current_user: {
|
||||
can_set_issue_metadata: true,
|
||||
},
|
||||
};
|
||||
store.state.userData = userDataMock;
|
||||
store.state.noteableData.targetType = 'issue';
|
||||
useNotes().userData = userDataMock;
|
||||
useNotes().noteableData.targetType = 'issue';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -244,13 +253,13 @@ describe('noteActions', () => {
|
|||
wrapper = mountNoteActions(props, {
|
||||
targetType: () => 'issue',
|
||||
});
|
||||
store.state.noteableData = {
|
||||
useNotes().noteableData = {
|
||||
current_user: {
|
||||
can_update: true,
|
||||
can_set_issue_metadata: false,
|
||||
},
|
||||
};
|
||||
store.state.userData = userDataMock;
|
||||
useNotes().userData = userDataMock;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -282,7 +291,7 @@ describe('noteActions', () => {
|
|||
describe('user is not logged in', () => {
|
||||
beforeEach(() => {
|
||||
// userData can be null https://gitlab.com/gitlab-org/gitlab/-/issues/379375
|
||||
store.dispatch('setUserData', null);
|
||||
useNotes().setUserData(null);
|
||||
wrapper = mountNoteActions({
|
||||
...props,
|
||||
canDelete: false,
|
||||
|
@ -333,7 +342,7 @@ describe('noteActions', () => {
|
|||
|
||||
describe('Draft notes', () => {
|
||||
beforeEach(() => {
|
||||
store.dispatch('setUserData', userDataMock);
|
||||
useNotes().setUserData(userDataMock);
|
||||
|
||||
wrapper = mountNoteActions({ ...props, canResolve: true, isDraft: true });
|
||||
});
|
||||
|
@ -386,14 +395,11 @@ describe('noteActions', () => {
|
|||
});
|
||||
|
||||
it('when timeline-event-button emits click-promote-comment-to-event, dispatches action', () => {
|
||||
jest.spyOn(store, 'dispatch').mockImplementation();
|
||||
|
||||
expect(store.dispatch).not.toHaveBeenCalled();
|
||||
expect(useNotes().promoteCommentToTimelineEvent).not.toHaveBeenCalled();
|
||||
|
||||
findTimelineButton().vm.$emit('click-promote-comment-to-event');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(store.dispatch).toHaveBeenCalledWith('promoteCommentToTimelineEvent');
|
||||
expect(useNotes().promoteCommentToTimelineEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -403,7 +409,7 @@ describe('noteActions', () => {
|
|||
|
||||
describe('when user is not allowed to report abuse', () => {
|
||||
beforeEach(() => {
|
||||
store.dispatch('setUserData', userDataMock);
|
||||
useNotes().setUserData(userDataMock);
|
||||
wrapper = mountNoteActions({ ...props, canReportAsAbuse: false });
|
||||
});
|
||||
|
||||
|
@ -418,7 +424,7 @@ describe('noteActions', () => {
|
|||
|
||||
describe('when user is allowed to report abuse', () => {
|
||||
beforeEach(() => {
|
||||
store.dispatch('setUserData', userDataMock);
|
||||
useNotes().setUserData(userDataMock);
|
||||
wrapper = mountNoteActions({ ...props, canReportAsAbuse: true });
|
||||
});
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import AxiosMockAdapter from 'axios-mock-adapter';
|
||||
import Vue from 'vue';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import Vuex from 'vuex';
|
||||
import { PiniaVuePlugin } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { userDataMock } from 'jest/notes/mock_data';
|
||||
|
@ -9,12 +9,15 @@ import EmojiPicker from '~/emoji/components/picker.vue';
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
import awardsNote from '~/notes/components/note_awards_list.vue';
|
||||
import createStore from '~/notes/stores';
|
||||
import { globalAccessorPlugin } from '~/pinia/plugins';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import { useNotes } from '~/notes/store/legacy_notes';
|
||||
|
||||
Vue.use(Vuex);
|
||||
Vue.use(PiniaVuePlugin);
|
||||
|
||||
describe('Note Awards List', () => {
|
||||
let wrapper;
|
||||
let pinia;
|
||||
let mock;
|
||||
|
||||
const awardsMock = [
|
||||
|
@ -42,42 +45,34 @@ describe('Note Awards List', () => {
|
|||
const findAllEmojiAwards = () => wrapper.findAll('gl-emoji');
|
||||
const findEmojiPicker = () => wrapper.findComponent(EmojiPicker);
|
||||
|
||||
const createComponent = (props = defaultProps, store = createStore()) => {
|
||||
const createComponent = (props = defaultProps) => {
|
||||
wrapper = mountExtended(awardsNote, {
|
||||
store,
|
||||
pinia,
|
||||
propsData: {
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('Note Awards functionality', () => {
|
||||
const toggleAwardRequestSpy = jest.fn();
|
||||
const fakeStore = () => {
|
||||
return new Vuex.Store({
|
||||
getters: {
|
||||
getUserData: () => userDataMock,
|
||||
},
|
||||
actions: {
|
||||
toggleAwardRequest: toggleAwardRequestSpy,
|
||||
},
|
||||
beforeEach(() => {
|
||||
pinia = createTestingPinia({ plugins: [globalAccessorPlugin] });
|
||||
useLegacyDiffs();
|
||||
useNotes().setUserData(userDataMock);
|
||||
useNotes().toggleAwardRequest.mockResolvedValue();
|
||||
});
|
||||
};
|
||||
|
||||
describe('Note Awards functionality', () => {
|
||||
beforeEach(() => {
|
||||
mock = new AxiosMockAdapter(axios);
|
||||
mock.onPost(toggleAwardPathMock).reply(HTTP_STATUS_OK, '');
|
||||
|
||||
createComponent(
|
||||
{
|
||||
createComponent({
|
||||
awards: awardsMock,
|
||||
noteAuthorId: 2,
|
||||
noteId: '545',
|
||||
canAwardEmoji: true,
|
||||
toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji',
|
||||
},
|
||||
fakeStore(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -100,7 +95,7 @@ describe('Note Awards List', () => {
|
|||
await findAwardButton().vm.$emit('click');
|
||||
|
||||
const { toggleAwardPath, noteId } = defaultProps;
|
||||
expect(toggleAwardRequestSpy).toHaveBeenCalledWith(expect.anything(), {
|
||||
expect(useNotes().toggleAwardRequest).toHaveBeenCalledWith({
|
||||
awardName: awardsMock[0].name,
|
||||
endpoint: toggleAwardPath,
|
||||
noteId,
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import Vue from 'vue';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import Vuex from 'vuex';
|
||||
import { PiniaVuePlugin } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import NoteBody from '~/notes/components/note_body.vue';
|
||||
import NoteAwardsList from '~/notes/components/note_awards_list.vue';
|
||||
import NoteForm from '~/notes/components/note_form.vue';
|
||||
import createStore from '~/notes/stores';
|
||||
import notes from '~/notes/stores/modules/index';
|
||||
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import { globalAccessorPlugin } from '~/pinia/plugins';
|
||||
|
@ -24,23 +20,8 @@ describe('issue_note_body component', () => {
|
|||
let wrapper;
|
||||
let pinia;
|
||||
|
||||
const createComponent = ({
|
||||
props = {},
|
||||
noteableData = noteableDataMock,
|
||||
notesData = notesDataMock,
|
||||
store = null,
|
||||
} = {}) => {
|
||||
let mockStore;
|
||||
|
||||
if (!store) {
|
||||
mockStore = createStore();
|
||||
|
||||
mockStore.dispatch('setNoteableData', noteableData);
|
||||
mockStore.dispatch('setNotesData', notesData);
|
||||
}
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = shallowMountExtended(NoteBody, {
|
||||
store: mockStore || store,
|
||||
pinia,
|
||||
propsData: {
|
||||
note,
|
||||
|
@ -58,7 +39,8 @@ describe('issue_note_body component', () => {
|
|||
beforeEach(() => {
|
||||
pinia = createTestingPinia({ plugins: [globalAccessorPlugin] });
|
||||
useLegacyDiffs();
|
||||
useNotes();
|
||||
useNotes().setNoteableData(noteableDataMock);
|
||||
useNotes().setNotesData(notesDataMock);
|
||||
useMrNotes();
|
||||
createComponent();
|
||||
});
|
||||
|
@ -73,7 +55,7 @@ describe('issue_note_body component', () => {
|
|||
|
||||
describe('isInternalNote', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ props: { isInternalNote: true } });
|
||||
createComponent({ isInternalNote: true });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -82,7 +64,9 @@ describe('issue_note_body component', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: { isEditing: true, autosaveKey, restoreFromAutosave: true },
|
||||
isEditing: true,
|
||||
autosaveKey,
|
||||
restoreFromAutosave: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -98,7 +82,7 @@ describe('issue_note_body component', () => {
|
|||
${false} | ${'Save comment'}
|
||||
${true} | ${'Save internal note'}
|
||||
`('renders save button with text "$buttonText"', ({ internal, buttonText }) => {
|
||||
createComponent({ props: { note: { ...note, internal }, isEditing: true } });
|
||||
createComponent({ note: { ...note, internal }, isEditing: true });
|
||||
|
||||
expect(wrapper.findComponent(NoteForm).props('saveButtonTitle')).toBe(buttonText);
|
||||
});
|
||||
|
@ -112,40 +96,20 @@ describe('issue_note_body component', () => {
|
|||
|
||||
describe('commitMessage', () => {
|
||||
beforeEach(() => {
|
||||
const mrMetadata = {
|
||||
useMrNotes().mrMetadata = {
|
||||
branch_name: 'branch',
|
||||
project_path: '/path',
|
||||
project_name: 'name',
|
||||
username: 'user',
|
||||
user_full_name: 'user userton',
|
||||
};
|
||||
const notesStore = notes();
|
||||
|
||||
notesStore.state.notes = {};
|
||||
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
notes: notesStore,
|
||||
page: {
|
||||
namespaced: true,
|
||||
state: {
|
||||
mrMetadata,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useMrNotes().mrMetadata = mrMetadata;
|
||||
useLegacyDiffs().defaultSuggestionCommitMessage =
|
||||
'*** %{branch_name} %{project_path} %{project_name} %{username} %{user_full_name} %{file_paths} %{suggestions_count} %{files_count} %{co_authored_by}';
|
||||
|
||||
createComponent({
|
||||
store,
|
||||
props: {
|
||||
note: { ...note, suggestions: [12345] },
|
||||
canEdit: true,
|
||||
file: { file_path: 'abc' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -159,19 +123,6 @@ describe('issue_note_body component', () => {
|
|||
});
|
||||
|
||||
describe('duo code review feedback', () => {
|
||||
const createMockStoreWithDiscussion = (discussionId, discussionNotes) => {
|
||||
return new Vuex.Store({
|
||||
getters: {
|
||||
getDiscussion: () => (id) => {
|
||||
if (id === discussionId) {
|
||||
return { notes: discussionNotes };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it.each`
|
||||
userType | type | exists | existsText
|
||||
${'duo_code_review_bot'} | ${null} | ${true} | ${'renders'}
|
||||
|
@ -191,12 +142,9 @@ describe('issue_note_body component', () => {
|
|||
user_type: userType,
|
||||
},
|
||||
};
|
||||
const mockStore = createMockStoreWithDiscussion('discussion1', [duoNote]);
|
||||
useNotes().discussions = [{ id: 'discussion1', notes: [duoNote] }];
|
||||
|
||||
createComponent({
|
||||
props: { note: duoNote },
|
||||
store: mockStore,
|
||||
});
|
||||
createComponent({ note: duoNote });
|
||||
|
||||
expect(wrapper.findByTestId('code-review-feedback').exists()).toBe(exists);
|
||||
},
|
||||
|
@ -213,41 +161,15 @@ describe('issue_note_body component', () => {
|
|||
user_type: 'duo_code_review_bot',
|
||||
},
|
||||
};
|
||||
const mockStore = createMockStoreWithDiscussion('discussion1', [note, duoNote]);
|
||||
useNotes().discussions = [{ id: 'discussion1', notes: [note, duoNote] }];
|
||||
|
||||
createComponent({
|
||||
props: { note: duoNote },
|
||||
store: mockStore,
|
||||
});
|
||||
createComponent({ note: duoNote });
|
||||
|
||||
expect(wrapper.findByTestId('code-review-feedback').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('duo code review feedback text', () => {
|
||||
const createMockStoreWithDiscussion = (discussionId, discussionNotes) => {
|
||||
return new Vuex.Store({
|
||||
getters: {
|
||||
getDiscussion: () => (id) => {
|
||||
if (id === discussionId) {
|
||||
return { notes: discussionNotes };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
suggestionsCount: () => 0,
|
||||
getSuggestionsFilePaths: () => [],
|
||||
},
|
||||
modules: {
|
||||
notes: {
|
||||
state: { batchSuggestionsInfo: [] },
|
||||
},
|
||||
page: {
|
||||
state: { failedToLoadMetadata: false },
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createDuoNote = (props = {}) => ({
|
||||
...note,
|
||||
id: '1',
|
||||
|
@ -262,11 +184,10 @@ describe('issue_note_body component', () => {
|
|||
|
||||
it('renders feedback text for the first DiffNote from GitLabDuo', () => {
|
||||
const duoNote = createDuoNote();
|
||||
const mockStore = createMockStoreWithDiscussion('discussion1', [duoNote]);
|
||||
useNotes().discussions = [{ id: 'discussion1', notes: [duoNote] }];
|
||||
|
||||
createComponent({
|
||||
props: { note: duoNote },
|
||||
store: mockStore,
|
||||
note: duoNote,
|
||||
});
|
||||
|
||||
const feedbackDiv = wrapper.find('.gl-text-md.gl-mt-4.gl-text-gray-500');
|
||||
|
@ -275,9 +196,10 @@ describe('issue_note_body component', () => {
|
|||
|
||||
it('does not render feedback text for non-DiffNote from GitLabDuo', () => {
|
||||
const duoNote = createDuoNote({ type: 'DiscussionNote' });
|
||||
useNotes().discussions = [{ id: 'discussion1', notes: [duoNote] }];
|
||||
|
||||
createComponent({
|
||||
props: { note: duoNote },
|
||||
note: duoNote,
|
||||
});
|
||||
|
||||
const feedbackDiv = wrapper.find('.gl-text-md.gl-mt-4.gl-text-gray-500');
|
||||
|
@ -286,14 +208,10 @@ describe('issue_note_body component', () => {
|
|||
|
||||
it('does not render feedback text for follow-up DiffNote from GitLabDuo', () => {
|
||||
const duoNote = createDuoNote({ id: '2' });
|
||||
const mockStore = createMockStoreWithDiscussion('discussion1', [
|
||||
{ id: '1' }, // First note has different ID
|
||||
duoNote,
|
||||
]);
|
||||
useNotes().discussions = [{ id: 'discussion1', notes: [{ id: '1' }, duoNote] }];
|
||||
|
||||
createComponent({
|
||||
props: { note: duoNote },
|
||||
store: mockStore,
|
||||
note: duoNote,
|
||||
});
|
||||
|
||||
const feedbackDiv = wrapper.find('.gl-text-md.gl-mt-4.gl-text-gray-500');
|
||||
|
@ -302,11 +220,10 @@ describe('issue_note_body component', () => {
|
|||
|
||||
it('shows default awards list with thumbsup and thumbsdown for first DiffNote from GitLabDuo', () => {
|
||||
const duoNote = createDuoNote();
|
||||
const mockStore = createMockStoreWithDiscussion('discussion1', [duoNote]);
|
||||
useNotes().discussions = [{ id: 'discussion1', notes: [duoNote] }];
|
||||
|
||||
createComponent({
|
||||
props: { note: duoNote },
|
||||
store: mockStore,
|
||||
note: duoNote,
|
||||
});
|
||||
|
||||
const awardsList = wrapper.findComponent(NoteAwardsList);
|
||||
|
@ -325,7 +242,7 @@ describe('issue_note_body component', () => {
|
|||
};
|
||||
|
||||
createComponent({
|
||||
props: { note: regularNote },
|
||||
note: regularNote,
|
||||
});
|
||||
|
||||
const awardsList = wrapper.findComponent(NoteAwardsList);
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import Vue, { nextTick } from 'vue';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import Vuex from 'vuex';
|
||||
import { PiniaVuePlugin } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import NoteHeader from '~/notes/components/note_header.vue';
|
||||
import ImportedBadge from '~/vue_shared/components/imported_badge.vue';
|
||||
import { globalAccessorPlugin } from '~/pinia/plugins';
|
||||
import { useLegacyDiffs } from '~/diffs/stores/legacy_diffs';
|
||||
import { useNotes } from '~/notes/store/legacy_notes';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const actions = {
|
||||
setTargetNoteHash: jest.fn(),
|
||||
};
|
||||
Vue.use(PiniaVuePlugin);
|
||||
|
||||
describe('NoteHeader component', () => {
|
||||
let wrapper;
|
||||
let pinia;
|
||||
|
||||
const findActionText = () => wrapper.findComponent({ ref: 'actionText' });
|
||||
const findTimestampLink = () => wrapper.findComponent({ ref: 'noteTimestampLink' });
|
||||
|
@ -51,13 +52,17 @@ describe('NoteHeader component', () => {
|
|||
|
||||
const createComponent = (props) => {
|
||||
wrapper = shallowMountExtended(NoteHeader, {
|
||||
store: new Vuex.Store({
|
||||
actions,
|
||||
}),
|
||||
pinia,
|
||||
propsData: { ...props },
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
pinia = createTestingPinia({ plugins: [globalAccessorPlugin] });
|
||||
useLegacyDiffs();
|
||||
useNotes();
|
||||
});
|
||||
|
||||
it('renders an author link if author is passed to props', () => {
|
||||
createComponent({ author });
|
||||
|
||||
|
@ -106,14 +111,16 @@ describe('NoteHeader component', () => {
|
|||
expect(findActionText().text()).toBe('Test action text');
|
||||
});
|
||||
|
||||
it('calls an action when timestamp is clicked', () => {
|
||||
it('calls an action when timestamp is clicked', async () => {
|
||||
createComponent({
|
||||
createdAt: '2017-08-02T10:51:58.559Z',
|
||||
noteId: 123,
|
||||
});
|
||||
findTimestampLink().vm.$emit('click');
|
||||
|
||||
expect(actions.setTargetNoteHash).toHaveBeenCalled();
|
||||
await waitForPromises();
|
||||
|
||||
expect(useNotes().setTargetNoteHash).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -73,7 +73,8 @@ describe('noteable_discussion component', () => {
|
|||
beforeEach(() => {
|
||||
pinia = createTestingPinia({ plugins: [globalAccessorPlugin] });
|
||||
useLegacyDiffs();
|
||||
useNotes();
|
||||
useNotes().saveNote.mockResolvedValue();
|
||||
useNotes().fetchDiscussionDiffLines.mockResolvedValue();
|
||||
useBatchComments();
|
||||
axiosMock = new MockAdapter(axios);
|
||||
createComponent();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import TagsHeader from '~/packages_and_registries/harbor_registry/components/tags/tags_header.vue';
|
||||
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
|
||||
import { EMPTY_TAG_LABEL } from '~/packages_and_registries/harbor_registry/constants';
|
||||
import { mockArtifactDetail, MOCK_SHA_DIGEST } from '../../mock_data';
|
||||
|
||||
describe('Harbor Tags Header', () => {
|
||||
|
@ -28,7 +28,11 @@ describe('Harbor Tags Header', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
mountComponent({
|
||||
propsData: { artifactDetail: mockArtifactDetail, pageInfo: mockPageInfo, tagsLoading: false },
|
||||
propsData: {
|
||||
artifactDetail: mockArtifactDetail,
|
||||
pageInfo: mockPageInfo,
|
||||
tagsLoading: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -39,10 +43,54 @@ describe('Harbor Tags Header', () => {
|
|||
});
|
||||
|
||||
describe('tags count', () => {
|
||||
it('would has the correct text', async () => {
|
||||
await nextTick();
|
||||
|
||||
it('displays the tags count', () => {
|
||||
expect(findTagsCount().props('text')).toBe('1 tag');
|
||||
});
|
||||
|
||||
describe('when pageInfo.total is NaN', () => {
|
||||
const nanMockPageInfo = {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: NaN,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mountComponent({
|
||||
propsData: {
|
||||
artifactDetail: mockArtifactDetail,
|
||||
pageInfo: nanMockPageInfo,
|
||||
tagsLoading: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('displays empty label when there are no tags', () => {
|
||||
expect(findTagsCount().props('text')).toBe(EMPTY_TAG_LABEL);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when pageInfo.total is 0', () => {
|
||||
const nanMockPageInfo = {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mountComponent({
|
||||
propsData: {
|
||||
artifactDetail: mockArtifactDetail,
|
||||
pageInfo: nanMockPageInfo,
|
||||
tagsLoading: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('displays empty label when there are no tags', () => {
|
||||
expect(findTagsCount().props('text')).toBe(EMPTY_TAG_LABEL);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ import axios from '~/lib/utils/axios_utils';
|
|||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
import * as urlUtils from '~/lib/utils/url_utility';
|
||||
import * as actions from '~/search/store/actions';
|
||||
|
||||
import {
|
||||
GROUPS_LOCAL_STORAGE_KEY,
|
||||
PROJECTS_LOCAL_STORAGE_KEY,
|
||||
|
@ -22,6 +22,7 @@ import {
|
|||
import * as types from '~/search/store/mutation_types';
|
||||
import createState from '~/search/store/state';
|
||||
import * as storeUtils from '~/search/store/utils';
|
||||
import * as actions from '~/search/store/actions';
|
||||
import {
|
||||
MOCK_QUERY,
|
||||
MOCK_GROUPS,
|
||||
|
@ -203,26 +204,12 @@ describe('Global Search Store Actions', () => {
|
|||
fetchSidebarCountSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should update URL, document title, and history', async () => {
|
||||
it('should update URL, document title, and history', () => {
|
||||
const getters = { currentScope: 'blobs' };
|
||||
|
||||
await actions.setQuery({ state, commit, getters }, payload);
|
||||
|
||||
expect(setUrlParams).toHaveBeenCalledWith(
|
||||
{ ...state.query },
|
||||
window.location.href,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(document.title).toBe(state.query.search);
|
||||
|
||||
expect(updateHistory).toHaveBeenCalledWith({
|
||||
state: state.query,
|
||||
title: state.query.search,
|
||||
url: 'mocked-new-url',
|
||||
replace: false,
|
||||
});
|
||||
return testAction(actions.setQuery, payload, { ...state, ...getters }, [
|
||||
{ type: types.SET_QUERY, payload: { key: 'some-key', value: 'some-value' } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not update URL or fetch sidebar counts when conditions are not met', async () => {
|
||||
|
@ -268,6 +255,156 @@ describe('Global Search Store Actions', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('zoekt search type with blob scope - page handling scenarios', () => {
|
||||
let originalGon;
|
||||
let commit;
|
||||
let fetchSidebarCountSpy;
|
||||
let modifySearchQuerySpy;
|
||||
|
||||
beforeEach(() => {
|
||||
originalGon = window.gon;
|
||||
commit = jest.fn();
|
||||
|
||||
fetchSidebarCountSpy = jest
|
||||
.spyOn(actions, 'fetchSidebarCount')
|
||||
.mockImplementation(() => Promise.resolve());
|
||||
|
||||
modifySearchQuerySpy = jest
|
||||
.spyOn(storeUtils, 'modifySearchQuery')
|
||||
.mockReturnValue('mocked-clean-url');
|
||||
|
||||
window.gon = { features: {} };
|
||||
storeUtils.isSidebarDirty = jest.fn().mockReturnValue(false);
|
||||
storeUtils.buildDocumentTitle = jest.fn().mockReturnValue('Built Document Title');
|
||||
|
||||
state = createState({
|
||||
query: { ...MOCK_QUERY, search: 'test-search' },
|
||||
navigation: { ...MOCK_NAVIGATION },
|
||||
searchType: 'zoekt',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.gon = originalGon;
|
||||
fetchSidebarCountSpy.mockRestore();
|
||||
modifySearchQuerySpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('when only "page" attribute changes', () => {
|
||||
it('should only update history without fetching sidebar counts', async () => {
|
||||
const getters = { currentScope: 'blobs' };
|
||||
const payload = { key: 'page', value: 2 };
|
||||
|
||||
await actions.setQuery({ state, commit, getters }, payload);
|
||||
|
||||
expect(setUrlParams).toHaveBeenCalledWith(
|
||||
{ ...state.query },
|
||||
window.location.href,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(updateHistory).toHaveBeenCalledWith({
|
||||
state: state.query,
|
||||
title: state.query.search,
|
||||
url: 'mocked-new-url',
|
||||
replace: true,
|
||||
});
|
||||
|
||||
expect(fetchSidebarCountSpy).not.toHaveBeenCalled();
|
||||
expect(commit).toHaveBeenCalledWith(types.SET_QUERY, payload);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when "search" attribute changes and page attribute is not present', () => {
|
||||
beforeEach(() => {
|
||||
const res = { count: 666 };
|
||||
mock.onGet().replyOnce(HTTP_STATUS_OK, res);
|
||||
});
|
||||
|
||||
it('should update URL, title, history and fetch sidebar counts', async () => {
|
||||
const getters = { currentScope: 'blobs' };
|
||||
const payload = { key: 'search', value: 'new-search' };
|
||||
|
||||
state.query = { ...state.query };
|
||||
delete state.query.page;
|
||||
state.urlQuery = { ...state.urlQuery };
|
||||
delete state.urlQuery.page;
|
||||
|
||||
await testAction(
|
||||
actions.setQuery,
|
||||
payload,
|
||||
{ ...state, ...getters },
|
||||
[{ type: types.SET_QUERY, payload: { key: 'search', value: 'new-search' } }],
|
||||
[{ type: 'fetchSidebarCount' }],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when "search" attribute changes but page is present and not equal to 1', () => {
|
||||
it('should reset page to 1, update URL with clean URL, and fetch sidebar counts', () => {
|
||||
const getters = { currentScope: 'blobs' };
|
||||
const payload = { key: 'search', value: 'new-search' };
|
||||
|
||||
state.query = { ...state.query, page: 3 };
|
||||
state.urlQuery = { ...state.urlQuery, page: 3 };
|
||||
|
||||
return testAction(
|
||||
actions.setQuery,
|
||||
payload,
|
||||
{ ...state, ...getters },
|
||||
[
|
||||
{ type: types.SET_QUERY, payload: { key: 'search', value: 'new-search' } },
|
||||
{ type: types.SET_QUERY, payload: { key: 'page', value: 1 } },
|
||||
],
|
||||
[{ type: 'fetchSidebarCount' }],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when "search" attribute changes but page is present and equal to 1', () => {
|
||||
it('should not modify URL for page, update history with original URL', async () => {
|
||||
const getters = { currentScope: 'blobs' };
|
||||
const payload = { key: 'search', value: 'new-search' };
|
||||
|
||||
state.query = { ...state.query, page: 1 };
|
||||
state.urlQuery = { ...state.urlQuery, page: 1 };
|
||||
|
||||
await testAction(
|
||||
actions.setQuery,
|
||||
payload,
|
||||
{ ...state, ...getters },
|
||||
[{ type: types.SET_QUERY, payload: { key: 'search', value: 'new-search' } }],
|
||||
[{ type: 'fetchSidebarCount' }],
|
||||
);
|
||||
|
||||
expect(updateHistory).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when urlQuery has no page but state.query has page', () => {
|
||||
it('should use original URL without modification', () => {
|
||||
const getters = { currentScope: 'blobs' };
|
||||
const payload = { key: 'search', value: 'new-search' };
|
||||
|
||||
state.query = { ...state.query, page: 2 };
|
||||
state.urlQuery = { ...state.urlQuery };
|
||||
delete state.urlQuery.page;
|
||||
|
||||
return testAction(
|
||||
actions.setQuery,
|
||||
payload,
|
||||
{ ...state, ...getters },
|
||||
[
|
||||
{ type: types.SET_QUERY, payload: { key: 'search', value: 'new-search' } },
|
||||
{ type: types.SET_QUERY, payload: { key: 'page', value: 1 } },
|
||||
],
|
||||
[{ type: 'fetchSidebarCount' }],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyQuery', () => {
|
||||
|
@ -410,7 +547,7 @@ describe('Global Search Store Actions', () => {
|
|||
it(`should ${expectedMutations.length === 0 ? 'NOT' : ''} dispatch ${
|
||||
expectedMutations.length === 0 ? '' : 'the correct'
|
||||
} mutations for ${scope}`, () => {
|
||||
return testAction({ action, state, expectedMutations }).then(() => {
|
||||
return testAction(action, undefined, state, expectedMutations, []).then(() => {
|
||||
expect(logger.logError).toHaveBeenCalledTimes(errorLogs);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { GlAnimatedChevronLgDownUpIcon } from '@gitlab/ui';
|
||||
import { nextTick } from 'vue';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
|
@ -11,6 +12,7 @@ import WidgetContentRow from '~/vue_merge_request_widget/components/widget/widge
|
|||
import ReportListItem from '~/merge_requests/reports/components/report_list_item.vue';
|
||||
import * as logger from '~/lib/logger';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
|
||||
jest.mock('~/vue_merge_request_widget/components/widget/telemetry', () => ({
|
||||
|
@ -28,6 +30,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
|
|||
const findExpandedSection = () => wrapper.findByTestId('widget-extension-collapsed-section');
|
||||
const findActionButtons = () => wrapper.findComponent(ActionButtons);
|
||||
const findToggleButton = () => wrapper.findByTestId('toggle-button');
|
||||
const findToggleChevron = () => findToggleButton().findComponent(GlAnimatedChevronLgDownUpIcon);
|
||||
const findHelpPopover = () => wrapper.findComponent(HelpPopover);
|
||||
const findDynamicScroller = () => wrapper.findByTestId('dynamic-content-scroller');
|
||||
|
||||
|
@ -282,6 +285,27 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
|
|||
expect(findToggleButton().attributes('aria-label')).toBe('Show details');
|
||||
});
|
||||
|
||||
it('displays the chevron correctly when toggle is clicked', async () => {
|
||||
await createComponent({
|
||||
propsData: {
|
||||
isCollapsible: true,
|
||||
},
|
||||
slots: {
|
||||
content: '<b>More complex content</b>',
|
||||
},
|
||||
});
|
||||
|
||||
// Vue compat doesn't know about component props if it extends other component
|
||||
expect(
|
||||
findToggleChevron().props('isOn') ?? parseBoolean(findToggleChevron().attributes('is-on')),
|
||||
).toBe(false);
|
||||
|
||||
findToggleButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
expect(
|
||||
findToggleChevron().props('isOn') ?? parseBoolean(findToggleChevron().attributes('is-on')),
|
||||
).toBe(true);
|
||||
});
|
||||
it('does not display the content slot until toggle is clicked', async () => {
|
||||
await createComponent({
|
||||
propsData: {
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlDisclosureDropdown, GlToggle, GlDisclosureDropdownItem } from '@gitlab/ui';
|
||||
import { createAlert } from '~/alert';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import WorkItemsListPreferences from '~/work_items/components/shared/work_item_list_preferences.vue';
|
||||
import updateWorkItemsDisplaySettings from '~/work_items/graphql/update_user_preferences.mutation.graphql';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
jest.mock('~/alert');
|
||||
|
||||
describe('WorkItemsListPreferences', () => {
|
||||
let wrapper;
|
||||
let mockApolloProvider;
|
||||
|
||||
const successHandler = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
userPreferencesUpdate: {
|
||||
__typename: 'UserPreferencesUpdatePayload',
|
||||
userPreferences: {
|
||||
__typename: 'UserPreferences',
|
||||
workItemsDisplaySettings: { shouldOpenItemsInSidePanel: false },
|
||||
},
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createComponent = ({ props = {}, provide = {}, mutationHandler = successHandler } = {}) => {
|
||||
mockApolloProvider = createMockApollo([[updateWorkItemsDisplaySettings, mutationHandler]]);
|
||||
|
||||
wrapper = shallowMount(WorkItemsListPreferences, {
|
||||
apolloProvider: mockApolloProvider,
|
||||
propsData: {
|
||||
displaySettings: { shouldOpenItemsInSidePanel: true },
|
||||
...props,
|
||||
},
|
||||
provide: { isSignedIn: true, ...provide },
|
||||
});
|
||||
};
|
||||
|
||||
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
|
||||
const findToggle = () => wrapper.findComponent(GlToggle);
|
||||
const findDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
|
||||
|
||||
describe('when user is signed in', () => {
|
||||
it('renders dropdown with toggle', () => {
|
||||
createComponent();
|
||||
expect(findDropdown().exists()).toBe(true);
|
||||
expect(findToggle().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders toggle with correct initial value', () => {
|
||||
createComponent();
|
||||
expect(findToggle().props('value')).toBe(true);
|
||||
});
|
||||
|
||||
it('handles empty displaySettings gracefully', () => {
|
||||
createComponent({ props: { displaySettings: {} } });
|
||||
expect(findToggle().props('value')).toBe(true); // defaults to true
|
||||
});
|
||||
|
||||
describe('when toggle is clicked', () => {
|
||||
it('saves preference and emits event on success', async () => {
|
||||
createComponent();
|
||||
|
||||
findDropdownItem().vm.$emit('action');
|
||||
await waitForPromises();
|
||||
|
||||
expect(successHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
workItemsDisplaySettings: { shouldOpenItemsInSidePanel: false },
|
||||
},
|
||||
});
|
||||
expect(wrapper.emitted('displaySettingsChanged')).toHaveLength(1);
|
||||
expect(wrapper.emitted('displaySettingsChanged')[0][0]).toEqual({
|
||||
shouldOpenItemsInSidePanel: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows loading state while saving', async () => {
|
||||
createComponent();
|
||||
|
||||
// Store the initial state
|
||||
expect(findToggle().props('isLoading')).toBe(false);
|
||||
|
||||
findDropdownItem().vm.$emit('action');
|
||||
await nextTick();
|
||||
|
||||
// Check loading state
|
||||
expect(findToggle().props('isLoading')).toBe(true);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
// Check state after loading
|
||||
expect(findToggle().props('isLoading')).toBe(false);
|
||||
});
|
||||
|
||||
it('handles mutation errors gracefully', async () => {
|
||||
const error = new Error('Network error');
|
||||
const errorHandler = jest.fn().mockRejectedValue(error);
|
||||
|
||||
createComponent({ mutationHandler: errorHandler });
|
||||
|
||||
findDropdownItem().vm.$emit('action');
|
||||
await waitForPromises();
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
message: 'Something went wrong while saving the preference.',
|
||||
captureError: true,
|
||||
error,
|
||||
});
|
||||
expect(wrapper.emitted('displaySettingsChanged')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('dropdown visibility', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('shows tooltip when dropdown is closed', () => {
|
||||
expect(wrapper.vm.tooltipText).toBe('Display options');
|
||||
});
|
||||
|
||||
it('hides tooltip when dropdown is open', async () => {
|
||||
findDropdown().vm.$emit('shown');
|
||||
await nextTick();
|
||||
expect(wrapper.vm.tooltipText).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user is not signed in', () => {
|
||||
it('does not render dropdown', () => {
|
||||
createComponent({ provide: { isSignedIn: false } });
|
||||
expect(findDropdown().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -23,6 +23,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
|||
import { STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
|
||||
import { CREATED_DESC, UPDATED_DESC, urlSortParams } from '~/issues/list/constants';
|
||||
import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
|
||||
import getUserWorkItemsDisplaySettingsPreferences from '~/work_items/graphql/get_user_preferences.query.graphql';
|
||||
import { scrollUp } from '~/lib/utils/scroll_utils';
|
||||
import { getParameterByName, removeParams, updateHistory } from '~/lib/utils/url_utility';
|
||||
import {
|
||||
|
@ -45,6 +46,7 @@ import {
|
|||
TOKEN_TYPE_UPDATED,
|
||||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
|
||||
import WorkItemUserPreferences from '~/work_items/components/shared/work_item_list_preferences.vue';
|
||||
import CreateWorkItemModal from '~/work_items/components/create_work_item_modal.vue';
|
||||
import WorkItemsListApp from '~/work_items/pages/work_items_list_app.vue';
|
||||
import getWorkItemStateCountsQuery from 'ee_else_ce/work_items/graphql/list/get_work_item_state_counts.query.graphql';
|
||||
|
@ -94,6 +96,11 @@ describeSkipVue3(skipReason, () => {
|
|||
.fn()
|
||||
.mockResolvedValue(groupWorkItemStateCountsQueryResponse);
|
||||
const mutationHandler = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
|
||||
const mockPreferencesQueryHandler = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
currentUser: null,
|
||||
},
|
||||
});
|
||||
|
||||
const findIssuableList = () => wrapper.findComponent(IssuableList);
|
||||
const findIssueCardStatistics = () => wrapper.findComponent(IssueCardStatistics);
|
||||
|
@ -105,6 +112,7 @@ describeSkipVue3(skipReason, () => {
|
|||
const findBulkEditStartButton = () => wrapper.find('[data-testid="bulk-edit-start-button"]');
|
||||
const findBulkEditSidebar = () => wrapper.findComponent(WorkItemBulkEditSidebar);
|
||||
const findWorkItemListHeading = () => wrapper.findComponent(WorkItemListHeading);
|
||||
const findWorkItemUserPreferences = () => wrapper.findComponent(WorkItemUserPreferences);
|
||||
|
||||
const mountComponent = ({
|
||||
provide = {},
|
||||
|
@ -112,6 +120,7 @@ describeSkipVue3(skipReason, () => {
|
|||
slimQueryHandler = defaultSlimQueryHandler,
|
||||
countsQueryHandler = defaultCountsQueryHandler,
|
||||
sortPreferenceMutationResponse = mutationHandler,
|
||||
mockPreferencesHandler = mockPreferencesQueryHandler,
|
||||
workItemsToggleEnabled = true,
|
||||
workItemPlanningView = false,
|
||||
props = {},
|
||||
|
@ -131,6 +140,7 @@ describeSkipVue3(skipReason, () => {
|
|||
[getWorkItemsSlimQuery, slimQueryHandler],
|
||||
[getWorkItemStateCountsQuery, countsQueryHandler],
|
||||
[setSortPreferenceMutation, sortPreferenceMutationResponse],
|
||||
[getUserWorkItemsDisplaySettingsPreferences, mockPreferencesHandler],
|
||||
...additionalHandlers,
|
||||
]),
|
||||
provide: {
|
||||
|
@ -722,6 +732,47 @@ describeSkipVue3(skipReason, () => {
|
|||
expect(findDrawer().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('display settings', () => {
|
||||
it('updates displaySettings when displaySettingsChanged event is emitted', async () => {
|
||||
mountComponent();
|
||||
await waitForPromises();
|
||||
|
||||
const newSettings = { shouldOpenItemsInSidePanel: false };
|
||||
findWorkItemUserPreferences().vm.$emit('displaySettingsChanged', newSettings);
|
||||
await nextTick();
|
||||
|
||||
expect(findWorkItemUserPreferences().props('displaySettings')).toEqual(newSettings);
|
||||
});
|
||||
|
||||
describe('workItemDrawerEnabled with display settings', () => {
|
||||
it('returns false when shouldOpenItemsInSidePanel is false', async () => {
|
||||
const mockHandler = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
currentUser: {
|
||||
id: 'gid://gitlab/User/1',
|
||||
userPreferences: {
|
||||
workItemsDisplaySettings: { shouldOpenItemsInSidePanel: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mountComponent({
|
||||
mockPreferencesHandler: mockHandler,
|
||||
provide: {
|
||||
glFeatures: { workItemViewForIssues: true },
|
||||
isSignedIn: true,
|
||||
},
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
await nextTick();
|
||||
|
||||
expect(findIssuableList().props('preventRedirect')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('selecting issues', () => {
|
||||
const issue = workItemsQueryResponseCombined.data.namespace.workItems.nodes[0];
|
||||
const payload = {
|
||||
|
|
|
@ -613,7 +613,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
|
|||
:create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
|
||||
:create_cluster, :read_cluster, :update_cluster, :admin_cluster,
|
||||
:create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment,
|
||||
:download_code, :build_download_code
|
||||
:download_code, :build_download_code, :read_code
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -1530,6 +1530,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
|
|||
let!(:deploy_keys_project) { create(:deploy_keys_project, project: project, deploy_key: deploy_key) }
|
||||
|
||||
it { is_expected.to be_allowed(:download_code) }
|
||||
it { is_expected.to be_allowed(:read_code) }
|
||||
it { is_expected.to be_disallowed(:push_code) }
|
||||
it { is_expected.to be_disallowed(:read_project) }
|
||||
end
|
||||
|
@ -1538,12 +1539,14 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
|
|||
let!(:deploy_keys_project) { create(:deploy_keys_project, :write_access, project: project, deploy_key: deploy_key) }
|
||||
|
||||
it { is_expected.to be_allowed(:download_code) }
|
||||
it { is_expected.to be_allowed(:read_code) }
|
||||
it { is_expected.to be_allowed(:push_code) }
|
||||
it { is_expected.to be_disallowed(:read_project) }
|
||||
end
|
||||
|
||||
context 'when the deploy key is not enabled in the project' do
|
||||
it { is_expected.to be_disallowed(:download_code) }
|
||||
it { is_expected.to be_disallowed(:read_code) }
|
||||
it { is_expected.to be_disallowed(:push_code) }
|
||||
it { is_expected.to be_disallowed(:read_project) }
|
||||
end
|
||||
|
@ -3690,6 +3693,7 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
|
|||
with_them do
|
||||
it do
|
||||
expect(subject.can?(:download_code)).to be(allowed)
|
||||
expect(subject.can?(:read_code)).to be(allowed)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -3710,33 +3714,13 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do
|
|||
with_them do
|
||||
it do
|
||||
expect(subject.can?(:download_code)).to be(allowed)
|
||||
expect(subject.can?(:read_code)).to be(allowed)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'read_code' do
|
||||
let(:current_user) { create(:user) }
|
||||
|
||||
before do
|
||||
allow(subject).to receive(:allowed?).and_call_original
|
||||
allow(subject).to receive(:allowed?).with(:download_code).and_return(can_download_code)
|
||||
end
|
||||
|
||||
context 'when the current_user can download_code' do
|
||||
let(:can_download_code) { true }
|
||||
|
||||
it { expect_allowed(:read_code) }
|
||||
end
|
||||
|
||||
context 'when the current_user cannot download_code' do
|
||||
let(:can_download_code) { false }
|
||||
|
||||
it { expect_disallowed(:read_code) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'read_namespace_catalog' do
|
||||
let(:current_user) { owner }
|
||||
|
||||
|
|
|
@ -49,6 +49,16 @@ RSpec.describe Integrations::HarborSerializers::ArtifactEntity, feature_category
|
|||
})
|
||||
end
|
||||
|
||||
context 'when artifact has no tags' do
|
||||
before do
|
||||
artifact['tags'] = nil
|
||||
end
|
||||
|
||||
it 'returns an empty array for tags' do
|
||||
expect(subject[:tags]).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with data that may contain path traversal attacks' do
|
||||
before do
|
||||
artifact['digest'] = './../../../../../etc/hosts'
|
||||
|
|
|
@ -4,11 +4,11 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Commits::CherryPickService, feature_category: :source_code_management do
|
||||
let(:project) { create(:project, :repository) }
|
||||
# * ddd0f15ae83993f5cb66a927a28673882e99100b (HEAD -> master, origin/master, origin/HEAD) Merge branch 'po-fix-test-en
|
||||
# * ddd0f15 (HEAD -> master, origin/master, origin/HEAD) Merge branch 'po-fix-test-en
|
||||
# |\
|
||||
# | * 2d1db523e11e777e49377cfb22d368deec3f0793 Correct test_env.rb path for adding branch
|
||||
# | * 2d1db52 Correct test_env.rb path for adding branch
|
||||
# |/
|
||||
# * 1e292f8fedd741b75372e19097c76d327140c312 Merge branch 'cherry-pick-ce369011' into 'master'
|
||||
# * 1e292f8 Merge branch 'cherry-pick-ce369011' into 'master'
|
||||
|
||||
let_it_be(:merge_commit_sha) { 'ddd0f15ae83993f5cb66a927a28673882e99100b' }
|
||||
let_it_be(:merge_base_sha) { '1e292f8fedd741b75372e19097c76d327140c312' }
|
||||
|
@ -69,7 +69,16 @@ RSpec.describe Commits::CherryPickService, feature_category: :source_code_manage
|
|||
it_behaves_like 'successful cherry-pick'
|
||||
|
||||
context 'when picking a merge-request' do
|
||||
let!(:merge_request) { create(:merge_request, :simple, :merged, author: user, source_project: project, merge_commit_sha: merge_commit_sha) }
|
||||
let!(:merge_request) do
|
||||
create(
|
||||
:merge_request,
|
||||
:simple,
|
||||
:merged,
|
||||
author: user,
|
||||
source_project: project,
|
||||
merge_commit_sha: merge_commit_sha
|
||||
)
|
||||
end
|
||||
|
||||
it_behaves_like 'successful cherry-pick'
|
||||
|
||||
|
|
|
@ -129,6 +129,22 @@ RSpec.shared_examples 'a harbor artifacts controller' do |args|
|
|||
|
||||
it_behaves_like 'responds with 200 status with json'
|
||||
end
|
||||
|
||||
context 'with artifacts that have no tags' do
|
||||
let(:mock_artifacts) { [super()[0].merge(tags: nil)] }
|
||||
|
||||
subject do
|
||||
get harbor_artifact_url(container, repository_id), headers: json_header
|
||||
end
|
||||
|
||||
it_behaves_like 'responds with 200 status with json'
|
||||
|
||||
it 'returns empty tags array' do
|
||||
subject
|
||||
|
||||
expect(Gitlab::Json.parse(response.body).first['tags']).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid params' do
|
||||
|
|
Loading…
Reference in New Issue