Skip to content

Commit e1189e4

Browse files
author
GitLab Bot
committed
Add latest changes from gitlab-org/gitlab@master
1 parent 8ce82c1 commit e1189e4

File tree

144 files changed

+2131
-555
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

144 files changed

+2131
-555
lines changed

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
/public/
88
/tmp/
99
/vendor/
10+
/sitespeed-result/

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
/public/
55
/vendor/
66
/tmp/
7+
/sitespeed-result/
78

89
# ignore stylesheets for now as this clashes with our linter
910
*.css

GITALY_SERVER_VERSION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
d4ea957f6131538cd78e490a585ea3a455251064
1+
40511f7a14ded77c826809d054d740a66e1c106f

app/assets/javascripts/boards/components/board_column.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export default {
4141
watch: {
4242
filterParams: {
4343
handler() {
44-
if (this.list.id) {
44+
if (this.list.id && !this.list.collapsed) {
4545
this.fetchItemsForList({ listId: this.list.id });
4646
}
4747
},

app/assets/javascripts/boards/stores/actions.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ export default {
240240
},
241241

242242
updateList: (
243-
{ commit, state: { issuableType } },
243+
{ commit, state: { issuableType, boardItemsByListId = {} }, dispatch },
244244
{ listId, position, collapsed, backupList },
245245
) => {
246246
gqlClient
@@ -255,6 +255,12 @@ export default {
255255
.then(({ data }) => {
256256
if (data?.updateBoardList?.errors.length) {
257257
commit(types.UPDATE_LIST_FAILURE, backupList);
258+
return;
259+
}
260+
261+
// Only fetch when board items havent been fetched on a collapsed list
262+
if (!boardItemsByListId[listId]) {
263+
dispatch('fetchItemsForList', { listId });
258264
}
259265
})
260266
.catch(() => {

app/assets/javascripts/content_editor/extensions/image.js

+116-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,65 @@
11
import { Image } from '@tiptap/extension-image';
2-
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
2+
import { VueNodeViewRenderer } from '@tiptap/vue-2';
3+
import { Plugin, PluginKey } from 'prosemirror-state';
4+
import { __ } from '~/locale';
5+
import ImageWrapper from '../components/wrappers/image.vue';
6+
import { uploadFile } from '../services/upload_file';
7+
import { getImageAlt, readFileAsDataURL } from '../services/utils';
8+
9+
export const acceptedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/jpg'];
10+
11+
const resolveImageEl = (element) =>
12+
element.nodeName === 'IMG' ? element : element.querySelector('img');
13+
14+
const startFileUpload = async ({ editor, file, uploadsPath, renderMarkdown }) => {
15+
const encodedSrc = await readFileAsDataURL(file);
16+
const { view } = editor;
17+
18+
editor.commands.setImage({ uploading: true, src: encodedSrc });
19+
20+
const { state } = view;
21+
const position = state.selection.from - 1;
22+
const { tr } = state;
23+
24+
try {
25+
const { src, canonicalSrc } = await uploadFile({ file, uploadsPath, renderMarkdown });
26+
27+
view.dispatch(
28+
tr.setNodeMarkup(position, undefined, {
29+
uploading: false,
30+
src: encodedSrc,
31+
alt: getImageAlt(src),
32+
canonicalSrc,
33+
}),
34+
);
35+
} catch (e) {
36+
editor.commands.deleteRange({ from: position, to: position + 1 });
37+
editor.emit('error', __('An error occurred while uploading the image. Please try again.'));
38+
}
39+
};
40+
41+
const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown }) => {
42+
if (acceptedMimes.includes(file?.type)) {
43+
startFileUpload({ editor, file, uploadsPath, renderMarkdown });
44+
45+
return true;
46+
}
47+
48+
return false;
49+
};
350

451
const ExtendedImage = Image.extend({
52+
defaultOptions: {
53+
...Image.options,
54+
uploadsPath: null,
55+
renderMarkdown: null,
56+
},
557
addAttributes() {
658
return {
759
...this.parent?.(),
60+
uploading: {
61+
default: false,
62+
},
863
src: {
964
default: null,
1065
/*
@@ -14,17 +69,25 @@ const ExtendedImage = Image.extend({
1469
* attribute.
1570
*/
1671
parseHTML: (element) => {
17-
const img = element.querySelector('img');
72+
const img = resolveImageEl(element);
1873

1974
return {
2075
src: img.dataset.src || img.getAttribute('src'),
2176
};
2277
},
2378
},
79+
canonicalSrc: {
80+
default: null,
81+
parseHTML: (element) => {
82+
return {
83+
canonicalSrc: element.dataset.canonicalSrc,
84+
};
85+
},
86+
},
2487
alt: {
2588
default: null,
2689
parseHTML: (element) => {
27-
const img = element.querySelector('img');
90+
const img = resolveImageEl(element);
2891

2992
return {
3093
alt: img.getAttribute('alt'),
@@ -44,9 +107,58 @@ const ExtendedImage = Image.extend({
44107
},
45108
];
46109
},
110+
addCommands() {
111+
return {
112+
...this.parent(),
113+
uploadImage: ({ file }) => () => {
114+
const { uploadsPath, renderMarkdown } = this.options;
115+
116+
handleFileEvent({ file, uploadsPath, renderMarkdown, editor: this.editor });
117+
},
118+
};
119+
},
120+
addProseMirrorPlugins() {
121+
const { editor } = this;
122+
123+
return [
124+
new Plugin({
125+
key: new PluginKey('handleDropAndPasteImages'),
126+
props: {
127+
handlePaste: (_, event) => {
128+
const { uploadsPath, renderMarkdown } = this.options;
129+
130+
return handleFileEvent({
131+
editor,
132+
file: event.clipboardData.files[0],
133+
uploadsPath,
134+
renderMarkdown,
135+
});
136+
},
137+
handleDrop: (_, event) => {
138+
const { uploadsPath, renderMarkdown } = this.options;
139+
140+
return handleFileEvent({
141+
editor,
142+
file: event.dataTransfer.files[0],
143+
uploadsPath,
144+
renderMarkdown,
145+
});
146+
},
147+
},
148+
}),
149+
];
150+
},
151+
addNodeView() {
152+
return VueNodeViewRenderer(ImageWrapper);
153+
},
47154
});
48155

49-
const serializer = defaultMarkdownSerializer.nodes.image;
156+
const serializer = (state, node) => {
157+
const { alt, canonicalSrc, src, title } = node.attrs;
158+
const quotedTitle = title ? ` ${state.quote(title)}` : '';
159+
160+
state.write(`![${state.esc(alt || '')}](${state.esc(canonicalSrc || src)}${quotedTitle})`);
161+
};
50162

51163
export const configure = ({ renderMarkdown, uploadsPath }) => {
52164
return {

app/assets/javascripts/content_editor/services/utils.js

+12
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,15 @@ export const hasSelection = (tiptapEditor) => {
33

44
return from < to;
55
};
6+
7+
export const getImageAlt = (src) => {
8+
return src.replace(/^.*\/|\..*$/g, '').replace(/\W+/g, ' ');
9+
};
10+
11+
export const readFileAsDataURL = (file) => {
12+
return new Promise((resolve) => {
13+
const reader = new FileReader();
14+
reader.addEventListener('load', (e) => resolve(e.target.result), { once: true });
15+
reader.readAsDataURL(file);
16+
});
17+
};

app/assets/javascripts/design_management/components/design_notes/design_note.vue

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script>
22
import { GlTooltipDirective, GlIcon, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
33
import { ApolloMutation } from 'vue-apollo';
4+
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
45
import { __ } from '~/locale';
56
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
67
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -48,6 +49,9 @@ export default {
4849
author() {
4950
return this.note.author;
5051
},
52+
authorId() {
53+
return getIdFromGraphQLId(this.author.id);
54+
},
5155
noteAnchorId() {
5256
return findNoteId(this.note.id);
5357
},
@@ -94,7 +98,7 @@ export default {
9498
v-once
9599
:href="author.webUrl"
96100
class="js-user-link"
97-
:data-user-id="author.id"
101+
:data-user-id="authorId"
98102
:data-username="author.username"
99103
>
100104
<span class="note-header-author-name gl-font-weight-bold">{{ author.name }}</span>

app/assets/javascripts/lib/graphql.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export default (resolvers = {}, config = {}) => {
9797
*/
9898

9999
const fetchIntervention = (url, options) => {
100-
return fetch(stripWhitespaceFromQuery(url, path), options);
100+
return fetch(stripWhitespaceFromQuery(url, uri), options);
101101
};
102102

103103
const requestLink = ApolloLink.split(

app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
<script>
22
import { GlFilteredSearchToken } from '@gitlab/ui';
33
import { mapState } from 'vuex';
4-
// eslint-disable-next-line import/no-deprecated
5-
import { getParameterByName, setUrlParams, urlParamsToObject } from '~/lib/utils/url_utility';
4+
import { getParameterByName, setUrlParams, queryToObject } from '~/lib/utils/url_utility';
65
import { s__ } from '~/locale';
76
import {
87
SEARCH_TOKEN_TYPE,
@@ -68,8 +67,7 @@ export default {
6867
},
6968
},
7069
created() {
71-
// eslint-disable-next-line import/no-deprecated
72-
const query = urlParamsToObject(window.location.search);
70+
const query = queryToObject(window.location.search);
7371
7472
const tokens = this.tokens
7573
.filter((token) => query[token.type])

app/assets/javascripts/notes/components/notes_app.vue

+6-1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export default {
6666
data() {
6767
return {
6868
currentFilter: null,
69+
renderSkeleton: !this.shouldShow,
6970
};
7071
},
7172
computed: {
@@ -93,7 +94,7 @@ export default {
9394
return this.noteableData.noteableType;
9495
},
9596
allDiscussions() {
96-
if (this.isLoading) {
97+
if (this.renderSkeleton || this.isLoading) {
9798
const prerenderedNotesCount = parseInt(this.notesData.prerenderedNotesCount, 10) || 0;
9899
99100
return new Array(prerenderedNotesCount).fill({
@@ -122,6 +123,10 @@ export default {
122123
if (!this.isNotesFetched) {
123124
this.fetchNotes();
124125
}
126+
127+
setTimeout(() => {
128+
this.renderSkeleton = !this.shouldShow;
129+
});
125130
},
126131
discussionTabCounterText(val) {
127132
if (this.discussionsCount) {

app/assets/javascripts/projects/project_new.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ const deriveProjectPathFromUrl = ($projectImportUrl) => {
7474
const bindEvents = () => {
7575
const $newProjectForm = $('#new_project');
7676
const $projectImportUrl = $('#project_import_url');
77+
const $projectImportUrlWarning = $('.js-import-url-warning');
7778
const $projectPath = $('.tab-pane.active #project_path');
7879
const $useTemplateBtn = $('.template-button > input');
7980
const $projectFieldsForm = $('.project-fields-form');
@@ -134,7 +135,25 @@ const bindEvents = () => {
134135
$projectPath.val($projectPath.val().trim());
135136
});
136137

137-
$projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl));
138+
function updateUrlPathWarningVisibility() {
139+
const url = $projectImportUrl.val();
140+
const URL_PATTERN = /(?:git|https?):\/\/.*\/.*\.git$/;
141+
const isUrlValid = URL_PATTERN.test(url);
142+
$projectImportUrlWarning.toggleClass('hide', isUrlValid);
143+
}
144+
145+
let isProjectImportUrlDirty = false;
146+
$projectImportUrl.on('blur', () => {
147+
isProjectImportUrlDirty = true;
148+
updateUrlPathWarningVisibility();
149+
});
150+
$projectImportUrl.on('keyup', () => {
151+
deriveProjectPathFromUrl($projectImportUrl);
152+
// defer error message till first input blur
153+
if (isProjectImportUrlDirty) {
154+
updateUrlPathWarningVisibility();
155+
}
156+
});
138157

139158
$('.js-import-git-toggle-button').on('click', () => {
140159
const $projectMirror = $('#project_mirror');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<script>
2+
import { GlIcon, GlLink } from '@gitlab/ui';
3+
import { numberToHumanSize } from '~/lib/utils/number_utils';
4+
import { sprintf, __ } from '~/locale';
5+
6+
export default {
7+
components: {
8+
GlIcon,
9+
GlLink,
10+
},
11+
props: {
12+
fileName: {
13+
type: String,
14+
required: true,
15+
},
16+
filePath: {
17+
type: String,
18+
required: true,
19+
},
20+
fileSize: {
21+
type: Number,
22+
required: false,
23+
default: 0,
24+
},
25+
},
26+
computed: {
27+
downloadFileSize() {
28+
return numberToHumanSize(this.fileSize);
29+
},
30+
downloadText() {
31+
if (this.fileSize > 0) {
32+
return sprintf(__('Download (%{fileSizeReadable})'), {
33+
fileSizeReadable: this.downloadFileSize,
34+
});
35+
}
36+
return __('Download');
37+
},
38+
},
39+
};
40+
</script>
41+
42+
<template>
43+
<div class="gl-text-center gl-py-13 gl-bg-gray-50">
44+
<gl-link :href="filePath" rel="nofollow" :download="fileName" target="_blank">
45+
<div>
46+
<gl-icon :size="16" name="download" class="gl-text-gray-900" />
47+
</div>
48+
<h4>{{ downloadText }}</h4>
49+
</gl-link>
50+
</div>
51+
</template>

0 commit comments

Comments
 (0)