Commit e1189e4c authored by 🤖 GitLab Bot 🤖's avatar 🤖 GitLab Bot 🤖
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 8ce82c1e
......@@ -7,3 +7,4 @@
/public/
/tmp/
/vendor/
/sitespeed-result/
......@@ -4,6 +4,7 @@
/public/
/vendor/
/tmp/
/sitespeed-result/
# ignore stylesheets for now as this clashes with our linter
*.css
......
d4ea957f6131538cd78e490a585ea3a455251064
40511f7a14ded77c826809d054d740a66e1c106f
......@@ -41,7 +41,7 @@ export default {
watch: {
filterParams: {
handler() {
if (this.list.id) {
if (this.list.id && !this.list.collapsed) {
this.fetchItemsForList({ listId: this.list.id });
}
},
......
......@@ -240,7 +240,7 @@ export default {
},
updateList: (
{ commit, state: { issuableType } },
{ commit, state: { issuableType, boardItemsByListId = {} }, dispatch },
{ listId, position, collapsed, backupList },
) => {
gqlClient
......@@ -255,6 +255,12 @@ export default {
.then(({ data }) => {
if (data?.updateBoardList?.errors.length) {
commit(types.UPDATE_LIST_FAILURE, backupList);
return;
}
// Only fetch when board items havent been fetched on a collapsed list
if (!boardItemsByListId[listId]) {
dispatch('fetchItemsForList', { listId });
}
})
.catch(() => {
......
import { Image } from '@tiptap/extension-image';
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
import { VueNodeViewRenderer } from '@tiptap/vue-2';
import { Plugin, PluginKey } from 'prosemirror-state';
import { __ } from '~/locale';
import ImageWrapper from '../components/wrappers/image.vue';
import { uploadFile } from '../services/upload_file';
import { getImageAlt, readFileAsDataURL } from '../services/utils';
export const acceptedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/jpg'];
const resolveImageEl = (element) =>
element.nodeName === 'IMG' ? element : element.querySelector('img');
const startFileUpload = async ({ editor, file, uploadsPath, renderMarkdown }) => {
const encodedSrc = await readFileAsDataURL(file);
const { view } = editor;
editor.commands.setImage({ uploading: true, src: encodedSrc });
const { state } = view;
const position = state.selection.from - 1;
const { tr } = state;
try {
const { src, canonicalSrc } = await uploadFile({ file, uploadsPath, renderMarkdown });
view.dispatch(
tr.setNodeMarkup(position, undefined, {
uploading: false,
src: encodedSrc,
alt: getImageAlt(src),
canonicalSrc,
}),
);
} catch (e) {
editor.commands.deleteRange({ from: position, to: position + 1 });
editor.emit('error', __('An error occurred while uploading the image. Please try again.'));
}
};
const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown }) => {
if (acceptedMimes.includes(file?.type)) {
startFileUpload({ editor, file, uploadsPath, renderMarkdown });
return true;
}
return false;
};
const ExtendedImage = Image.extend({
defaultOptions: {
...Image.options,
uploadsPath: null,
renderMarkdown: null,
},
addAttributes() {
return {
...this.parent?.(),
uploading: {
default: false,
},
src: {
default: null,
/*
......@@ -14,17 +69,25 @@ const ExtendedImage = Image.extend({
* attribute.
*/
parseHTML: (element) => {
const img = element.querySelector('img');
const img = resolveImageEl(element);
return {
src: img.dataset.src || img.getAttribute('src'),
};
},
},
canonicalSrc: {
default: null,
parseHTML: (element) => {
return {
canonicalSrc: element.dataset.canonicalSrc,
};
},
},
alt: {
default: null,
parseHTML: (element) => {
const img = element.querySelector('img');
const img = resolveImageEl(element);
return {
alt: img.getAttribute('alt'),
......@@ -44,9 +107,58 @@ const ExtendedImage = Image.extend({
},
];
},
addCommands() {
return {
...this.parent(),
uploadImage: ({ file }) => () => {
const { uploadsPath, renderMarkdown } = this.options;
handleFileEvent({ file, uploadsPath, renderMarkdown, editor: this.editor });
},
};
},
addProseMirrorPlugins() {
const { editor } = this;
return [
new Plugin({
key: new PluginKey('handleDropAndPasteImages'),
props: {
handlePaste: (_, event) => {
const { uploadsPath, renderMarkdown } = this.options;
return handleFileEvent({
editor,
file: event.clipboardData.files[0],
uploadsPath,
renderMarkdown,
});
},
handleDrop: (_, event) => {
const { uploadsPath, renderMarkdown } = this.options;
return handleFileEvent({
editor,
file: event.dataTransfer.files[0],
uploadsPath,
renderMarkdown,
});
},
},
}),
];
},
addNodeView() {
return VueNodeViewRenderer(ImageWrapper);
},
});
const serializer = defaultMarkdownSerializer.nodes.image;
const serializer = (state, node) => {
const { alt, canonicalSrc, src, title } = node.attrs;
const quotedTitle = title ? ` ${state.quote(title)}` : '';
state.write(`![${state.esc(alt || '')}](${state.esc(canonicalSrc || src)}${quotedTitle})`);
};
export const configure = ({ renderMarkdown, uploadsPath }) => {
return {
......
......@@ -3,3 +3,15 @@ export const hasSelection = (tiptapEditor) => {
return from < to;
};
export const getImageAlt = (src) => {
return src.replace(/^.*\/|\..*$/g, '').replace(/\W+/g, ' ');
};
export const readFileAsDataURL = (file) => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.addEventListener('load', (e) => resolve(e.target.result), { once: true });
reader.readAsDataURL(file);
});
};
<script>
import { GlTooltipDirective, GlIcon, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
import { ApolloMutation } from 'vue-apollo';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
......@@ -48,6 +49,9 @@ export default {
author() {
return this.note.author;
},
authorId() {
return getIdFromGraphQLId(this.author.id);
},
noteAnchorId() {
return findNoteId(this.note.id);
},
......@@ -94,7 +98,7 @@ export default {
v-once
:href="author.webUrl"
class="js-user-link"
:data-user-id="author.id"
:data-user-id="authorId"
:data-username="author.username"
>
<span class="note-header-author-name gl-font-weight-bold">{{ author.name }}</span>
......
......@@ -97,7 +97,7 @@ export default (resolvers = {}, config = {}) => {
*/
const fetchIntervention = (url, options) => {
return fetch(stripWhitespaceFromQuery(url, path), options);
return fetch(stripWhitespaceFromQuery(url, uri), options);
};
const requestLink = ApolloLink.split(
......
<script>
import { GlFilteredSearchToken } from '@gitlab/ui';
import { mapState } from 'vuex';
// eslint-disable-next-line import/no-deprecated
import { getParameterByName, setUrlParams, urlParamsToObject } from '~/lib/utils/url_utility';
import { getParameterByName, setUrlParams, queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import {
SEARCH_TOKEN_TYPE,
......@@ -68,8 +67,7 @@ export default {
},
},
created() {
// eslint-disable-next-line import/no-deprecated
const query = urlParamsToObject(window.location.search);
const query = queryToObject(window.location.search);
const tokens = this.tokens
.filter((token) => query[token.type])
......
......@@ -66,6 +66,7 @@ export default {
data() {
return {
currentFilter: null,
renderSkeleton: !this.shouldShow,
};
},
computed: {
......@@ -93,7 +94,7 @@ export default {
return this.noteableData.noteableType;
},
allDiscussions() {
if (this.isLoading) {
if (this.renderSkeleton || this.isLoading) {
const prerenderedNotesCount = parseInt(this.notesData.prerenderedNotesCount, 10) || 0;
return new Array(prerenderedNotesCount).fill({
......@@ -122,6 +123,10 @@ export default {
if (!this.isNotesFetched) {
this.fetchNotes();
}
setTimeout(() => {
this.renderSkeleton = !this.shouldShow;
});
},
discussionTabCounterText(val) {
if (this.discussionsCount) {
......
......@@ -74,6 +74,7 @@ const deriveProjectPathFromUrl = ($projectImportUrl) => {
const bindEvents = () => {
const $newProjectForm = $('#new_project');
const $projectImportUrl = $('#project_import_url');
const $projectImportUrlWarning = $('.js-import-url-warning');
const $projectPath = $('.tab-pane.active #project_path');
const $useTemplateBtn = $('.template-button > input');
const $projectFieldsForm = $('.project-fields-form');
......@@ -134,7 +135,25 @@ const bindEvents = () => {
$projectPath.val($projectPath.val().trim());
});
$projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl));
function updateUrlPathWarningVisibility() {
const url = $projectImportUrl.val();
const URL_PATTERN = /(?:git|https?):\/\/.*\/.*\.git$/;
const isUrlValid = URL_PATTERN.test(url);
$projectImportUrlWarning.toggleClass('hide', isUrlValid);
}
let isProjectImportUrlDirty = false;
$projectImportUrl.on('blur', () => {
isProjectImportUrlDirty = true;
updateUrlPathWarningVisibility();
});
$projectImportUrl.on('keyup', () => {
deriveProjectPathFromUrl($projectImportUrl);
// defer error message till first input blur
if (isProjectImportUrlDirty) {
updateUrlPathWarningVisibility();
}
});
$('.js-import-git-toggle-button').on('click', () => {
const $projectMirror = $('#project_mirror');
......
<script>
import { GlIcon, GlLink } from '@gitlab/ui';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { sprintf, __ } from '~/locale';
export default {
components: {
GlIcon,
GlLink,
},
props: {
fileName: {
type: String,
required: true,
},
filePath: {
type: String,
required: true,
},
fileSize: {
type: Number,
required: false,
default: 0,
},
},
computed: {
downloadFileSize() {
return numberToHumanSize(this.fileSize);
},
downloadText() {
if (this.fileSize > 0) {
return sprintf(__('Download (%{fileSizeReadable})'), {
fileSizeReadable: this.downloadFileSize,
});
}
return __('Download');
},
},
};
</script>
<template>
<div class="gl-text-center gl-py-13 gl-bg-gray-50">
<gl-link :href="filePath" rel="nofollow" :download="fileName" target="_blank">
<div>
<gl-icon :size="16" name="download" class="gl-text-gray-900" />
</div>
<h4>{{ downloadText }}</h4>
</gl-link>
</div>
</template>
......@@ -5,8 +5,7 @@ export const loadViewer = (type) => {
case 'text':
return () => import(/* webpackChunkName: 'blob_text_viewer' */ './text_viewer.vue');
case 'download':
// TODO (follow-up): import the download viewer
return null; // () => import(/* webpackChunkName: 'blob_download_viewer' */ './download_viewer.vue');
return () => import(/* webpackChunkName: 'blob_download_viewer' */ './download_viewer.vue');
default:
return null;
}
......@@ -19,5 +18,10 @@ export const viewerProps = (type, blob) => {
fileName: blob.name,
readOnly: true,
},
download: {
fileName: blob.name,
filePath: blob.rawPath,
fileSize: blob.rawSize,
},
}[type];
};
......@@ -87,6 +87,12 @@
padding-bottom: $gl-spacing-scale-8;
}
// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1495
.gl-py-13 {
padding-top: $gl-spacing-scale-13;
padding-bottom: $gl-spacing-scale-13;
}
.gl-transition-property-stroke-opacity {
transition-property: stroke-opacity;
}
......
......@@ -325,7 +325,11 @@ def with_preloads
build.run_after_commit do
build.run_status_commit_hooks!
BuildFinishedWorker.perform_async(id)
if Feature.enabled?(:ci_build_finished_worker_namespace_changed, build.project, default_enabled: :yaml)
Ci::BuildFinishedWorker.perform_async(id)
else
::BuildFinishedWorker.perform_async(id)
end
end
end
......
......@@ -11,11 +11,48 @@ class PendingBuild < ApplicationRecord
scope :queued_before, ->(time) { where(arel_table[:created_at].lt(time)) }
def self.upsert_from_build!(build)
entry = self.new(build: build, project: build.project, protected: build.protected?)
entry = self.new(args_from_build(build))
entry.validate!
self.upsert(entry.attributes.compact, returning: %w[build_id], unique_by: :build_id)
end
def self.args_from_build(build)
args = {
build: build,
project: build.project,
protected: build.protected?
}
if Feature.enabled?(:ci_pending_builds_maintain_shared_runners_data, type: :development, default_enabled: :yaml)
args.merge(instance_runners_enabled: shareable?(build))
else
args
end
end
private_class_method :args_from_build
def self.shareable?(build)
shared_runner_enabled?(build) &&
builds_access_level?(build) &&
project_not_removed?(build)
end
private_class_method :shareable?
def self.shared_runner_enabled?(build)
build.project.shared_runners.exists?
end
private_class_method :shared_runner_enabled?
def self.project_not_removed?(build)
!build.project.pending_delete?
end
private_class_method :project_not_removed?
def self.builds_access_level?(build)
build.project.project_feature.builds_access_level.nil? || build.project.project_feature.builds_access_level > 0
end
private_class_method :builds_access_level?
end
end
......@@ -224,7 +224,7 @@ class Pipeline < ApplicationRecord
end
after_transition [:created, :waiting_for_resource, :preparing, :pending, :running] => :success do |pipeline|
# We wait a little bit to ensure that all BuildFinishedWorkers finish first
# We wait a little bit to ensure that all Ci::BuildFinishedWorkers finish first
# because this is where some metrics like code coverage is parsed and stored
# in CI build records which the daily build metrics worker relies on.
pipeline.run_after_commit { Ci::DailyBuildGroupReportResultsWorker.perform_in(10.minutes, pipeline.id) }
......
......@@ -10,12 +10,12 @@ module PartitionedTable
monthly: Gitlab::Database::Partitioning::MonthlyStrategy
}.freeze
def partitioned_by(partitioning_key, strategy:)
def partitioned_by(partitioning_key, strategy:, **kwargs)
strategy_class = PARTITIONING_STRATEGIES[strategy.to_sym] || raise(ArgumentError, "Unknown partitioning strategy: #{strategy}")
@partitioning_strategy = strategy_class.new(self, partitioning_key)
@partitioning_strategy = strategy_class.new(self, partitioning_key, **kwargs)
Gitlab::Database::Partitioning::PartitionCreator.register(self)
Gitlab::Database::Partitioning::PartitionManager.register(self)
end
end
end
......@@ -9,7 +9,7 @@ class WebHookLog < ApplicationRecord
self.primary_key = :id
partitioned_by :created_at, strategy: :monthly
partitioned_by :created_at, strategy: :monthly, retain_for: 3.months
belongs_to :web_hook
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment