diff options
28 files changed, 346 insertions, 426 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index bbaaa1152a0..1af466b7274 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -f6ebdc9e7754cf97776fe388f3b0914ae123f5ad +633a58b478d6fcffc59fe600fe8dcbd66bec42f1 diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js index 7f655091cd0..7d6179a8547 100644 --- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js +++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js @@ -11,7 +11,12 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; Vue.use(VueApollo); const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), + defaultClient: createDefaultClient( + {}, + { + assumeImmutableResults: true, + }, + ), }); export default (params = {}) => { diff --git a/app/assets/javascripts/content_editor/extensions/inline_diff.js b/app/assets/javascripts/content_editor/extensions/inline_diff.js new file mode 100644 index 00000000000..9471d324764 --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/inline_diff.js @@ -0,0 +1,50 @@ +import { Mark, markInputRule, mergeAttributes } from '@tiptap/core'; + +export const inputRegexAddition = /(\{\+(.+?)\+\})$/gm; +export const inputRegexDeletion = /(\{-(.+?)-\})$/gm; + +export default Mark.create({ + name: 'inlineDiff', + + defaultOptions: { + HTMLAttributes: {}, + }, + + addAttributes() { + return { + type: { + default: 'addition', + parseHTML: (element) => { + return { + type: element.classList.contains('deletion') ? 'deletion' : 'addition', + }; + }, + }, + }; + }, + + parseHTML() { + return [ + { + tag: 'span.idiff', + }, + ]; + }, + + renderHTML({ HTMLAttributes: { type, ...HTMLAttributes } }) { + return [ + 'span', + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { + class: `idiff left right ${type}`, + }), + 0, + ]; + }, + + addInputRules() { + return [ + markInputRule(inputRegexAddition, this.type, () => ({ type: 'addition' })), + markInputRule(inputRegexDeletion, this.type, () => ({ type: 'deletion' })), + ]; + }, +}); diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js index bf738d5ad36..4bb4d55a816 100644 --- a/app/assets/javascripts/content_editor/services/create_content_editor.js +++ b/app/assets/javascripts/content_editor/services/create_content_editor.js @@ -16,6 +16,7 @@ import Heading from '../extensions/heading'; import History from '../extensions/history'; import HorizontalRule from '../extensions/horizontal_rule'; import Image from '../extensions/image'; +import InlineDiff from '../extensions/inline_diff'; import Italic from '../extensions/italic'; import Link from '../extensions/link'; import ListItem from '../extensions/list_item'; @@ -74,6 +75,7 @@ export const createContentEditor = ({ History, HorizontalRule, Image, + InlineDiff, Italic, Link, ListItem, diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js index f059d83635c..80ad3e15a42 100644 --- a/app/assets/javascripts/content_editor/services/markdown_serializer.js +++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js @@ -13,6 +13,7 @@ import HardBreak from '../extensions/hard_break'; import Heading from '../extensions/heading'; import HorizontalRule from '../extensions/horizontal_rule'; import Image from '../extensions/image'; +import InlineDiff from '../extensions/inline_diff'; import Italic from '../extensions/italic'; import Link from '../extensions/link'; import ListItem from '../extensions/list_item'; @@ -36,6 +37,15 @@ const defaultSerializerConfig = { [Italic.name]: { open: '_', close: '_', mixable: true, expelEnclosingWhitespace: true }, [Subscript.name]: { open: '<sub>', close: '</sub>', mixable: true }, [Superscript.name]: { open: '<sup>', close: '</sup>', mixable: true }, + [InlineDiff.name]: { + mixable: true, + open(state, mark) { + return mark.attrs.type === 'addition' ? '{+' : '{-'; + }, + close(state, mark) { + return mark.attrs.type === 'addition' ? '+}' : '-}'; + }, + }, [Link.name]: { open() { return '['; diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue index 5859fd10688..4ecfc1cf40c 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue @@ -1,27 +1,19 @@ <script> -import { - GlToken, - GlFilteredSearchToken, - GlFilteredSearchSuggestion, - GlDropdownDivider, - GlLoadingIcon, -} from '@gitlab/ui'; -import { debounce } from 'lodash'; - +import { GlFilteredSearchSuggestion } from '@gitlab/ui'; import createFlash from '~/flash'; import { __ } from '~/locale'; - -import { DEBOUNCE_DELAY } from '../constants'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; export default { components: { - GlToken, - GlFilteredSearchToken, + BaseToken, GlFilteredSearchSuggestion, - GlDropdownDivider, - GlLoadingIcon, }, props: { + active: { + type: Boolean, + required: true, + }, config: { type: Object, required: true, @@ -34,82 +26,62 @@ export default { data() { return { branches: this.config.initialBranches || [], - defaultBranches: this.config.defaultBranches || [], - loading: true, + loading: false, }; }, computed: { - currentValue() { - return this.value.data.toLowerCase(); - }, - activeBranch() { - return this.branches.find((branch) => branch.name.toLowerCase() === this.currentValue); - }, - }, - watch: { - active: { - immediate: true, - handler(newValue) { - if (!newValue && !this.branches.length) { - this.fetchBranchBySearchTerm(this.value.data); - } - }, + defaultBranches() { + return this.config.defaultBranches || []; }, }, methods: { - fetchBranchBySearchTerm(searchTerm) { + getActiveBranch(branches, data) { + return branches.find((branch) => branch.name.toLowerCase() === data.toLowerCase()); + }, + fetchBranches(searchTerm) { this.loading = true; this.config .fetchBranches(searchTerm) .then(({ data }) => { this.branches = data; }) - .catch(() => createFlash({ message: __('There was a problem fetching branches.') })) + .catch(() => { + createFlash({ message: __('There was a problem fetching branches.') }); + }) .finally(() => { this.loading = false; }); }, - searchBranches: debounce(function debouncedSearch({ data }) { - this.fetchBranchBySearchTerm(data); - }, DEBOUNCE_DELAY), }, }; </script> <template> - <gl-filtered-search-token + <base-token + :active="active" :config="config" - v-bind="{ ...$props, ...$attrs }" + :value="value" + :default-suggestions="defaultBranches" + :suggestions="branches" + :suggestions-loading="loading" + :get-active-token-value="getActiveBranch" + @fetch-suggestions="fetchBranches" v-on="$listeners" - @input="searchBranches" > - <template #view-token="{ inputValue }"> - <gl-token variant="search-value">{{ - activeBranch ? activeBranch.name : inputValue - }}</gl-token> + <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }"> + {{ activeTokenValue ? activeTokenValue.name : inputValue }} </template> - <template #suggestions> + <template #suggestions-list="{ suggestions }"> <gl-filtered-search-suggestion - v-for="branch in defaultBranches" - :key="branch.value" - :value="branch.value" + v-for="branch in suggestions" + :key="branch.id" + :value="branch.name" > - {{ branch.text }} + <div class="gl-display-flex"> + <span class="gl-display-inline-block gl-mr-3 gl-p-3"></span> + {{ branch.name }} + </div> </gl-filtered-search-suggestion> - <gl-dropdown-divider v-if="defaultBranches.length" /> - <gl-loading-icon v-if="loading" size="sm" /> - <template v-else> - <gl-filtered-search-suggestion - v-for="branch in branches" - :key="branch.id" - :value="branch.name" - > - <div class="gl-display-flex"> - <span class="gl-display-inline-block gl-mr-3 gl-p-3"></span> - <div>{{ branch.name }}</div> - </div> - </gl-filtered-search-suggestion> - </template> </template> - </gl-filtered-search-token> + </base-token> </template> diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue index d186f46866c..5a69751a2cc 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue @@ -1,26 +1,21 @@ <script> -import { - GlFilteredSearchToken, - GlFilteredSearchSuggestion, - GlDropdownDivider, - GlLoadingIcon, -} from '@gitlab/ui'; -import { debounce } from 'lodash'; - +import { GlFilteredSearchSuggestion } from '@gitlab/ui'; import createFlash from '~/flash'; import { __ } from '~/locale'; - -import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY } from '../constants'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; +import { DEFAULT_NONE_ANY } from '../constants'; import { stripQuotes } from '../filtered_search_utils'; export default { components: { - GlFilteredSearchToken, + BaseToken, GlFilteredSearchSuggestion, - GlDropdownDivider, - GlLoadingIcon, }, props: { + active: { + type: Boolean, + required: true, + }, config: { type: Object, required: true, @@ -33,87 +28,63 @@ export default { data() { return { emojis: this.config.initialEmojis || [], - defaultEmojis: this.config.defaultEmojis || DEFAULT_NONE_ANY, - loading: true, + loading: false, }; }, computed: { - currentValue() { - return this.value.data.toLowerCase(); - }, - activeEmoji() { - return this.emojis.find( - (emoji) => emoji.name.toLowerCase() === stripQuotes(this.currentValue), - ); - }, - }, - watch: { - active: { - immediate: true, - handler(newValue) { - if (!newValue && !this.emojis.length) { - this.fetchEmojiBySearchTerm(this.value.data); - } - }, + defaultEmojis() { + return this.config.defaultEmojis || DEFAULT_NONE_ANY; }, }, methods: { - fetchEmojiBySearchTerm(searchTerm) { + getActiveEmoji(emojis, data) { + return emojis.find((emoji) => emoji.name.toLowerCase() === stripQuotes(data).toLowerCase()); + }, + fetchEmojis(searchTerm) { this.loading = true; this.config .fetchEmojis(searchTerm) - .then((res) => { - this.emojis = Array.isArray(res) ? res : res.data; + .then((response) => { + this.emojis = Array.isArray(response) ? response : response.data; + }) + .catch(() => { + createFlash({ message: __('There was a problem fetching emojis.') }); }) - .catch(() => - createFlash({ - message: __('There was a problem fetching emojis.'), - }), - ) .finally(() => { this.loading = false; }); }, - searchEmojis: debounce(function debouncedSearch({ data }) { - this.fetchEmojiBySearchTerm(data); - }, DEBOUNCE_DELAY), }, }; </script> <template> - <gl-filtered-search-token + <base-token + :active="active" :config="config" - v-bind="{ ...$props, ...$attrs }" + :value="value" + :default-suggestions="defaultEmojis" + :suggestions="emojis" + :suggestions-loading="loading" + :get-active-token-value="getActiveEmoji" + @fetch-suggestions="fetchEmojis" v-on="$listeners" - @input="searchEmojis" > - <template #view="{ inputValue }"> - <gl-emoji v-if="activeEmoji" :data-name="activeEmoji.name" /> - <span v-else>{{ inputValue }}</span> + <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }"> + <gl-emoji v-if="activeTokenValue" :data-name="activeTokenValue.name" /> + <template v-else>{{ inputValue }}</template> </template> - <template #suggestions> + <template #suggestions-list="{ suggestions }"> <gl-filtered-search-suggestion - v-for="emoji in defaultEmojis" - :key="emoji.value" - :value="emoji.value" + v-for="emoji in suggestions" + :key="emoji.name" + :value="emoji.name" > - {{ emoji.value }} + <div class="gl-display-flex"> + <gl-emoji class="gl-mr-3" :data-name="emoji.name" /> + {{ emoji.name }} + </div> </gl-filtered-search-suggestion> - <gl-dropdown-divider v-if="defaultEmojis.length" /> - <gl-loading-icon v-if="loading" size="sm" /> - <template v-else> - <gl-filtered-search-suggestion - v-for="emoji in emojis" - :key="emoji.name" - :value="emoji.name" - > - <div class="gl-display-flex"> - <gl-emoji :data-name="emoji.name" /> - <span class="gl-ml-3">{{ emoji.name }}</span> - </div> - </gl-filtered-search-suggestion> - </template> </template> - </gl-filtered-search-token> + </base-token> </template> diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue index ba8b2421726..c1d1bc7da91 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue @@ -1,24 +1,21 @@ <script> -import { - GlDropdownDivider, - GlFilteredSearchSuggestion, - GlFilteredSearchToken, - GlLoadingIcon, -} from '@gitlab/ui'; -import { debounce } from 'lodash'; +import { GlFilteredSearchSuggestion } from '@gitlab/ui'; import createFlash from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { __ } from '~/locale'; -import { DEBOUNCE_DELAY, DEFAULT_ITERATIONS } from '../constants'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; +import { DEFAULT_ITERATIONS } from '../constants'; export default { components: { - GlDropdownDivider, + BaseToken, GlFilteredSearchSuggestion, - GlFilteredSearchToken, - GlLoadingIcon, }, props: { + active: { + type: Boolean, + required: true, + }, config: { type: Object, required: true, @@ -35,84 +32,58 @@ export default { }; }, computed: { - currentValue() { - return this.value.data; - }, - activeIteration() { - return this.iterations.find( - (iteration) => getIdFromGraphQLId(iteration.id) === Number(this.currentValue), - ); - }, defaultIterations() { return this.config.defaultIterations || DEFAULT_ITERATIONS; }, }, - watch: { - active: { - immediate: true, - handler(newValue) { - if (!newValue && !this.iterations.length) { - this.fetchIterationBySearchTerm(this.currentValue); - } - }, - }, - }, methods: { - getValue(iteration) { - return String(getIdFromGraphQLId(iteration.id)); + getActiveIteration(iterations, data) { + return iterations.find((iteration) => this.getValue(iteration) === data); }, - fetchIterationBySearchTerm(searchTerm) { - const fetchPromise = this.config.fetchPath - ? this.config.fetchIterations(this.config.fetchPath, searchTerm) - : this.config.fetchIterations(searchTerm); - + fetchIterations(searchTerm) { this.loading = true; - - fetchPromise + this.config + .fetchIterations(searchTerm) .then((response) => { this.iterations = Array.isArray(response) ? response : response.data; }) - .catch(() => createFlash({ message: __('There was a problem fetching iterations.') })) + .catch(() => { + createFlash({ message: __('There was a problem fetching iterations.') }); + }) .finally(() => { this.loading = false; }); }, - searchIterations: debounce(function debouncedSearch({ data }) { - this.fetchIterationBySearchTerm(data); - }, DEBOUNCE_DELAY), + getValue(iteration) { + return String(getIdFromGraphQLId(iteration.id)); + }, }, }; </script> <template> - <gl-filtered-search-token + <base-token + :active="active" :config="config" - v-bind="{ ...$props, ...$attrs }" + :value="value" + :default-suggestions="defaultIterations" + :suggestions="iterations" + :suggestions-loading="loading" + :get-active-token-value="getActiveIteration" + @fetch-suggestions="fetchIterations" v-on="$listeners" - @input="searchIterations" > - <template #view="{ inputValue }"> - {{ activeIteration ? activeIteration.title : inputValue }} + <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }"> + {{ activeTokenValue ? activeTokenValue.title : inputValue }} </template> - <template #suggestions> + <template #suggestions-list="{ suggestions }"> <gl-filtered-search-suggestion - v-for="iteration in defaultIterations" - :key="iteration.value" - :value="iteration.value" + v-for="iteration in suggestions" + :key="iteration.id" + :value="getValue(iteration)" > - {{ iteration.text }} + {{ iteration.title }} </gl-filtered-search-suggestion> - <gl-dropdown-divider v-if="defaultIterations.length" /> - <gl-loading-icon v-if="loading" size="sm" /> - <template v-else> - <gl-filtered-search-suggestion - v-for="iteration in iterations" - :key="iteration.id" - :value="getValue(iteration)" - > - {{ iteration.title }} - </gl-filtered-search-suggestion> - </template> </template> - </gl-filtered-search-token> + </base-token> </template> diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue index 66ad5ef5b4e..4b9ad6d8f91 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue @@ -1,27 +1,22 @@ <script> -import { - GlFilteredSearchToken, - GlFilteredSearchSuggestion, - GlDropdownDivider, - GlLoadingIcon, -} from '@gitlab/ui'; -import { debounce } from 'lodash'; - +import { GlFilteredSearchSuggestion } from '@gitlab/ui'; import createFlash from '~/flash'; import { __ } from '~/locale'; import { sortMilestonesByDueDate } from '~/milestones/milestone_utils'; - -import { DEFAULT_MILESTONES, DEBOUNCE_DELAY } from '../constants'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; +import { DEFAULT_MILESTONES } from '../constants'; import { stripQuotes } from '../filtered_search_utils'; export default { components: { - GlFilteredSearchToken, + BaseToken, GlFilteredSearchSuggestion, - GlDropdownDivider, - GlLoadingIcon, }, props: { + active: { + type: Boolean, + required: true, + }, config: { type: Object, required: true, @@ -34,36 +29,21 @@ export default { data() { return { milestones: this.config.initialMilestones || [], - defaultMilestones: this.config.defaultMilestones || DEFAULT_MILESTONES, loading: false, }; }, computed: { - currentValue() { - return this.value.data.toLowerCase(); - }, - activeMilestone() { - return this.milestones.find( - (milestone) => milestone.title.toLowerCase() === stripQuotes(this.currentValue), - ); - }, - }, - watch: { - active: { - immediate: true, - handler(newValue) { - if (!newValue && !this.milestones.length) { - this.fetchMilestoneBySearchTerm(this.value.data); - } - }, + defaultMilestones() { + return this.config.defaultMilestones || DEFAULT_MILESTONES; }, }, methods: { - fetchMilestoneBySearchTerm(searchTerm = '') { - if (this.loading) { - return; - } - + getActiveMilestone(milestones, data) { + return milestones.find( + (milestone) => milestone.title.toLowerCase() === stripQuotes(data).toLowerCase(), + ); + }, + fetchMilestones(searchTerm) { this.loading = true; this.config .fetchMilestones(searchTerm) @@ -71,47 +51,40 @@ export default { const data = Array.isArray(response) ? response : response.data; this.milestones = data.slice().sort(sortMilestonesByDueDate); }) - .catch(() => createFlash({ message: __('There was a problem fetching milestones.') })) + .catch(() => { + createFlash({ message: __('There was a problem fetching milestones.') }); + }) .finally(() => { this.loading = false; }); }, - searchMilestones: debounce(function debouncedSearch({ data }) { - this.fetchMilestoneBySearchTerm(data); - }, DEBOUNCE_DELAY), }, }; </script> <template> - <gl-filtered-search-token + <base-token + :active="active" :config="config" - v-bind="{ ...$props, ...$attrs }" + :value="value" + :default-suggestions="defaultMilestones" + :suggestions="milestones" + :suggestions-loading="loading" + :get-active-token-value="getActiveMilestone" + @fetch-suggestions="fetchMilestones" v-on="$listeners" - @input="searchMilestones" > - <template #view="{ inputValue }"> - <span>%{{ activeMilestone ? activeMilestone.title : inputValue }}</span> + <template #view="{ viewTokenProps: { inputValue, activeTokenValue } }"> + %{{ activeTokenValue ? activeTokenValue.title : inputValue }} </template> - <template #suggestions> + <template #suggestions-list="{ suggestions }"> <gl-filtered-search-suggestion - v-for="milestone in defaultMilestones" - :key="milestone.value" - :value="milestone.value" + v-for="milestone in suggestions" + :key="milestone.id" + :value="milestone.title" > - {{ milestone.text }} + {{ milestone.title }} </gl-filtered-search-suggestion> - <gl-dropdown-divider v-if="defaultMilestones.length" /> - <gl-loading-icon v-if="loading" size="sm" /> - <template v-else> - <gl-filtered-search-suggestion - v-for="milestone in milestones" - :key="milestone.id" - :value="milestone.title" - > - <div>{{ milestone.title }}</div> - </gl-filtered-search-suggestion> - </template> </template> - </gl-filtered-search-token> + </base-token> </template> diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue index 10b68021604..280fb234576 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue @@ -1,16 +1,20 @@ <script> -import { GlDropdownDivider, GlFilteredSearchSuggestion, GlFilteredSearchToken } from '@gitlab/ui'; +import { GlFilteredSearchSuggestion } from '@gitlab/ui'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; import { DEFAULT_NONE_ANY, WEIGHT_TOKEN_SUGGESTIONS_SIZE } from '../constants'; const weights = Array.from(Array(WEIGHT_TOKEN_SUGGESTIONS_SIZE), (_, index) => index.toString()); export default { components: { - GlDropdownDivider, + BaseToken, GlFilteredSearchSuggestion, - GlFilteredSearchToken, }, props: { + active: { + type: Boolean, + required: true, + }, config: { type: Object, required: true, @@ -23,12 +27,19 @@ export default { data() { return { weights, - defaultWeights: this.config.defaultWeights || DEFAULT_NONE_ANY, }; }, + computed: { + defaultWeights() { + return this.config.defaultWeights || DEFAULT_NONE_ANY; + }, + }, methods: { - updateWeights({ data }) { - const weight = parseInt(data, 10); + getActiveWeight(weightSuggestions, data) { + return weightSuggestions.find((weight) => weight === data); + }, + updateWeights(searchTerm) { + const weight = parseInt(searchTerm, 10); this.weights = Number.isNaN(weight) ? weights : [String(weight)]; }, }, @@ -36,24 +47,20 @@ export default { </script> <template> - <gl-filtered-search-token + <base-token + :active="active" :config="config" - v-bind="{ ...$props, ...$attrs }" + :value="value" + :default-suggestions="defaultWeights" + :suggestions="weights" + :get-active-token-value="getActiveWeight" + @fetch-suggestions="updateWeights" v-on="$listeners" - @input="updateWeights" > - <template #suggestions> - <gl-filtered-search-suggestion - v-for="weight in defaultWeights" - :key="weight.value" - :value="weight.value" - > - {{ weight.text }} - </gl-filtered-search-suggestion> - <gl-dropdown-divider v-if="defaultWeights.length" /> - <gl-filtered-search-suggestion v-for="weight of weights" :key="weight" :value="weight"> + <template #suggestions-list="{ suggestions }"> + <gl-filtered-search-suggestion v-for="weight of suggestions" :key="weight" :value="weight"> {{ weight }} </gl-filtered-search-suggestion> </template> - </gl-filtered-search-token> + </base-token> </template> diff --git a/app/models/group.rb b/app/models/group.rb index a0ab4182af6..f6b45a755e4 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -296,7 +296,7 @@ class Group < Namespace end def add_users(users, access_level, current_user: nil, expires_at: nil) - Members::Groups::CreatorService.add_users( # rubocop:todo CodeReuse/ServiceClass + Members::Groups::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass self, users, access_level, @@ -306,7 +306,7 @@ class Group < Namespace end def add_user(user, access_level, current_user: nil, expires_at: nil, ldap: false) - Members::Groups::CreatorService.new(self, # rubocop:todo CodeReuse/ServiceClass + Members::Groups::CreatorService.new(self, # rubocop:disable CodeReuse/ServiceClass user, access_level, current_user: current_user, diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 49144d9dbf3..b45c0b6a0cc 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -44,7 +44,7 @@ class ProjectMember < Member project_ids.each do |project_id| project = Project.find(project_id) - Members::Projects::CreatorService.add_users( # rubocop:todo CodeReuse/ServiceClass + Members::Projects::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass project, users, access_level, diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 4323f89865a..2e45753c182 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -16,7 +16,7 @@ class NotificationSetting < ApplicationRecord validates :user_id, uniqueness: { scope: [:source_type, :source_id], message: "already exists in source", allow_nil: true } - validate :owns_notification_email, if: :notification_email_changed? + validate :notification_email_verified, if: :notification_email_changed? scope :for_groups, -> { where(source_type: 'Namespace') } @@ -110,11 +110,11 @@ class NotificationSetting < ApplicationRecord has_attribute?(event) && !!read_attribute(event) end - def owns_notification_email + def notification_email_verified return if user.temp_oauth_email? return if notification_email.empty? - errors.add(:notification_email, _("is not an email you own")) unless user.verified_emails.include?(notification_email) + errors.add(:notification_email, _("must be an email you have verified")) unless user.verified_emails.include?(notification_email) end end diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 16339c98b90..4ae3bc01a01 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -42,7 +42,7 @@ class ProjectTeam end def add_users(users, access_level, current_user: nil, expires_at: nil) - Members::Projects::CreatorService.add_users( # rubocop:todo CodeReuse/ServiceClass + Members::Projects::CreatorService.add_users( # rubocop:disable CodeReuse/ServiceClass project, users, access_level, @@ -52,7 +52,7 @@ class ProjectTeam end def add_user(user, access_level, current_user: nil, expires_at: nil) - Members::Projects::CreatorService.new(project, # rubocop:todo CodeReuse/ServiceClass + Members::Projects::CreatorService.new(project, # rubocop:disable CodeReuse/ServiceClass user, access_level, current_user: current_user, diff --git a/app/models/user.rb b/app/models/user.rb index 561b2f0fd8d..d9d1003a298 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -224,7 +224,7 @@ class User < ApplicationRecord validates :email, confirmation: true validates :notification_email, presence: true validates :notification_email, devise_email: true, if: ->(user) { user.notification_email != user.email } - validates :public_email, presence: true, uniqueness: true, devise_email: true, allow_blank: true + validates :public_email, uniqueness: true, devise_email: true, allow_blank: true validates :commit_email, devise_email: true, allow_nil: true, if: ->(user) { user.commit_email != user.email } validates :projects_limit, presence: true, @@ -235,9 +235,9 @@ class User < ApplicationRecord validate :namespace_move_dir_allowed, if: :username_changed? validate :unique_email, if: :email_changed? - validate :owns_notification_email, if: :notification_email_changed? - validate :owns_public_email, if: :public_email_changed? - validate :owns_commit_email, if: :commit_email_changed? + validate :notification_email_verified, if: :notification_email_changed? + validate :public_email_verified, if: :public_email_changed? + validate :commit_email_verified, if: :commit_email_changed? validate :signup_email_valid?, on: :create, if: ->(user) { !user.created_by_id } validate :check_username_format, if: :username_changed? @@ -928,22 +928,22 @@ class User < ApplicationRecord end end - def owns_notification_email + def notification_email_verified return if new_record? || temp_oauth_email? - errors.add(:notification_email, _("is not an email you own")) unless verified_emails.include?(notification_email) + errors.add(:notification_email, _("must be an email you have verified")) unless verified_emails.include?(notification_email) end - def owns_public_email + def public_email_verified return if public_email.blank? - errors.add(:public_email, _("is not an email you own")) unless verified_emails.include?(public_email) + errors.add(:public_email, _("must be an email you have verified")) unless verified_emails.include?(public_email) end - def owns_commit_email + def commit_email_verified return if read_attribute(:commit_email).blank? - errors.add(:commit_email, _("is not an email you own")) unless verified_emails.include?(commit_email) + errors.add(:commit_email, _("must be an email you have verified")) unless verified_emails.include?(commit_email) end # Define commit_email-related attribute methods explicitly instead of relying diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml index 5a1e263141d..b89aa9d402e 100644 --- a/app/views/projects/mirrors/_mirror_repos.html.haml +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -70,10 +70,10 @@ = render 'projects/mirrors/disabled_mirror_badge' - if mirror.last_error.present? .badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true', qa_selector: 'mirror_error_badge' }, title: html_escape(mirror.last_error.try(:strip)) }= _('Error') - %td + %td.gl-display-flex - if mirror_settings_enabled - .btn-group.mirror-actions-group.float-right{ role: 'group' } + %button.js-delete-mirror.qa-delete-mirror.rspec-delete-mirror.btn.btn-icon.gl-button.btn-danger.gl-mr-3{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= sprite_icon('remove') + .btn-group.mirror-actions-group{ role: 'group' } - if mirror.ssh_key_auth? = clipboard_button(text: mirror.ssh_public_key, class: 'gl-button btn btn-default', title: _('Copy SSH public key'), qa_selector: 'copy_public_key_button') = render 'shared/remote_mirror_update_button', remote_mirror: mirror - %button.js-delete-mirror.qa-delete-mirror.rspec-delete-mirror.btn.btn-icon.gl-button.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= sprite_icon('remove') diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md index 9b26e1f102c..dfd1f09e297 100644 --- a/doc/gitlab-basics/start-using-git.md +++ b/doc/gitlab-basics/start-using-git.md @@ -450,11 +450,11 @@ You can learn more about the different ways Git can undo changes in the ### Merge a branch with default branch When you are ready to add your changes to -the default branch, you `merge` the two together: +the default branch, you merge the feature branch into it: ```shell -git checkout <feature-branch> -git merge <default-branch> +git checkout <default-branch> +git merge <feature-branch> ``` In GitLab, you typically use a [merge request](../user/project/merge_requests/) to merge your changes, instead of using the command line. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index fa69bfe466b..fa81b20804a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -22179,9 +22179,6 @@ msgstr "" msgid "NetworkPolicies|Save changes" msgstr "" -msgid "NetworkPolicies|Scan Execution" -msgstr "" - msgid "NetworkPolicies|Something went wrong, failed to update policy" msgstr "" @@ -29556,6 +29553,9 @@ msgstr "" msgid "SecurityConfiguration|Vulnerability details and statistics in the merge request" msgstr "" +msgid "SecurityOrchestration|All policies" +msgstr "" + msgid "SecurityOrchestration|An error occurred assigning your security policy project" msgstr "" @@ -29568,6 +29568,9 @@ msgstr "" msgid "SecurityOrchestration|Enforce security for this project. %{linkStart}More information.%{linkEnd}" msgstr "" +msgid "SecurityOrchestration|Network" +msgstr "" + msgid "SecurityOrchestration|New policy" msgstr "" @@ -29580,6 +29583,12 @@ msgstr "" msgid "SecurityOrchestration|Policy editor" msgstr "" +msgid "SecurityOrchestration|Scan Execution" +msgstr "" + +msgid "SecurityOrchestration|Scan execution" +msgstr "" + msgid "SecurityOrchestration|Security policy project was linked successfully" msgstr "" @@ -29598,9 +29607,6 @@ msgstr "" msgid "SecurityPolicies|+%{count} more" msgstr "" -msgid "SecurityPolicies|All policies" -msgstr "" - msgid "SecurityPolicies|Description" msgstr "" @@ -29613,9 +29619,6 @@ msgstr "" msgid "SecurityPolicies|Latest scan" msgstr "" -msgid "SecurityPolicies|Network" -msgstr "" - msgid "SecurityPolicies|Policy type" msgstr "" @@ -39476,9 +39479,6 @@ msgstr "" msgid "is not allowed. We do not currently support project-level iterations" msgstr "" -msgid "is not an email you own" -msgstr "" - msgid "is not from an allowed domain." msgstr "" @@ -39917,6 +39917,9 @@ msgstr "" msgid "must be after start" msgstr "" +msgid "must be an email you have verified" +msgstr "" + msgid "must be greater than start date" msgstr "" diff --git a/package.json b/package.json index f941cfcee1b..169fb209a4c 100644 --- a/package.json +++ b/package.json @@ -57,9 +57,9 @@ "@babel/preset-env": "^7.10.1", "@gitlab/at.js": "1.5.7", "@gitlab/favicon-overlay": "2.0.0", - "@gitlab/svgs": "1.210.0", + "@gitlab/svgs": "1.211.0", "@gitlab/tributejs": "1.0.0", - "@gitlab/ui": "32.2.0", + "@gitlab/ui": "32.2.1", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "6.1.3-2", "@rails/ujs": "6.1.3-2", diff --git a/spec/frontend/content_editor/extensions/inline_diff_spec.js b/spec/frontend/content_editor/extensions/inline_diff_spec.js new file mode 100644 index 00000000000..63cdf665e7f --- /dev/null +++ b/spec/frontend/content_editor/extensions/inline_diff_spec.js @@ -0,0 +1,27 @@ +import { inputRegexAddition, inputRegexDeletion } from '~/content_editor/extensions/inline_diff'; + +describe('content_editor/extensions/inline_diff', () => { + describe.each` + inputRegex | description | input | matches + ${inputRegexAddition} | ${'inputRegexAddition'} | ${'hello{+world+}'} | ${true} + ${inputRegexAddition} | ${'inputRegexAddition'} | ${'hello{+ world +}'} | ${true} + ${inputRegexAddition} | ${'inputRegexAddition'} | ${'hello {+ world+}'} | ${true} + ${inputRegexAddition} | ${'inputRegexAddition'} | ${'{+hello world +}'} | ${true} + ${inputRegexAddition} | ${'inputRegexAddition'} | ${'{+hello with \nnewline+}'} | ${false} + ${inputRegexAddition} | ${'inputRegexAddition'} | ${'{+open only'} | ${false} + ${inputRegexAddition} | ${'inputRegexAddition'} | ${'close only+}'} | ${false} + ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'hello{-world-}'} | ${true} + ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'hello{- world -}'} | ${true} + ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'hello {- world-}'} | ${true} + ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'{-hello world -}'} | ${true} + ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'{+hello with \nnewline+}'} | ${false} + ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'{-open only'} | ${false} + ${inputRegexDeletion} | ${'inputRegexDeletion'} | ${'close only-}'} | ${false} + `('$description', ({ inputRegex, input, matches }) => { + it(`${matches ? 'matches' : 'does not match'}: "${input}"`, () => { + const match = new RegExp(inputRegex).test(input); + + expect(match).toBe(matches); + }); + }); +}); diff --git a/spec/frontend/fixtures/api_markdown.yml b/spec/frontend/fixtures/api_markdown.yml index fd496ab6327..e4ca38804a7 100644 --- a/spec/frontend/fixtures/api_markdown.yml +++ b/spec/frontend/fixtures/api_markdown.yml @@ -8,6 +8,10 @@ markdown: '_emphasized text_' - name: inline_code markdown: '`code`' +- name: inline_diff + markdown: |- + * {-deleted-} + * {+added+} - name: subscript markdown: H<sub>2</sub>O - name: superscript diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js index 331c9c2c14d..09eac636cae 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js @@ -61,40 +61,16 @@ describe('BranchToken', () => { wrapper.destroy(); }); - describe('computed', () => { - beforeEach(async () => { - wrapper = createComponent({ value: { data: mockBranches[0].name } }); - - wrapper.setData({ - branches: mockBranches, - }); - - await wrapper.vm.$nextTick(); - }); - - describe('currentValue', () => { - it('returns lowercase string for `value.data`', () => { - expect(wrapper.vm.currentValue).toBe('main'); - }); - }); - - describe('activeBranch', () => { - it('returns object for currently present `value.data`', () => { - expect(wrapper.vm.activeBranch).toEqual(mockBranches[0]); - }); - }); - }); - describe('methods', () => { beforeEach(() => { wrapper = createComponent(); }); - describe('fetchBranchBySearchTerm', () => { + describe('fetchBranches', () => { it('calls `config.fetchBranches` with provided searchTerm param', () => { jest.spyOn(wrapper.vm.config, 'fetchBranches'); - wrapper.vm.fetchBranchBySearchTerm('foo'); + wrapper.vm.fetchBranches('foo'); expect(wrapper.vm.config.fetchBranches).toHaveBeenCalledWith('foo'); }); @@ -102,7 +78,7 @@ describe('BranchToken', () => { it('sets response to `branches` when request is succesful', () => { jest.spyOn(wrapper.vm.config, 'fetchBranches').mockResolvedValue({ data: mockBranches }); - wrapper.vm.fetchBranchBySearchTerm('foo'); + wrapper.vm.fetchBranches('foo'); return waitForPromises().then(() => { expect(wrapper.vm.branches).toEqual(mockBranches); @@ -112,7 +88,7 @@ describe('BranchToken', () => { it('calls `createFlash` with flash error message when request fails', () => { jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({}); - wrapper.vm.fetchBranchBySearchTerm('foo'); + wrapper.vm.fetchBranches('foo'); return waitForPromises().then(() => { expect(createFlash).toHaveBeenCalledWith({ @@ -124,7 +100,7 @@ describe('BranchToken', () => { it('sets `loading` to false when request completes', () => { jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({}); - wrapper.vm.fetchBranchBySearchTerm('foo'); + wrapper.vm.fetchBranches('foo'); return waitForPromises().then(() => { expect(wrapper.vm.loading).toBe(false); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js index 778a214f97e..c2d61fd9f05 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js @@ -67,40 +67,16 @@ describe('EmojiToken', () => { wrapper.destroy(); }); - describe('computed', () => { - beforeEach(async () => { - wrapper = createComponent({ value: { data: mockEmojis[0].name } }); - - wrapper.setData({ - emojis: mockEmojis, - }); - - await wrapper.vm.$nextTick(); - }); - - describe('currentValue', () => { - it('returns lowercase string for `value.data`', () => { - expect(wrapper.vm.currentValue).toBe(mockEmojis[0].name); - }); - }); - - describe('activeEmoji', () => { - it('returns object for currently present `value.data`', () => { - expect(wrapper.vm.activeEmoji).toEqual(mockEmojis[0]); - }); - }); - }); - describe('methods', () => { beforeEach(() => { wrapper = createComponent(); }); - describe('fetchEmojiBySearchTerm', () => { + describe('fetchEmojis', () => { it('calls `config.fetchEmojis` with provided searchTerm param', () => { jest.spyOn(wrapper.vm.config, 'fetchEmojis'); - wrapper.vm.fetchEmojiBySearchTerm('foo'); + wrapper.vm.fetchEmojis('foo'); expect(wrapper.vm.config.fetchEmojis).toHaveBeenCalledWith('foo'); }); @@ -108,7 +84,7 @@ describe('EmojiToken', () => { it('sets response to `emojis` when request is successful', () => { jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockResolvedValue(mockEmojis); - wrapper.vm.fetchEmojiBySearchTerm('foo'); + wrapper.vm.fetchEmojis('foo'); return waitForPromises().then(() => { expect(wrapper.vm.emojis).toEqual(mockEmojis); @@ -118,7 +94,7 @@ describe('EmojiToken', () => { it('calls `createFlash` with flash error message when request fails', () => { jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockRejectedValue({}); - wrapper.vm.fetchEmojiBySearchTerm('foo'); + wrapper.vm.fetchEmojis('foo'); return waitForPromises().then(() => { expect(createFlash).toHaveBeenCalledWith({ @@ -130,7 +106,7 @@ describe('EmojiToken', () => { it('sets `loading` to false when request completes', () => { jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockRejectedValue({}); - wrapper.vm.fetchEmojiBySearchTerm('foo'); + wrapper.vm.fetchEmojis('foo'); return waitForPromises().then(() => { expect(wrapper.vm.loading).toBe(false); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js index bd654c5a9cb..a609aaa1c4e 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js @@ -1,5 +1,6 @@ import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue'; import { mockIterationToken } from '../mock_data'; @@ -13,6 +14,7 @@ describe('IterationToken', () => { const createComponent = ({ config = mockIterationToken, value = { data: '' } } = {}) => mount(IterationToken, { propsData: { + active: false, config, value, }, @@ -69,7 +71,7 @@ describe('IterationToken', () => { config: { ...mockIterationToken, fetchIterations: fetchIterationsSpy }, }); - await wrapper.vm.$nextTick(); + await waitForPromises(); expect(createFlash).toHaveBeenCalledWith({ message: 'There was a problem fetching iterations.', diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js index 74ceb03bb96..529844817d3 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js @@ -14,12 +14,7 @@ import { sortMilestonesByDueDate } from '~/milestones/milestone_utils'; import { DEFAULT_MILESTONES } from '~/vue_shared/components/filtered_search_bar/constants'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; -import { - mockMilestoneToken, - mockMilestones, - mockRegularMilestone, - mockEscapedMilestone, -} from '../mock_data'; +import { mockMilestoneToken, mockMilestones, mockRegularMilestone } from '../mock_data'; jest.mock('~/flash'); jest.mock('~/milestones/milestone_utils'); @@ -70,37 +65,12 @@ describe('MilestoneToken', () => { wrapper.destroy(); }); - describe('computed', () => { - beforeEach(async () => { - // Milestone title with spaces is always enclosed in quotations by component. - wrapper = createComponent({ value: { data: `"${mockEscapedMilestone.title}"` } }); - - wrapper.setData({ - milestones: mockMilestones, - }); - - await wrapper.vm.$nextTick(); - }); - - describe('currentValue', () => { - it('returns lowercase string for `value.data`', () => { - expect(wrapper.vm.currentValue).toBe('"5.0 rc1"'); - }); - }); - - describe('activeMilestone', () => { - it('returns object for currently present `value.data`', () => { - expect(wrapper.vm.activeMilestone).toEqual(mockEscapedMilestone); - }); - }); - }); - describe('methods', () => { - describe('fetchMilestoneBySearchTerm', () => { + describe('fetchMilestones', () => { it('calls `config.fetchMilestones` with provided searchTerm param', () => { jest.spyOn(wrapper.vm.config, 'fetchMilestones'); - wrapper.vm.fetchMilestoneBySearchTerm('foo'); + wrapper.vm.fetchMilestones('foo'); expect(wrapper.vm.config.fetchMilestones).toHaveBeenCalledWith('foo'); }); @@ -110,7 +80,7 @@ describe('MilestoneToken', () => { data: mockMilestones, }); - wrapper.vm.fetchMilestoneBySearchTerm(); + wrapper.vm.fetchMilestones(); return waitForPromises().then(() => { expect(wrapper.vm.milestones).toEqual(mockMilestones); @@ -121,7 +91,7 @@ describe('MilestoneToken', () => { it('calls `createFlash` with flash error message when request fails', () => { jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({}); - wrapper.vm.fetchMilestoneBySearchTerm('foo'); + wrapper.vm.fetchMilestones('foo'); return waitForPromises().then(() => { expect(createFlash).toHaveBeenCalledWith({ @@ -133,7 +103,7 @@ describe('MilestoneToken', () => { it('sets `loading` to false when request completes', () => { jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({}); - wrapper.vm.fetchMilestoneBySearchTerm('foo'); + wrapper.vm.fetchMilestones('foo'); return waitForPromises().then(() => { expect(wrapper.vm.loading).toBe(false); diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js index 9a72be636cd..e788c742736 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js @@ -12,6 +12,7 @@ describe('WeightToken', () => { const createComponent = ({ config = mockWeightToken, value = { data: '' } } = {}) => mount(WeightToken, { propsData: { + active: false, config, value, }, diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ef229489241..48e56051fc5 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -704,7 +704,7 @@ RSpec.describe User do user.notification_email = email.email expect(user).to be_invalid - expect(user.errors[:notification_email]).to include('is not an email you own') + expect(user.errors[:notification_email]).to include(_('must be an email you have verified')) end end @@ -723,7 +723,7 @@ RSpec.describe User do user.public_email = email.email expect(user).to be_invalid - expect(user.errors[:public_email]).to include('is not an email you own') + expect(user.errors[:public_email]).to include(_('must be an email you have verified')) end end diff --git a/yarn.lock b/yarn.lock index 940b76ae245..0449fc6124a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -898,25 +898,25 @@ stylelint-declaration-strict-value "1.7.7" stylelint-scss "3.18.0" -"@gitlab/svgs@1.210.0": - version "1.210.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.210.0.tgz#f2d36a2073eb5059fe48a08130145d740c220efa" - integrity sha512-IVALHZKM4QB0djEWJbwgWlpYFD4UF9sqml2SLS5vS/p/FVDeMe7fz7hYOH42xZaZn2iWE6XE9DOVyd9IDNWzPg== +"@gitlab/svgs@1.211.0": + version "1.211.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.211.0.tgz#0351fa4cc008c4830f366aede535df0a8e63dda6" + integrity sha512-fkHJfmKiy7lDwLFQ6z64sbGL+/hDDLzcMTj8O+VBC1xnlBVAIxe2eIs2DZLJcJwgLWncf4Uovp8+CeEfCY12sw== "@gitlab/tributejs@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8" integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw== -"@gitlab/ui@32.2.0": - version "32.2.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-32.2.0.tgz#cfff74221c62292bfe329274381352f2335ca493" - integrity sha512-ASezPNr97rGmIsSbUWkmY9kDBtat/FSnErQYRx/xGYOf9KkCHzVvYV6s8536abAux7LyIIMv5iwtg2U39wEv9A== +"@gitlab/ui@32.2.1": + version "32.2.1" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-32.2.1.tgz#e019124af981e8ceffd39f30cf08d315c53d4ac8" + integrity sha512-19qe30gHtBG7g7wJy36bvS+ji1puJwVzAwqJOFXAh3axSa0Getyjyl9t5gmfwmZ2HehFTWLlThXppclnJVCEGA== dependencies: "@babel/standalone" "^7.0.0" bootstrap-vue "2.18.1" copy-to-clipboard "^3.0.8" - dompurify "^2.3.0" + dompurify "^2.3.1" echarts "^4.9.0" highlight.js "^10.6.0" js-beautify "^1.8.8" @@ -4568,7 +4568,7 @@ domhandler@^4.0.0, domhandler@^4.2.0: dependencies: domelementtype "^2.2.0" -dompurify@^2.3.0, dompurify@^2.3.1: +dompurify@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.1.tgz#a47059ca21fd1212d3c8f71fdea6943b8bfbdf6a" integrity sha512-xGWt+NHAQS+4tpgbOAI08yxW0Pr256Gu/FNE2frZVTbgrBUn8M7tz7/ktS/LZ2MHeGqz6topj0/xY+y8R5FBFw== |