Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/boards/mount_multiple_boards_switcher.js7
-rw-r--r--app/assets/javascripts/content_editor/extensions/inline_diff.js50
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js2
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue100
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue109
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue99
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue97
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue47
-rw-r--r--app/models/group.rb4
-rw-r--r--app/models/members/project_member.rb2
-rw-r--r--app/models/notification_setting.rb6
-rw-r--r--app/models/project_team.rb4
-rw-r--r--app/models/user.rb20
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml6
-rw-r--r--doc/gitlab-basics/start-using-git.md6
-rw-r--r--locale/gitlab.pot27
-rw-r--r--package.json4
-rw-r--r--spec/frontend/content_editor/extensions/inline_diff_spec.js27
-rw-r--r--spec/frontend/fixtures/api_markdown.yml4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js42
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js1
-rw-r--r--spec/models/user_spec.rb4
-rw-r--r--yarn.lock20
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==