Merge branch 'master' into 'template-improvements-for-documentation'

# Conflicts:
#   .gitlab/merge_request_templates/Documentation.md
This commit is contained in:
Mike Lewis 2019-02-18 21:38:24 +00:00
commit 07c32a0df5
568 changed files with 8302 additions and 2273 deletions

View file

@ -509,6 +509,7 @@ rspec-mysql:
parallel: 50
.rspec-quarantine: &rspec-quarantine
retry: 0
script:
- export CACHE_CLASSES=true
- scripts/gitaly-test-spawn

View file

@ -26,7 +26,7 @@ https://docs.gitlab.com/ce/development/documentation/index.html#changing-documen
to the new document if there are any Disqus comments on the old document thread.
- [ ] Update the link in `features.yml` (if applicable)
- [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE
with the changes as well (https://docs.gitlab.com/ce/development/writing_documentation.html#cherry-picking-from-ce-to-ee).
with the changes as well (https://docs.gitlab.com/ce/development/documentation/index.html#cherry-picking-from-ce-to-ee).
- [ ] Ping one of the technical writers for review.
/label ~Documentation

View file

@ -16,7 +16,7 @@ Add a description of your merge request here.
## Database checklist
- [ ] Conforms to the [database guides](https://docs.gitlab.com/ee/development/README.html#databases-guides)
- [ ] Conforms to the [database guides](https://docs.gitlab.com/ee/development/README.html#database-guides)
When adding migrations:
@ -49,10 +49,10 @@ When removing columns, tables, indexes or other structures:
## General checklist
- [ ] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) added, if necessary
- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/documentation/index.html#contributing-to-docs)
- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/documentation/)
- [ ] [Tests added for this feature/bug](https://docs.gitlab.com/ee/development/testing_guide/index.html)
- [ ] Conforms to the [code review guidelines](https://docs.gitlab.com/ee/development/code_review.html)
- [ ] Conforms to the [merge request performance guidelines](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html)
- [ ] Conforms to the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides)
- [ ] Conforms to the [style guides](https://docs.gitlab.com/ee/development/contributing/style_guides.html)
/label ~database

30
.stylelintrc Normal file
View file

@ -0,0 +1,30 @@
{
"extends": "stylelint-config-recommended",
"plugins": [
"stylelint-scss"
],
"rules": {
"no-descending-specificity": null,
"font-family-no-missing-generic-family-keyword": null,
"at-rule-no-unknown": [ true, {
ignoreAtRules: ["include", "each", "mixin", "extend", "if", "function", "for", "else", "return"]
}],
"selector-type-no-unknown": [true, {
"ignoreTypes": ["gl-emoji"]
}],
"unit-no-unknown" : [true, {
"ignoreFunctions": ["-webkit-image-set"]
}],
"scss/at-extend-no-missing-placeholder": null,
"scss/at-function-pattern": "^[a-z]+([a-z0-9-]+[a-z0-9]+)?$",
"scss/at-import-no-partial-leading-underscore": true,
"scss/at-import-partial-extension-blacklist": ["scss"],
"scss/at-mixin-pattern": "^[a-z]+([a-z0-9-]+[a-z0-9]+)?$",
"scss/at-rule-no-unknown": true,
"scss/dollar-variable-colon-space-after": "always",
"scss/dollar-variable-colon-space-before": "never",
"scss/dollar-variable-pattern": "^[_]?[a-z]+([a-z0-9-]+[a-z0-9]+)?$",
"scss/percent-placeholder-pattern": "^[a-z]+([a-z0-9-]+[a-z0-9]+)?$",
"scss/selector-no-redundant-nesting-selector": true,
}
}

View file

@ -11,3 +11,4 @@ danger.import_dangerfile(path: 'danger/commit_messages')
danger.import_dangerfile(path: 'danger/duplicate_yarn_dependencies')
danger.import_dangerfile(path: 'danger/prettier')
danger.import_dangerfile(path: 'danger/eslint')
danger.import_dangerfile(path: 'danger/roulette')

View file

@ -1 +1 @@
1.19.0
1.20.0

View file

@ -1 +1 @@
8.3.0
8.3.1

View file

@ -143,7 +143,7 @@ gem 'diffy', '~> 3.1.0'
gem 'rack', '2.0.6'
group :unicorn do
gem 'unicorn', '~> 5.1.0'
gem 'unicorn', '~> 5.4.1'
gem 'unicorn-worker-killer', '~> 0.4.4'
end
@ -410,7 +410,7 @@ gem 'sys-filesystem', '~> 1.1.6'
# SSH host key support
gem 'net-ssh', '~> 5.0'
gem 'sshkey', '~> 1.9.0'
gem 'sshkey', '~> 2.0'
# Required for ED25519 SSH host key support
group :ed25519 do

View file

@ -422,7 +422,7 @@ GEM
activerecord
kaminari-core (= 1.0.1)
kaminari-core (1.0.1)
kgio (2.10.0)
kgio (2.11.2)
knapsack (1.17.0)
rake
kubeclient (4.2.2)
@ -666,7 +666,7 @@ GEM
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (3.0.0)
raindrops (0.18.0)
raindrops (0.19.0)
rake (12.3.2)
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
@ -855,7 +855,7 @@ GEM
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sqlite3 (1.3.13)
sshkey (1.9.0)
sshkey (2.0.0)
stackprof (0.2.10)
state_machines (0.5.0)
state_machines-activemodel (0.5.1)
@ -898,7 +898,7 @@ GEM
unf_ext
unf_ext (0.0.7.5)
unicode-display_width (1.3.2)
unicorn (5.1.0)
unicorn (5.4.1)
kgio (~> 2.6)
raindrops (~> 0.7)
unicorn-worker-killer (0.4.4)
@ -1157,7 +1157,7 @@ DEPENDENCIES
spring (~> 2.0.0)
spring-commands-rspec (~> 1.0.4)
sprockets (~> 3.7.0)
sshkey (~> 1.9.0)
sshkey (~> 2.0)
stackprof (~> 0.2.10)
state_machines-activerecord (~> 0.5.1)
sys-filesystem (~> 1.1.6)
@ -1169,7 +1169,7 @@ DEPENDENCIES
u2f (~> 0.2.1)
uglifier (~> 2.7.2)
unf (~> 0.1.4)
unicorn (~> 5.1.0)
unicorn (~> 5.4.1)
unicorn-worker-killer (~> 0.4.4)
validates_hostname (~> 1.0.6)
version_sorter (~> 2.1.0)

View file

@ -108,7 +108,19 @@ Merge requests that make changes hidden behind a feature flag, or remove an
existing feature flag because a feature is deemed stable, may be merged (and
picked into the stable branches) up to the 19th of the month. Such merge
requests should have the ~"feature flag" label assigned, and don't require a
corresponding exception request to be created.
corresponding exception request to be created.
A level of common sense should be applied when deciding whether to have a feature
behind a feature flag off or on by default.
The following guideliness can be applied to help make this decision:
* If the feature is not fully ready or functioning, the feature flag should be disabled by default.
* If the feature is ready but there are concerns about performance or impact, the feature flag should be enabled by default, but
disabled via chatops before deployment on GitLab.com environments. If the performance concern is confirmed, the final release should have the feature flag disabled by default.
* In most other cases, the feature flag can be enabled by default.
For more information on rolling out changes using feature flags, read [through the documentation](https://docs.gitlab.com/ee/development/rolling_out_changes_using_feature_flags.html).
In order to build the final package and present the feature for self-hosted
customers, the feature flag should be removed. This should happen before the

View file

@ -36,13 +36,20 @@ export class CopyAsGFM {
div.appendChild(el.cloneNode(true));
const html = div.innerHTML;
clipboardData.setData('text/plain', el.textContent);
clipboardData.setData('text/html', html);
// We are also setting this as fallback to transform the selection to gfm on paste
clipboardData.setData('text/x-gfm-html', html);
CopyAsGFM.nodeToGFM(el)
.then(res => {
clipboardData.setData('text/plain', el.textContent);
clipboardData.setData('text/x-gfm', res);
clipboardData.setData('text/html', html);
})
.catch(() => {});
.catch(() => {
// Not showing the error as Firefox might doesn't allow
// it or other browsers who have a time limit on the execution
// of the copy event
});
}
static pasteGFM(e) {
@ -51,11 +58,28 @@ export class CopyAsGFM {
const text = clipboardData.getData('text/plain');
const gfm = clipboardData.getData('text/x-gfm');
if (!gfm) return;
const gfmHtml = clipboardData.getData('text/x-gfm-html');
if (!gfm && !gfmHtml) return;
e.preventDefault();
window.gl.utils.insertText(e.target, textBefore => {
// We have the original selection already converted to gfm
if (gfm) {
CopyAsGFM.insertPastedText(e.target, text, gfm);
} else {
// Due to the async copy call we are not able to produce gfm so we transform the cached HTML
const div = document.createElement('div');
div.innerHTML = gfmHtml;
CopyAsGFM.nodeToGFM(div)
.then(transformedGfm => {
CopyAsGFM.insertPastedText(e.target, text, transformedGfm);
})
.catch(() => {});
}
}
static insertPastedText(target, text, gfm) {
window.gl.utils.insertText(target, textBefore => {
// If the text before the cursor contains an odd number of backticks,
// we are either inside an inline code span that starts with 1 backtick
// or a code block that starts with 3 backticks.

View file

@ -221,7 +221,7 @@ export default {
</script>
<template>
<div class="board-list-component d-flex flex-column">
<div class="board-list-component">
<div v-if="loading" class="board-list-loading text-center" aria-label="Loading issues">
<gl-loading-icon />
</div>

View file

@ -4,6 +4,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
import createFlash from '~/flash';
import { GlLoadingIcon } from '@gitlab/ui';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue';
import DiffFile from './diff_file.vue';
@ -11,6 +12,13 @@ import NoChanges from './no_changes.vue';
import HiddenFilesWarning from './hidden_files_warning.vue';
import CommitWidget from './commit_widget.vue';
import TreeList from './tree_list.vue';
import {
TREE_LIST_WIDTH_STORAGE_KEY,
INITIAL_TREE_WIDTH,
MIN_TREE_WIDTH,
MAX_TREE_WIDTH,
TREE_HIDE_STATS_WIDTH,
} from '../constants';
export default {
name: 'DiffsApp',
@ -23,6 +31,7 @@ export default {
CommitWidget,
TreeList,
GlLoadingIcon,
PanelResizer,
},
props: {
endpoint: {
@ -54,8 +63,12 @@ export default {
},
},
data() {
const treeWidth =
parseInt(localStorage.getItem(TREE_LIST_WIDTH_STORAGE_KEY), 10) || INITIAL_TREE_WIDTH;
return {
assignedDiscussions: false,
treeWidth,
};
},
computed: {
@ -96,6 +109,9 @@ export default {
this.startVersion.version_index === this.mergeRequestDiff.version_index)
);
},
hideFileStats() {
return this.treeWidth <= TREE_HIDE_STATS_WIDTH;
},
},
watch: {
diffViewType() {
@ -142,6 +158,7 @@ export default {
'startRenderDiffsQueue',
'assignDiscussionsToDiff',
'setHighlightedRow',
'cacheTreeListWidth',
]),
fetchData() {
this.fetchDiffFiles()
@ -184,6 +201,8 @@ export default {
}
},
},
minTreeWidth: MIN_TREE_WIDTH,
maxTreeWidth: MAX_TREE_WIDTH,
};
</script>
@ -209,7 +228,21 @@ export default {
:data-can-create-note="getNoteableData.current_user.can_create_note"
class="files d-flex prepend-top-default"
>
<div v-show="showTreeList" class="diff-tree-list"><tree-list /></div>
<div
v-show="showTreeList"
:style="{ width: `${treeWidth}px` }"
class="diff-tree-list js-diff-tree-list"
>
<panel-resizer
:size.sync="treeWidth"
:start-size="treeWidth"
:min-size="$options.minTreeWidth"
:max-size="$options.maxTreeWidth"
side="right"
@resize-end="cacheTreeListWidth"
/>
<tree-list :hide-file-stats="hideFileStats" />
</div>
<div class="diff-files-holder">
<commit-widget v-if="commit" :commit="commit" />
<template v-if="renderDiffFiles">

View file

@ -1,7 +1,8 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import EmptyFileViewer from '~/vue_shared/components/diff_viewer/viewers/empty_file.vue';
import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue';
import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue';
import InlineDiffView from './inline_diff_view.vue';
import ParallelDiffView from './parallel_diff_view.vue';
import NoteForm from '../../notes/components/note_form.vue';
@ -9,6 +10,7 @@ import ImageDiffOverlay from './image_diff_overlay.vue';
import DiffDiscussions from './diff_discussions.vue';
import { IMAGE_DIFF_POSITION_TYPE } from '../constants';
import { getDiffMode } from '../store/utils';
import { diffViewerModes } from '~/ide/constants';
export default {
components: {
@ -18,7 +20,8 @@ export default {
NoteForm,
DiffDiscussions,
ImageDiffOverlay,
EmptyFileViewer,
NotDiffableViewer,
NoPreviewViewer,
},
props: {
diffFile: {
@ -42,11 +45,17 @@ export default {
diffMode() {
return getDiffMode(this.diffFile);
},
isTextFile() {
return this.diffFile.viewer.name === 'text';
diffViewerMode() {
return this.diffFile.viewer.name;
},
errorMessage() {
return this.diffFile.viewer.error;
isTextFile() {
return this.diffViewerMode === diffViewerModes.text;
},
noPreview() {
return this.diffViewerMode === diffViewerModes.no_preview;
},
notDiffable() {
return this.diffViewerMode === diffViewerModes.not_diffable;
},
diffFileCommentForm() {
return this.getCommentFormForDiffFile(this.diffFile.file_hash);
@ -78,11 +87,10 @@ export default {
<template>
<div class="diff-content">
<div v-if="!errorMessage" class="diff-viewer">
<div class="diff-viewer">
<template v-if="isTextFile">
<empty-file-viewer v-if="diffFile.empty" />
<inline-diff-view
v-else-if="isInlineView"
v-if="isInlineView"
:diff-file="diffFile"
:diff-lines="diffFile.highlighted_diff_lines || []"
:help-page-path="helpPagePath"
@ -94,9 +102,12 @@ export default {
:help-page-path="helpPagePath"
/>
</template>
<not-diffable-viewer v-else-if="notDiffable" />
<no-preview-viewer v-else-if="noPreview" />
<diff-viewer
v-else
:diff-mode="diffMode"
:diff-viewer-mode="diffViewerMode"
:new-path="diffFile.new_path"
:new-sha="diffFile.diff_refs.head_sha"
:old-path="diffFile.old_path"
@ -132,8 +143,5 @@ export default {
</div>
</diff-viewer>
</div>
<div v-else class="diff-viewer">
<div class="nothing-here-block" v-html="errorMessage"></div>
</div>
</div>
</template>

View file

@ -7,6 +7,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import eventHub from '../../notes/event_hub';
import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue';
import { diffViewerErrors } from '~/ide/constants';
export default {
components: {
@ -33,15 +34,13 @@ export default {
return {
isLoadingCollapsedDiff: false,
forkMessageVisible: false,
isCollapsed: this.file.viewer.collapsed || false,
};
},
computed: {
...mapState('diffs', ['currentDiffFileId']),
...mapGetters(['isNotesFetched']),
...mapGetters('diffs', ['getDiffFileDiscussions']),
isCollapsed() {
return this.file.collapsed || false;
},
viewBlobLink() {
return sprintf(
__('You can %{linkStart}view the blob%{linkEnd} instead.'),
@ -52,17 +51,6 @@ export default {
false,
);
},
showExpandMessage() {
return (
this.isCollapsed ||
(!this.file.highlighted_diff_lines &&
!this.isLoadingCollapsedDiff &&
!this.file.too_large &&
this.file.text &&
!this.file.renamed_file &&
!this.file.mode_changed)
);
},
showLoadingIcon() {
return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
},
@ -73,9 +61,15 @@ export default {
this.file.parallel_diff_lines.length > 0
);
},
isFileTooLarge() {
return this.file.viewer.error === diffViewerErrors.too_large;
},
errorMessage() {
return this.file.viewer.error_message;
},
},
watch: {
'file.collapsed': function fileCollapsedWatch(newVal, oldVal) {
isCollapsed: function fileCollapsedWatch(newVal, oldVal) {
if (!newVal && oldVal && !this.hasDiffLines) {
this.handleLoadCollapsedDiff();
}
@ -85,13 +79,13 @@ export default {
eventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.handleLoadCollapsedDiff);
},
methods: {
...mapActions('diffs', ['loadCollapsedDiff', 'assignDiscussionsToDiff']),
...mapActions('diffs', ['loadCollapsedDiff', 'assignDiscussionsToDiff', 'setRenderIt']),
handleToggle() {
if (!this.hasDiffLines) {
this.handleLoadCollapsedDiff();
} else {
this.file.collapsed = !this.file.collapsed;
this.file.renderIt = true;
this.isCollapsed = !this.isCollapsed;
this.setRenderIt(this.file);
}
},
handleLoadCollapsedDiff() {
@ -100,8 +94,8 @@ export default {
this.loadCollapsedDiff(this.file)
.then(() => {
this.isLoadingCollapsedDiff = false;
this.file.collapsed = false;
this.file.renderIt = true;
this.isCollapsed = false;
this.setRenderIt(this.file);
})
.then(() => {
requestIdleCallback(
@ -164,21 +158,25 @@ export default {
Cancel
</button>
</div>
<diff-content
v-if="!isCollapsed && file.renderIt"
:class="{ hidden: isCollapsed || file.too_large }"
:diff-file="file"
:help-page-path="helpPagePath"
/>
<gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" />
<div v-else-if="showExpandMessage" class="nothing-here-block diff-collapsed">
{{ __('This diff is collapsed.') }}
<a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{
__('Click to expand it.')
}}</a>
</div>
<div v-if="file.too_large" class="nothing-here-block diff-collapsed js-too-large-diff">
<template v-else>
<div v-if="errorMessage" class="diff-viewer">
<div class="nothing-here-block" v-html="errorMessage"></div>
</div>
<div v-else-if="isCollapsed" class="nothing-here-block diff-collapsed">
{{ __('This diff is collapsed.') }}
<a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{
__('Click to expand it.')
}}</a>
</div>
<diff-content
v-else
:class="{ hidden: isCollapsed || isFileTooLarge }"
:diff-file="file"
:help-page-path="helpPagePath"
/>
</template>
<div v-if="isFileTooLarge" class="nothing-here-block diff-collapsed js-too-large-diff">
{{ __('This source diff could not be displayed because it is too large.') }}
<span v-html="viewBlobLink"></span>
</div>

View file

@ -8,6 +8,7 @@ import FileIcon from '~/vue_shared/components/file_icon.vue';
import { GlTooltipDirective } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale';
import { diffViewerModes } from '~/ide/constants';
import EditButton from './edit_button.vue';
import DiffStats from './diff_stats.vue';
@ -118,6 +119,12 @@ export default {
gfmCopyText() {
return `\`${this.diffFile.file_path}\``;
},
isFileRenamed() {
return this.diffFile.viewer.name === diffViewerModes.renamed;
},
isModeChanged() {
return this.diffFile.viewer.name === diffViewerModes.mode_changed;
},
},
mounted() {
polyfillSticky(this.$refs.header);
@ -165,7 +172,7 @@ export default {
aria-hidden="true"
css-classes="js-file-icon append-right-5"
/>
<span v-if="diffFile.renamed_file">
<span v-if="isFileRenamed">
<strong
v-gl-tooltip
:title="diffFile.old_path"
@ -193,7 +200,7 @@ export default {
css-class="btn-default btn-transparent btn-clipboard"
/>
<small v-if="diffFile.mode_changed" ref="fileMode">
<small v-if="isModeChanged" ref="fileMode">
{{ diffFile.a_mode }} {{ diffFile.b_mode }}
</small>

View file

@ -13,6 +13,12 @@ export default {
Icon,
FileRow,
},
props: {
hideFileStats: {
type: Boolean,
required: true,
},
},
data() {
return {
search: '',
@ -40,6 +46,9 @@ export default {
return acc;
}, []);
},
fileRowExtraComponent() {
return this.hideFileStats ? null : FileRowStats;
},
},
methods: {
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile', 'toggleFileFinder']),
@ -48,7 +57,6 @@ export default {
},
},
shortcutKeyCharacter: `${/Mac/i.test(navigator.userAgent) ? '&#8984;' : 'Ctrl'}+P`,
FileRowStats,
diffTreeFiltering: gon.features && gon.features.diffTreeFiltering,
};
</script>
@ -98,7 +106,7 @@ export default {
:file="file"
:level="0"
:hide-extra-on-tree="true"
:extra-component="$options.FileRowStats"
:extra-component="fileRowExtraComponent"
:show-changed-icon="true"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="scrollToFile"

View file

@ -36,3 +36,9 @@ export const MR_TREE_SHOW_KEY = 'mr_tree_show';
export const TREE_TYPE = 'tree';
export const TREE_LIST_STORAGE_KEY = 'mr_diff_tree_list';
export const WHITESPACE_STORAGE_KEY = 'mr_show_whitespace';
export const TREE_LIST_WIDTH_STORAGE_KEY = 'mr_tree_list_width';
export const INITIAL_TREE_WIDTH = 320;
export const MIN_TREE_WIDTH = 240;
export const MAX_TREE_WIDTH = 400;
export const TREE_HIDE_STATS_WIDTH = 260;

View file

@ -16,7 +16,9 @@ import {
MR_TREE_SHOW_KEY,
TREE_LIST_STORAGE_KEY,
WHITESPACE_STORAGE_KEY,
TREE_LIST_WIDTH_STORAGE_KEY,
} from '../constants';
import { diffViewerModes } from '~/ide/constants';
export const setBaseConfig = ({ commit }, options) => {
const { endpoint, projectPath } = options;
@ -91,7 +93,7 @@ export const renderFileForDiscussionId = ({ commit, rootState, state }, discussi
commit(types.RENDER_FILE, file);
}
if (file.collapsed) {
if (file.viewer.collapsed) {
eventHub.$emit(`loadCollapsedDiff/${file.file_hash}`);
scrollToElement(document.getElementById(file.file_hash));
} else {
@ -105,7 +107,8 @@ export const startRenderDiffsQueue = ({ state, commit }) => {
const checkItem = () =>
new Promise(resolve => {
const nextFile = state.diffFiles.find(
file => !file.renderIt && (!file.collapsed || !file.text),
file =>
!file.renderIt && (!file.viewer.collapsed || !file.viewer.name === diffViewerModes.text),
);
if (nextFile) {
@ -128,6 +131,8 @@ export const startRenderDiffsQueue = ({ state, commit }) => {
return checkItem();
};
export const setRenderIt = ({ commit }, file) => commit(types.RENDER_FILE, file);
export const setInlineDiffViewType = ({ commit }) => {
commit(types.SET_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE);
@ -300,5 +305,9 @@ export const toggleFileFinder = ({ commit }, visible) => {
commit(types.TOGGLE_FILE_FINDER_VISIBLE, visible);
};
export const cacheTreeListWidth = (_, size) => {
localStorage.setItem(TREE_LIST_WIDTH_STORAGE_KEY, size);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View file

@ -4,7 +4,8 @@ export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW
export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE;
export const hasCollapsedFile = state => state.diffFiles.some(file => file.collapsed);
export const hasCollapsedFile = state =>
state.diffFiles.some(file => file.viewer && file.viewer.collapsed);
export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null);

View file

@ -144,6 +144,7 @@ export default {
if (left || right) {
return {
...line,
left: line.left ? mapDiscussions(line.left) : null,
right: line.right ? mapDiscussions(line.right, () => !left) : null,
};

View file

@ -1,6 +1,6 @@
import _ from 'underscore';
import { diffModes } from '~/ide/constants';
import { truncatePathMiddleToLength } from '~/lib/utils/text_utility';
import { diffModes, diffViewerModes } from '~/ide/constants';
import {
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
@ -161,6 +161,7 @@ export function addContextLines(options) {
const normalizedParallelLines = contextLines.map(line => ({
left: line,
right: line,
line_code: line.line_code,
}));
if (options.bottom) {
@ -247,7 +248,8 @@ export function prepareDiffData(diffData) {
Object.assign(file, {
renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY,
collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED,
collapsed:
file.viewer.name === diffViewerModes.text && showingLines > MAX_LINES_TO_BE_RENDERED,
discussions: [],
});
}
@ -403,7 +405,9 @@ export const getDiffMode = diffFile => {
const diffModeKey = Object.keys(diffModes).find(key => diffFile[`${key}_file`]);
return (
diffModes[diffModeKey] ||
(diffFile.mode_changed && diffModes.mode_changed) ||
(diffFile.viewer &&
diffFile.viewer.name === diffViewerModes.mode_changed &&
diffViewerModes.mode_changed) ||
diffModes.replaced
);
};

View file

@ -0,0 +1,63 @@
import { __ } from '~/locale';
import emojiRegex from 'emoji-regex';
const invalidInputClass = 'gl-field-error-outline';
export default class NoEmojiValidator {
constructor(opts = {}) {
const container = opts.container || '';
this.noEmojiEmelents = document.querySelectorAll(`${container} .js-block-emoji`);
this.noEmojiEmelents.forEach(element =>
element.addEventListener('input', this.eventHandler.bind(this)),
);
}
eventHandler(event) {
this.inputDomElement = event.target;
this.inputErrorMessage = this.inputDomElement.nextSibling;
const { value } = this.inputDomElement;
this.validatePattern(value);
this.setValidationStateAndMessage();
}
validatePattern(value) {
const pattern = emojiRegex();
this.hasEmojis = new RegExp(pattern).test(value);
if (this.hasEmojis) {
this.inputDomElement.setCustomValidity(__('Invalid input, please avoid emojis'));
} else {
this.inputDomElement.setCustomValidity('');
}
}
setValidationStateAndMessage() {
if (!this.inputDomElement.checkValidity()) {
this.setInvalidState();
} else {
this.clearFieldValidationState();
}
}
clearFieldValidationState() {
this.inputDomElement.classList.remove(invalidInputClass);
this.inputErrorMessage.classList.add('hide');
}
setInvalidState() {
this.inputDomElement.classList.add(invalidInputClass);
this.setErrorMessage();
}
setErrorMessage() {
if (this.hasEmojis) {
this.inputErrorMessage.innerHTML = this.inputDomElement.validationMessage;
} else {
this.inputErrorMessage.innerHTML = this.inputDomElement.title;
}
this.inputErrorMessage.classList.remove('hide');
}
}

View file

@ -163,7 +163,7 @@ export default class FilteredSearchVisualTokens {
const tokenValueElement = tokenValueContainer.querySelector('.value');
tokenValueElement.innerText = tokenValue;
if (tokenValue === 'none' || tokenValue === 'any') {
if (['none', 'any'].includes(tokenValue.toLowerCase())) {
return;
}

View file

@ -44,7 +44,7 @@ export default {
<div class="d-flex ide-commit-editor-header align-items-center">
<file-icon :file-name="activeFile.name" :size="16" class="mr-2" />
<strong class="mr-2"> {{ activeFile.path }} </strong>
<changed-file-icon :file="activeFile" class="ml-0" />
<changed-file-icon :file="activeFile" :is-centered="false" />
<div class="ml-auto">
<button
v-if="!isStaged"

View file

@ -51,8 +51,11 @@ export default {
return __('Create file');
},
isCreatingNew() {
return this.entryModal.type !== modalTypes.rename;
isCreatingNewFile() {
return this.entryModal.type === 'blob';
},
placeholder() {
return this.isCreatingNewFile ? 'dir/file_name' : 'dir/';
},
},
methods: {
@ -107,9 +110,12 @@ export default {
v-model="entryName"
type="text"
class="form-control qa-full-file-path"
placeholder="/dir/file_name"
:placeholder="placeholder"
/>
<ul v-if="isCreatingNew" class="prepend-top-default list-inline qa-template-list">
<ul
v-if="isCreatingNewFile"
class="file-templates prepend-top-default list-inline qa-template-list"
>
<li v-for="(template, index) in templateTypes" :key="index" class="list-inline-item">
<button
type="button"

View file

@ -24,6 +24,22 @@ export const diffModes = {
mode_changed: 'mode_changed',
};
export const diffViewerModes = Object.freeze({
not_diffable: 'not_diffable',
no_preview: 'no_preview',
added: 'added',
deleted: 'deleted',
renamed: 'renamed',
mode_changed: 'mode_changed',
text: 'text',
image: 'image',
});
export const diffViewerErrors = Object.freeze({
too_large: 'too_large',
stored_externally: 'server_side_but_stored_externally',
});
export const rightSidebarViews = {
pipelines: { name: 'pipelines-list', keepAlive: true },
jobsDetail: { name: 'jobs-detail', keepAlive: false },

View file

@ -0,0 +1,101 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { __, sprintf } from '~/locale';
import ImportedProjectTableRow from './imported_project_table_row.vue';
import ProviderRepoTableRow from './provider_repo_table_row.vue';
import eventHub from '../event_hub';
export default {
name: 'ImportProjectsTable',
components: {
ImportedProjectTableRow,
ProviderRepoTableRow,
LoadingButton,
GlLoadingIcon,
},
props: {
providerTitle: {
type: String,
required: true,
},
},
computed: {
...mapState(['importedProjects', 'providerRepos', 'isLoadingRepos']),
...mapGetters(['isImportingAnyRepo', 'hasProviderRepos', 'hasImportedProjects']),
emptyStateText() {
return sprintf(__('No %{providerTitle} repositories available to import'), {
providerTitle: this.providerTitle,
});
},
fromHeaderText() {
return sprintf(__('From %{providerTitle}'), { providerTitle: this.providerTitle });
},
},
mounted() {
return this.fetchRepos();
},
beforeDestroy() {
this.stopJobsPolling();
this.clearJobsEtagPoll();
},
methods: {
...mapActions(['fetchRepos', 'fetchJobs', 'stopJobsPolling', 'clearJobsEtagPoll']),
importAll() {
eventHub.$emit('importAll');
},
},
};
</script>
<template>
<div>
<div class="d-flex justify-content-between align-items-end flex-wrap mb-3">
<p class="light text-nowrap mt-2 my-sm-0">
{{ s__('ImportProjects|Select the projects you want to import') }}
</p>
<loading-button
container-class="btn btn-success js-import-all"
:loading="isImportingAnyRepo"
:label="__('Import all repositories')"
:disabled="!hasProviderRepos"
type="button"
@click="importAll"
/>
</div>
<gl-loading-icon
v-if="isLoadingRepos"
class="js-loading-button-icon import-projects-loading-icon"
:size="4"
/>
<div v-else-if="hasProviderRepos || hasImportedProjects" class="table-responsive">
<table class="table import-table">
<thead>
<th class="import-jobs-from-col">{{ fromHeaderText }}</th>
<th class="import-jobs-to-col">{{ __('To GitLab') }}</th>
<th class="import-jobs-status-col">{{ __('Status') }}</th>
<th class="import-jobs-cta-col"></th>
</thead>
<tbody>
<imported-project-table-row
v-for="project in importedProjects"
:key="project.id"
:project="project"
/>
<provider-repo-table-row v-for="repo in providerRepos" :key="repo.id" :repo="repo" />
</tbody>
</table>
</div>
<div v-else class="text-center">
<strong>{{ emptyStateText }}</strong>
</div>
</div>
</template>

View file

@ -0,0 +1,47 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import STATUS_MAP from '../constants';
export default {
name: 'ImportStatus',
components: {
CiIcon,
GlLoadingIcon,
},
props: {
status: {
type: String,
required: true,
},
},
computed: {
mappedStatus() {
return STATUS_MAP[this.status];
},
ciIconStatus() {
const { icon } = this.mappedStatus;
return {
icon: `status_${icon}`,
group: icon,
};
},
},
};
</script>
<template>
<div>
<gl-loading-icon
v-if="mappedStatus.loadingIcon"
:inline="true"
:class="mappedStatus.textClass"
class="align-middle mr-2"
/>
<ci-icon v-else css-classes="align-middle mr-2" :status="ciIconStatus" />
<span :class="mappedStatus.textClass">{{ mappedStatus.text }}</span>
</div>
</template>

View file

@ -0,0 +1,55 @@
<script>
import ImportStatus from './import_status.vue';
import { STATUSES } from '../constants';
export default {
name: 'ImportedProjectTableRow',
components: {
ImportStatus,
},
props: {
project: {
type: Object,
required: true,
},
},
computed: {
displayFullPath() {
return this.project.fullPath.replace(/^\//, '');
},
isFinished() {
return this.project.importStatus === STATUSES.FINISHED;
},
},
};
</script>
<template>
<tr class="js-imported-project import-row">
<td>
<a
:href="project.providerLink"
rel="noreferrer noopener"
target="_blank"
class="js-provider-link"
>
{{ project.importSource }}
</a>
</td>
<td class="js-full-path">{{ displayFullPath }}</td>
<td><import-status :status="project.importStatus" /></td>
<td>
<a
v-if="isFinished"
class="btn btn-default js-go-to-project"
:href="project.fullPath"
rel="noreferrer noopener"
target="_blank"
>
{{ __('Go to project') }}
</a>
</td>
</tr>
</template>

View file

@ -0,0 +1,110 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import Select2Select from '~/vue_shared/components/select2_select.vue';
import { __ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '../event_hub';
import { STATUSES } from '../constants';
import ImportStatus from './import_status.vue';
export default {
name: 'ProviderRepoTableRow',
components: {
Select2Select,
LoadingButton,
ImportStatus,
},
props: {
repo: {
type: Object,
required: true,
},
},
data() {
return {
targetNamespace: this.$store.state.defaultTargetNamespace,
newName: this.repo.sanitizedName,
};
},
computed: {
...mapState(['namespaces', 'reposBeingImported', 'ciCdOnly']),
...mapGetters(['namespaceSelectOptions']),
importButtonText() {
return this.ciCdOnly ? __('Connect') : __('Import');
},
select2Options() {
return {
data: this.namespaceSelectOptions,
containerCssClass:
'import-namespace-select js-namespace-select qa-project-namespace-select',
};
},
isLoadingImport() {
return this.reposBeingImported.includes(this.repo.id);
},
status() {
return this.isLoadingImport ? STATUSES.SCHEDULING : STATUSES.NONE;
},
},
created() {
eventHub.$on('importAll', () => this.importRepo());
},
methods: {
...mapActions(['fetchImport']),
importRepo() {
return this.fetchImport({
newName: this.newName,
targetNamespace: this.targetNamespace,
repo: this.repo,
});
},
},
};
</script>
<template>
<tr class="qa-project-import-row js-provider-repo import-row">
<td>
<a
:href="repo.providerLink"
rel="noreferrer noopener"
target="_blank"
class="js-provider-link"
>
{{ repo.fullName }}
</a>
</td>
<td class="d-flex flex-wrap flex-lg-nowrap">
<select2-select v-model="targetNamespace" :options="select2Options" />
<span class="px-2 import-slash-divider d-flex justify-content-center align-items-center"
>/</span
>
<input
v-model="newName"
type="text"
class="form-control import-project-name-input js-new-name qa-project-path-field"
/>
</td>
<td><import-status :status="status" /></td>
<td>
<button
v-if="!isLoadingImport"
type="button"
class="qa-import-button js-import-button btn btn-default"
@click="importRepo"
>
{{ importButtonText }}
</button>
</td>
</tr>
</template>

View file

@ -0,0 +1,48 @@
import { __ } from '../locale';
// The `scheduling` status is only present on the client-side,
// it is used as the status when we are requesting to start an import.
export const STATUSES = {
FINISHED: 'finished',
FAILED: 'failed',
SCHEDULED: 'scheduled',
STARTED: 'started',
NONE: 'none',
SCHEDULING: 'scheduling',
};
const STATUS_MAP = {
[STATUSES.FINISHED]: {
icon: 'success',
text: __('Done'),
textClass: 'text-success',
},
[STATUSES.FAILED]: {
icon: 'failed',
text: __('Failed'),
textClass: 'text-danger',
},
[STATUSES.SCHEDULED]: {
icon: 'pending',
text: __('Scheduled'),
textClass: 'text-warning',
},
[STATUSES.STARTED]: {
icon: 'running',
text: __('Running…'),
textClass: 'text-info',
},
[STATUSES.NONE]: {
icon: 'created',
text: __('Not started'),
textClass: 'text-muted',
},
[STATUSES.SCHEDULING]: {
loadingIcon: true,
text: __('Scheduling'),
textClass: 'text-warning',
},
};
export default STATUS_MAP;

View file

@ -0,0 +1,3 @@
import Vue from 'vue';
export default new Vue();

View file

@ -0,0 +1,47 @@
import Vue from 'vue';
import { mapActions } from 'vuex';
import Translate from '../vue_shared/translate';
import ImportProjectsTable from './components/import_projects_table.vue';
import { parseBoolean } from '../lib/utils/common_utils';
import store from './store';
Vue.use(Translate);
export default function mountImportProjectsTable(mountElement) {
if (!mountElement) return undefined;
const {
reposPath,
provider,
providerTitle,
canSelectNamespace,
jobsPath,
importPath,
ciCdOnly,
} = mountElement.dataset;
return new Vue({
el: mountElement,
store,
created() {
this.setInitialData({
reposPath,
provider,
jobsPath,
importPath,
defaultTargetNamespace: gon.current_username,
ciCdOnly: parseBoolean(ciCdOnly),
canSelectNamespace: parseBoolean(canSelectNamespace),
});
},
methods: {
...mapActions(['setInitialData']),
},
render(createElement) {
return createElement(ImportProjectsTable, { props: { providerTitle } });
},
});
}

View file

@ -0,0 +1,106 @@
import Visibility from 'visibilityjs';
import * as types from './mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
import createFlash from '~/flash';
import { s__, sprintf } from '~/locale';
import axios from '~/lib/utils/axios_utils';
let eTagPoll;
export const clearJobsEtagPoll = () => {
eTagPoll = null;
};
export const stopJobsPolling = () => {
if (eTagPoll) eTagPoll.stop();
};
export const restartJobsPolling = () => {
if (eTagPoll) eTagPoll.restart();
};
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const requestRepos = ({ commit }, repos) => commit(types.REQUEST_REPOS, repos);
export const receiveReposSuccess = ({ commit }, repos) =>
commit(types.RECEIVE_REPOS_SUCCESS, repos);
export const receiveReposError = ({ commit }) => commit(types.RECEIVE_REPOS_ERROR);
export const fetchRepos = ({ state, dispatch }) => {
dispatch('requestRepos');
return axios
.get(state.reposPath)
.then(({ data }) =>
dispatch('receiveReposSuccess', convertObjectPropsToCamelCase(data, { deep: true })),
)
.then(() => dispatch('fetchJobs'))
.catch(() => {
createFlash(
sprintf(s__('ImportProjects|Requesting your %{provider} repositories failed'), {
provider: state.provider,
}),
);
dispatch('receiveReposError');
});
};
export const requestImport = ({ commit, state }, repoId) => {
if (!state.reposBeingImported.includes(repoId)) commit(types.REQUEST_IMPORT, repoId);
};
export const receiveImportSuccess = ({ commit }, { importedProject, repoId }) =>
commit(types.RECEIVE_IMPORT_SUCCESS, { importedProject, repoId });
export const receiveImportError = ({ commit }, repoId) =>
commit(types.RECEIVE_IMPORT_ERROR, repoId);
export const fetchImport = ({ state, dispatch }, { newName, targetNamespace, repo }) => {
dispatch('requestImport', repo.id);
return axios
.post(state.importPath, {
ci_cd_only: state.ciCdOnly,
new_name: newName,
repo_id: repo.id,
target_namespace: targetNamespace,
})
.then(({ data }) =>
dispatch('receiveImportSuccess', {
importedProject: convertObjectPropsToCamelCase(data, { deep: true }),
repoId: repo.id,
}),
)
.catch(() => {
createFlash(s__('ImportProjects|Importing the project failed'));
dispatch('receiveImportError', { repoId: repo.id });
});
};
export const receiveJobsSuccess = ({ commit }, updatedProjects) =>
commit(types.RECEIVE_JOBS_SUCCESS, updatedProjects);
export const fetchJobs = ({ state, dispatch }) => {
if (eTagPoll) return;
eTagPoll = new Poll({
resource: {
fetchJobs: () => axios.get(state.jobsPath),
},
method: 'fetchJobs',
successCallback: ({ data }) =>
dispatch('receiveJobsSuccess', convertObjectPropsToCamelCase(data, { deep: true })),
errorCallback: () => createFlash(s__('ImportProjects|Updating the imported projects failed')),
});
if (!Visibility.hidden()) {
eTagPoll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
dispatch('restartJobsPolling');
} else {
dispatch('stopJobsPolling');
}
});
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View file

@ -0,0 +1,20 @@
export const namespaceSelectOptions = state => {
const serializedNamespaces = state.namespaces.map(({ fullPath }) => ({
id: fullPath,
text: fullPath,
}));
return [
{ text: 'Groups', children: serializedNamespaces },
{
text: 'Users',
children: [{ id: state.defaultTargetNamespace, text: state.defaultTargetNamespace }],
},
];
};
export const isImportingAnyRepo = state => state.reposBeingImported.length > 0;
export const hasProviderRepos = state => state.providerRepos.length > 0;
export const hasImportedProjects = state => state.importedProjects.length > 0;

View file

@ -0,0 +1,15 @@
import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
Vue.use(Vuex);
export default new Vuex.Store({
state: state(),
actions,
mutations,
getters,
});

View file

@ -0,0 +1,11 @@
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const REQUEST_REPOS = 'REQUEST_REPOS';
export const RECEIVE_REPOS_SUCCESS = 'RECEIVE_REPOS_SUCCESS';
export const RECEIVE_REPOS_ERROR = 'RECEIVE_REPOS_ERROR';
export const REQUEST_IMPORT = 'REQUEST_IMPORT';
export const RECEIVE_IMPORT_SUCCESS = 'RECEIVE_IMPORT_SUCCESS';
export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR';
export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';

View file

@ -0,0 +1,55 @@
import Vue from 'vue';
import * as types from './mutation_types';
export default {
[types.SET_INITIAL_DATA](state, data) {
Object.assign(state, data);
},
[types.REQUEST_REPOS](state) {
state.isLoadingRepos = true;
},
[types.RECEIVE_REPOS_SUCCESS](state, { importedProjects, providerRepos, namespaces }) {
state.isLoadingRepos = false;
state.importedProjects = importedProjects;
state.providerRepos = providerRepos;
state.namespaces = namespaces;
},
[types.RECEIVE_REPOS_ERROR](state) {
state.isLoadingRepos = false;
},
[types.REQUEST_IMPORT](state, repoId) {
state.reposBeingImported.push(repoId);
},
[types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }) {
const existingRepoIndex = state.reposBeingImported.indexOf(repoId);
if (state.reposBeingImported.includes(repoId))
state.reposBeingImported.splice(existingRepoIndex, 1);
const providerRepoIndex = state.providerRepos.findIndex(
providerRepo => providerRepo.id === repoId,
);
state.providerRepos.splice(providerRepoIndex, 1);
state.importedProjects.unshift(importedProject);
},
[types.RECEIVE_IMPORT_ERROR](state, repoId) {
const repoIndex = state.reposBeingImported.indexOf(repoId);
if (state.reposBeingImported.includes(repoId)) state.reposBeingImported.splice(repoIndex, 1);
},
[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects) {
updatedProjects.forEach(updatedProject => {
const existingProject = state.importedProjects.find(
importedProject => importedProject.id === updatedProject.id,
);
Vue.set(existingProject, 'importStatus', updatedProject.importStatus);
});
},
};

View file

@ -0,0 +1,15 @@
export default () => ({
reposPath: '',
importPath: '',
jobsPath: '',
currentProjectId: '',
provider: '',
currentUsername: '',
importedProjects: [],
providerRepos: [],
namespaces: [],
reposBeingImported: [],
isLoadingRepos: false,
canSelectNamespace: false,
ciCdOnly: false,
});

View file

@ -110,7 +110,7 @@ export default {
<div class="sidebar-container">
<div class="blocks-container">
<div class="block d-flex flex-nowrap align-items-center">
<h4 class="my-0 mr-2">{{ job.name }}</h4>
<h4 class="my-0 mr-2 text-break-word">{{ job.name }}</h4>
<div class="flex-grow-1 flex-shrink-0 text-right">
<gl-link
v-if="job.retry_path"

View file

@ -130,7 +130,7 @@ export const isInViewport = (el, offset = {}) => {
rect.top >= (top || 0) &&
rect.left >= (left || 0) &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth
parseInt(rect.right, 10) <= window.innerWidth
);
};

View file

@ -5,6 +5,7 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import { GlSkeletonLoading } from '@gitlab/ui';
import { getDiffMode } from '~/diffs/store/utils';
import { diffViewerModes } from '~/ide/constants';
export default {
components: {
@ -31,6 +32,12 @@ export default {
diffMode() {
return getDiffMode(this.discussion.diff_file);
},
diffViewerMode() {
return this.discussion.diff_file.viewer.name;
},
isTextFile() {
return this.diffViewerMode === diffViewerModes.text;
},
hasTruncatedDiffLines() {
return (
this.discussion.truncated_diff_lines && this.discussion.truncated_diff_lines.length !== 0
@ -58,18 +65,14 @@ export default {
</script>
<template>
<div :class="{ 'text-file': discussion.diff_file.text }" class="diff-file file-holder">
<div :class="{ 'text-file': isTextFile }" class="diff-file file-holder">
<diff-file-header
:discussion-path="discussion.discussion_path"
:diff-file="discussion.diff_file"
:can-current-user-fork="false"
:expanded="!discussion.diff_file.collapsed"
:expanded="!discussion.diff_file.viewer.collapsed"
/>
<div
v-if="discussion.diff_file.text"
:class="$options.userColorSchemeClass"
class="diff-content code"
>
<div v-if="isTextFile" :class="$options.userColorSchemeClass" class="diff-content code">
<table>
<template v-if="hasTruncatedDiffLines">
<tr
@ -109,6 +112,7 @@ export default {
<div v-else>
<diff-viewer
:diff-mode="diffMode"
:diff-viewer-mode="diffViewerMode"
:new-path="discussion.diff_file.new_path"
:new-sha="discussion.diff_file.diff_refs.head_sha"
:old-path="discussion.diff_file.old_path"

View file

@ -23,11 +23,6 @@ export default {
type: [String, Number],
required: true,
},
discussionId: {
type: String,
required: false,
default: '',
},
noteUrl: {
type: String,
required: false,
@ -126,6 +121,11 @@ export default {
onResolve() {
this.$emit('handleResolve');
},
closeTooltip() {
this.$nextTick(() => {
this.$root.$emit('bv::hide::tooltip');
});
},
},
};
</script>
@ -171,7 +171,7 @@ export default {
v-if="showReplyButton"
ref="replyButton"
class="js-reply-button"
:note-id="discussionId"
@startReplying="$emit('startReplying')"
/>
<div v-if="canEdit" class="note-actions-item">
<button
@ -202,6 +202,7 @@ export default {
title="More actions"
class="note-action-button more-actions-toggle btn btn-transparent"
data-toggle="dropdown"
@click="closeTooltip"
>
<icon css-classes="icon" name="ellipsis_v" />
</button>

View file

@ -1,5 +1,4 @@
<script>
import { mapActions } from 'vuex';
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
@ -12,15 +11,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
noteId: {
type: String,
required: true,
},
},
methods: {
...mapActions(['convertToDiscussion']),
},
};
</script>
@ -32,7 +22,7 @@ export default {
class="note-action-button"
variant="transparent"
:title="__('Reply to comment')"
@click="convertToDiscussion(noteId)"
@click="$emit('startReplying')"
>
<icon name="comment" css-classes="link-highlight" />
</gl-button>

View file

@ -95,6 +95,7 @@ export default {
<div ref="note-body" :class="{ 'js-task-list-container': canEdit }" class="note-body">
<suggestions
v-if="hasSuggestion && !isEditing"
class="note-text md"
:suggestions="note.suggestions"
:note-html="note.note_html"
:line-type="lineType"

View file

@ -26,6 +26,7 @@ import resolvable from '../mixins/resolvable';
import discussionNavigation from '../mixins/discussion_navigation';
import ReplyPlaceholder from './discussion_reply_placeholder.vue';
import jumpToNextDiscussionButton from './discussion_jump_to_next_button.vue';
import eventHub from '../event_hub';
export default {
name: 'NoteableDiscussion',
@ -93,6 +94,7 @@ export default {
},
computed: {
...mapGetters([
'convertedDisscussionIds',
'getNoteableData',
'nextUnresolvedDiscussionId',
'unresolvedDiscussionsCount',
@ -245,6 +247,12 @@ export default {
}
},
},
created() {
eventHub.$on('startReplying', this.onStartReplying);
},
beforeDestroy() {
eventHub.$off('startReplying', this.onStartReplying);
},
methods: {
...mapActions([
'saveNote',
@ -252,6 +260,7 @@ export default {
'removePlaceholderNotes',
'toggleResolveNote',
'expandDiscussion',
'removeConvertedDiscussion',
]),
truncateSha,
componentName(note) {
@ -291,6 +300,10 @@ export default {
}
}
if (this.convertedDisscussionIds.includes(this.discussion.id)) {
this.removeConvertedDiscussion(this.discussion.id);
}
this.isReplying = false;
this.resetAutoSave();
},
@ -301,6 +314,10 @@ export default {
note: { note: noteText },
};
if (this.convertedDisscussionIds.includes(this.discussion.id)) {
postData.return_discussion = true;
}
if (this.discussion.for_commit) {
postData.note_project_id = this.discussion.project_id;
}
@ -340,6 +357,11 @@ Please check your network connection and try again.`;
deleteNoteHandler(note) {
this.$emit('noteDeleted', this.discussion, note);
},
onStartReplying(discussionId) {
if (this.discussion.id === discussionId) {
this.showReplyForm();
}
},
},
};
</script>
@ -358,30 +380,32 @@ Please check your network connection and try again.`;
:img-size="40"
/>
</div>
<note-header
:author="author"
:created-at="initialDiscussion.created_at"
:note-id="initialDiscussion.id"
:include-toggle="true"
:expanded="discussion.expanded"
@toggleHandler="toggleDiscussionHandler"
>
<span v-html="actionText"></span>
</note-header>
<note-edited-text
v-if="discussion.resolved"
:edited-at="discussion.resolved_at"
:edited-by="discussion.resolved_by"
:action-text="resolvedText"
class-name="discussion-headline-light js-discussion-headline"
/>
<note-edited-text
v-else-if="lastUpdatedAt"
:edited-at="lastUpdatedAt"
:edited-by="lastUpdatedBy"
action-text="Last updated"
class-name="discussion-headline-light js-discussion-headline"
/>
<div class="timeline-content">
<note-header
:author="author"
:created-at="initialDiscussion.created_at"
:note-id="initialDiscussion.id"
:include-toggle="true"
:expanded="discussion.expanded"
@toggleHandler="toggleDiscussionHandler"
>
<span v-html="actionText"></span>
</note-header>
<note-edited-text
v-if="discussion.resolved"
:edited-at="discussion.resolved_at"
:edited-by="discussion.resolved_by"
:action-text="resolvedText"
class-name="discussion-headline-light js-discussion-headline"
/>
<note-edited-text
v-else-if="lastUpdatedAt"
:edited-at="lastUpdatedAt"
:edited-by="lastUpdatedBy"
action-text="Last updated"
class-name="discussion-headline-light js-discussion-headline"
/>
</div>
</div>
<div v-if="shouldShowDiscussions" class="discussion-body">
<component
@ -400,6 +424,7 @@ Please check your network connection and try again.`;
:help-page-path="helpPagePath"
:show-reply-button="canReply"
@handleDeleteNote="deleteNoteHandler"
@startReplying="showReplyForm"
>
<note-edited-text
v-if="discussion.resolved"

View file

@ -29,11 +29,6 @@ export default {
type: Object,
required: true,
},
discussion: {
type: Object,
required: false,
default: null,
},
line: {
type: Object,
required: false,
@ -49,6 +44,11 @@ export default {
required: false,
default: () => null,
},
showReplyButton: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@ -91,13 +91,6 @@ export default {
}
return '';
},
showReplyButton() {
if (!this.discussion || !this.getNoteableData.current_user.can_create_note) {
return false;
}
return this.discussion.individual_note && !this.commentsDisabled;
},
actionText() {
if (!this.commit) {
return '';
@ -260,10 +253,10 @@ export default {
:is-resolved="note.resolved"
:is-resolving="isResolving"
:resolved-by="note.resolved_by"
:discussion-id="discussionId"
@handleEdit="editHandler"
@handleDelete="deleteHandler"
@handleResolve="resolveHandler"
@startReplying="$emit('startReplying')"
/>
</div>
<div class="timeline-discussion-body">

View file

@ -60,9 +60,11 @@ export default {
...mapGetters([
'isNotesFetched',
'discussions',
'convertedDisscussionIds',
'getNotesDataByProp',
'isLoading',
'commentsDisabled',
'getNoteableData',
]),
noteableType() {
return this.noteableData.noteableType;
@ -78,6 +80,9 @@ export default {
return this.discussions;
},
canReply() {
return this.getNoteableData.current_user.can_create_note && !this.commentsDisabled;
},
},
watch: {
shouldShow() {
@ -128,6 +133,7 @@ export default {
'setNotesFetchedState',
'expandDiscussion',
'startTaskList',
'convertToDiscussion',
]),
fetchNotes() {
if (this.isFetching) return null;
@ -175,6 +181,11 @@ export default {
}
}
},
startReplying(discussionId) {
return this.convertToDiscussion(discussionId)
.then(() => this.$nextTick())
.then(() => eventHub.$emit('startReplying', discussionId));
},
},
systemNote: constants.SYSTEM_NOTE,
};
@ -193,7 +204,9 @@ export default {
/>
<placeholder-note v-else :key="discussion.id" :note="discussion.notes[0]" />
</template>
<template v-else-if="discussion.individual_note">
<template
v-else-if="discussion.individual_note && !convertedDisscussionIds.includes(discussion.id)"
>
<system-note
v-if="discussion.notes[0].system"
:key="discussion.id"
@ -203,7 +216,8 @@ export default {
v-else
:key="discussion.id"
:note="discussion.notes[0]"
:discussion="discussion"
:show-reply-button="canReply"
@startReplying="startReplying(discussion.id)"
/>
</template>
<noteable-discussion

View file

@ -83,12 +83,44 @@ export const updateNote = ({ commit, dispatch }, { endpoint, note }) =>
dispatch('startTaskList');
});
export const replyToDiscussion = ({ commit }, { endpoint, data }) =>
export const updateOrCreateNotes = ({ commit, state, getters, dispatch }, notes) => {
const { notesById } = getters;
notes.forEach(note => {
if (notesById[note.id]) {
commit(types.UPDATE_NOTE, note);
} else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) {
const discussion = utils.findNoteObjectById(state.discussions, note.discussion_id);
if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
} else if (note.type === constants.DIFF_NOTE) {
dispatch('fetchDiscussions', { path: state.notesData.discussionsPath });
} else {
commit(types.ADD_NEW_NOTE, note);
}
} else {
commit(types.ADD_NEW_NOTE, note);
}
});
};
export const replyToDiscussion = ({ commit, state, getters, dispatch }, { endpoint, data }) =>
service
.replyToDiscussion(endpoint, data)
.then(res => res.json())
.then(res => {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
if (res.discussion) {
commit(types.UPDATE_DISCUSSION, res.discussion);
updateOrCreateNotes({ commit, state, getters, dispatch }, res.discussion.notes);
dispatch('updateMergeRequestWidget');
dispatch('startTaskList');
dispatch('updateResolvableDiscussonsCounts');
} else {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
}
return res;
});
@ -262,25 +294,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
if (resp.notes && resp.notes.length) {
const { notesById } = getters;
resp.notes.forEach(note => {
if (notesById[note.id]) {
commit(types.UPDATE_NOTE, note);
} else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) {
const discussion = utils.findNoteObjectById(state.discussions, note.discussion_id);
if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
} else if (note.type === constants.DIFF_NOTE) {
dispatch('fetchDiscussions', { path: state.notesData.discussionsPath });
} else {
commit(types.ADD_NEW_NOTE, note);
}
} else {
commit(types.ADD_NEW_NOTE, note);
}
});
updateOrCreateNotes({ commit, state, getters, dispatch }, resp.notes);
dispatch('startTaskList');
}
@ -429,5 +443,8 @@ export const submitSuggestion = (
export const convertToDiscussion = ({ commit }, noteId) =>
commit(types.CONVERT_TO_DISCUSSION, noteId);
export const removeConvertedDiscussion = ({ commit }, noteId) =>
commit(types.REMOVE_CONVERTED_DISCUSSION, noteId);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View file

@ -4,6 +4,8 @@ import { collapseSystemNotes } from './collapse_utils';
export const discussions = state => collapseSystemNotes(state.discussions);
export const convertedDisscussionIds = state => state.convertedDisscussionIds;
export const targetNoteHash = state => state.targetNoteHash;
export const getNotesData = state => state.notesData;

View file

@ -5,6 +5,7 @@ import mutations from '../mutations';
export default () => ({
state: {
discussions: [],
convertedDisscussionIds: [],
targetNoteHash: null,
lastFetchedAt: null,

View file

@ -18,6 +18,7 @@ export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE';
export const DISABLE_COMMENTS = 'DISABLE_COMMENTS';
export const APPLY_SUGGESTION = 'APPLY_SUGGESTION';
export const CONVERT_TO_DISCUSSION = 'CONVERT_TO_DISCUSSION';
export const REMOVE_CONVERTED_DISCUSSION = 'REMOVE_CONVERTED_DISCUSSION';
// DISCUSSION
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';

View file

@ -266,7 +266,14 @@ export default {
},
[types.CONVERT_TO_DISCUSSION](state, discussionId) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
Object.assign(discussion, { individual_note: false });
const convertedDisscussionIds = [...state.convertedDisscussionIds, discussionId];
Object.assign(state, { convertedDisscussionIds });
},
[types.REMOVE_CONVERTED_DISCUSSION](state, discussionId) {
const convertedDisscussionIds = [...state.convertedDisscussionIds];
convertedDisscussionIds.splice(convertedDisscussionIds.indexOf(discussionId), 1);
Object.assign(state, { convertedDisscussionIds });
},
};

View file

@ -0,0 +1,7 @@
import mountImportProjectsTable from '~/import_projects';
document.addEventListener('DOMContentLoaded', () => {
const mountElement = document.getElementById('import-projects-mount-element');
mountImportProjectsTable(mountElement);
});

View file

@ -0,0 +1,7 @@
import mountImportProjectsTable from '~/import_projects';
document.addEventListener('DOMContentLoaded', () => {
const mountElement = document.getElementById('import-projects-mount-element');
mountImportProjectsTable(mountElement);
});

View file

@ -1,5 +1,6 @@
import $ from 'jquery';
import UsernameValidator from './username_validator';
import NoEmojiValidator from '../../../emoji/no_emoji_validator';
import SigninTabsMemoizer from './signin_tabs_memoizer';
import OAuthRememberMe from './oauth_remember_me';
import preserveUrlFragment from './preserve_url_fragment';
@ -7,6 +8,7 @@ import preserveUrlFragment from './preserve_url_fragment';
document.addEventListener('DOMContentLoaded', () => {
new UsernameValidator(); // eslint-disable-line no-new
new SigninTabsMemoizer(); // eslint-disable-line no-new
new NoEmojiValidator(); // eslint-disable-line no-new
new OAuthRememberMe({
container: $('.omniauth-container'),

View file

@ -59,17 +59,19 @@ export default {
</script>
<template>
<div class="btn-group">
<gl-button
<button
v-gl-tooltip
type="button"
:disabled="isLoading"
class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions"
title="Manual job"
:title="__('Manual job')"
data-toggle="dropdown"
aria-label="Manual job"
:aria-label="__('Manual job')"
>
<icon name="play" class="icon-play" /> <i class="fa fa-caret-down" aria-hidden="true"> </i>
<icon name="play" class="icon-play" />
<i class="fa fa-caret-down" aria-hidden="true"></i>
<gl-loading-icon v-if="isLoading" />
</gl-button>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li v-for="action in actions" :key="action.path">

View file

@ -1,5 +1,5 @@
<script>
import { GlLink, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
export default {
@ -9,7 +9,6 @@ export default {
components: {
Icon,
GlLink,
GlButton,
},
props: {
artifacts: {
@ -21,20 +20,22 @@ export default {
</script>
<template>
<div class="btn-group" role="group">
<gl-button
<button
v-gl-tooltip
class="dropdown-toggle build-artifacts js-pipeline-dropdown-download"
title="Artifacts"
type="button"
class="dropdown-toggle build-artifacts btn btn-default js-pipeline-dropdown-download"
:title="__('Artifacts')"
data-toggle="dropdown"
aria-label="Artifacts"
:aria-label="__('Artifacts')"
>
<icon name="download" /> <i class="fa fa-caret-down" aria-hidden="true"> </i>
</gl-button>
<icon name="download" />
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li v-for="(artifact, i) in artifacts" :key="i">
<gl-link :href="artifact.path" rel="nofollow" download>
Download {{ artifact.name }} artifacts
</gl-link>
<gl-link :href="artifact.path" rel="nofollow" download
>Download {{ artifact.name }} artifacts</gl-link
>
</li>
</ul>
</div>

View file

@ -73,14 +73,14 @@ export default {
<gl-button
:aria-label="ariaLabel"
variant="blank"
class="commit-edit-toggle mr-2"
class="commit-edit-toggle square s24 mr-2"
@click.stop="toggle()"
>
<icon :name="collapseIcon" :size="16" />
</gl-button>
<span v-if="expanded">{{ __('Collapse') }}</span>
<span v-else>
<span v-html="message"></span>
<span class="vertical-align-middle" v-html="message"></span>
<gl-button variant="link" class="modify-message-button">
{{ modifyLinkMessage }}
</gl-button>

View file

@ -37,6 +37,11 @@ export default {
required: false,
default: 12,
},
isCentered: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
changedIcon() {
@ -78,7 +83,12 @@ export default {
</script>
<template>
<span v-gl-tooltip.right :title="tooltipTitle" class="file-changed-icon ml-auto">
<span
v-gl-tooltip.right
:title="tooltipTitle"
:class="{ 'ml-auto': isCentered }"
class="file-changed-icon"
>
<icon v-if="showIcon" :name="changedIcon" :size="size" :css-classes="changedIconClass" />
</span>
</template>

View file

@ -46,6 +46,11 @@ export default {
required: false,
default: false,
},
cssClasses: {
type: String,
required: false,
default: '',
},
},
computed: {
cssClass() {
@ -59,5 +64,5 @@ export default {
};
</script>
<template>
<span :class="cssClass"> <icon :name="icon" :size="size" /> </span>
<span :class="cssClass"> <icon :name="icon" :size="size" :css-classes="cssClasses" /> </span>
</template>

View file

@ -1,6 +1,5 @@
<script>
import { diffModes } from '~/ide/constants';
import { viewerInformationForPath } from '../content_viewer/lib/viewer_utils';
import { diffViewerModes, diffModes } from '~/ide/constants';
import ImageDiffViewer from './viewers/image_diff_viewer.vue';
import DownloadDiffViewer from './viewers/download_diff_viewer.vue';
import RenamedFile from './viewers/renamed.vue';
@ -12,6 +11,10 @@ export default {
type: String,
required: true,
},
diffViewerMode: {
type: String,
required: true,
},
newPath: {
type: String,
required: true,
@ -46,7 +49,7 @@ export default {
},
computed: {
viewer() {
if (this.diffMode === diffModes.renamed) {
if (this.diffViewerMode === diffViewerModes.renamed) {
return RenamedFile;
} else if (this.diffMode === diffModes.mode_changed) {
return ModeChanged;
@ -54,11 +57,8 @@ export default {
if (!this.newPath) return null;
const previewInfo = viewerInformationForPath(this.newPath);
if (!previewInfo) return DownloadDiffViewer;
switch (previewInfo.id) {
case 'image':
switch (this.diffViewerMode) {
case diffViewerModes.image:
return ImageDiffViewer;
default:
return DownloadDiffViewer;

View file

@ -0,0 +1,5 @@
<template>
<div class="nothing-here-block">
{{ __('No preview for this file type') }}
</div>
</template>

View file

@ -0,0 +1,5 @@
<template>
<div class="nothing-here-block">
{{ __('This diff was suppressed by a .gitattributes entry.') }}
</div>
</template>

View file

@ -136,6 +136,7 @@ export default {
<div
v-else
:class="fileClass"
:title="file.name"
class="file-row"
role="button"
@click="clickFile"

View file

@ -130,6 +130,6 @@ export default {
<template>
<div>
<div class="flash-container js-suggestions-flash"></div>
<div v-show="isRendered" ref="container" class="note-text md" v-html="noteHtml"></div>
<div v-show="isRendered" ref="container" v-html="noteHtml"></div>
</div>
</template>

View file

@ -28,11 +28,12 @@ export default {
data() {
return {
size: this.startSize,
isDragging: false,
};
},
computed: {
className() {
return `drag-${this.side}`;
return [`position-${this.side}-0`, { 'is-dragging': this.isDragging }];
},
cursorStyle() {
if (this.enabled) {
@ -57,6 +58,7 @@ export default {
startDrag(e) {
if (this.enabled) {
e.preventDefault();
this.isDragging = true;
this.startPos = e.clientX;
this.currentStartSize = this.size;
document.addEventListener('mousemove', this.drag);
@ -80,6 +82,7 @@ export default {
},
endDrag(e) {
e.preventDefault();
this.isDragging = false;
document.removeEventListener('mousemove', this.drag);
this.$emit('resize-end', this.size);
},
@ -91,7 +94,7 @@ export default {
<div
:class="className"
:style="cursorStyle"
class="drag-handle"
class="position-absolute position-top-0 position-bottom-0 drag-handle"
@mousedown="startDrag"
@dblclick="resetSize"
></div>

View file

@ -0,0 +1,34 @@
<script>
import $ from 'jquery';
export default {
name: 'Select2Select',
props: {
options: {
type: Object,
required: false,
default: () => ({}),
},
value: {
type: String,
required: false,
default: '',
},
},
mounted() {
$(this.$refs.dropdownInput)
.val(this.value)
.select2(this.options)
.on('change', event => this.$emit('input', event.target.value));
},
beforeDestroy() {
$(this.$refs.dropdownInput).select2('destroy');
},
};
</script>
<template>
<input ref="dropdownInput" type="hidden" />
</template>

View file

@ -63,15 +63,15 @@
//
// Pass in any number of transitions
@mixin transition($transitions...) {
$unfoldedTransitions: ();
$unfolded-transitions: ();
@each $transition in $transitions {
$unfoldedTransitions: append($unfoldedTransitions, unfoldTransition($transition), comma);
$unfolded-transitions: append($unfolded-transitions, unfold-transition($transition), comma);
}
transition: $unfoldedTransitions;
transition: $unfolded-transitions;
}
@mixin disableAllAnimation {
@mixin disable-all-animation {
/*CSS transitions*/
-o-transition-property: none !important;
-moz-transition-property: none !important;
@ -92,27 +92,27 @@
animation: none !important;
}
@function unfoldTransition ($transition) {
@function unfold-transition ($transition) {
// Default values
$property: all;
$duration: $general-hover-transition-duration;
$easing: $general-hover-transition-curve; // Browser default is ease, which is what we want
$delay: null; // Browser default is 0, which is what we want
$defaultProperties: ($property, $duration, $easing, $delay);
$default-properties: ($property, $duration, $easing, $delay);
// Grab transition properties if they exist
$unfoldedTransition: ();
@for $i from 1 through length($defaultProperties) {
$unfolded-transition: ();
@for $i from 1 through length($default-properties) {
$p: null;
@if $i <= length($transition) {
$p: nth($transition, $i);
} @else {
$p: nth($defaultProperties, $i);
$p: nth($default-properties, $i);
}
$unfoldedTransition: append($unfoldedTransition, $p);
$unfolded-transition: append($unfolded-transition, $p);
}
@return $unfoldedTransition;
@return $unfolded-transition;
}
.btn {

View file

@ -15,7 +15,7 @@
margin-top: 3px;
padding: $gl-padding;
z-index: 300;
width: 300px;
width: $award-emoji-width;
font-size: 14px;
background-color: $white-light;
border: 1px solid $border-white-light;
@ -55,6 +55,10 @@
transform: none;
}
}
@include media-breakpoint-down(xs) {
width: $award-emoji-width-xs;
}
}
.emoji-search {
@ -229,10 +233,10 @@
height: $default-icon-size;
width: $default-icon-size;
border-radius: 50%;
}
path {
fill: $border-gray-normal;
}
path {
fill: $border-gray-normal;
}
}
@ -243,6 +247,10 @@
left: 10px;
bottom: 6px;
opacity: 0;
path {
fill: $award-emoji-positive-add-lines;
}
}
.award-control-text {

View file

@ -166,7 +166,8 @@
@include btn-outline($white-light, $green-600, $green-500, $green-500, $white-light, $green-600, $green-600, $green-700);
}
&.btn-remove {
&.btn-remove,
&.btn-danger {
@include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700);
}

View file

@ -48,6 +48,10 @@
color: $brand-info;
}
.text-break-word {
word-break: break-all;
}
.hint { font-style: italic; color: $gl-gray-400; }
.light { color: $gl-text-color; }
@ -442,3 +446,15 @@ img.emoji {
.position-left-0 { left: 0; }
.position-right-0 { right: 0; }
.position-top-0 { top: 0; }
.drag-handle {
width: 4px;
&:hover {
background-color: $white-normal;
}
&.is-dragging {
background-color: $gray-600;
}
}

View file

@ -565,15 +565,14 @@
}
.navbar-empty {
justify-content: center;
height: $header-height;
background: $white-light;
border-bottom: 1px solid $white-normal;
.mx-auto {
.tanuki-logo,
img {
height: 36px;
}
.tanuki-logo,
.brand-header-logo {
max-height: 100%;
}
}

View file

@ -228,7 +228,7 @@
.cur {
.avatar {
@include disableAllAnimation;
@include disable-all-animation;
border: 1px solid $white-light;
}
}

View file

@ -36,10 +36,6 @@
width: fit-content;
}
tbody {
background-color: $white-light;
}
tr {
th {
border-bottom: solid 2px $gl-gray-100;

View file

@ -111,10 +111,11 @@ body.modal-open {
flex-grow: 1;
height: 56px;
padding: $gl-btn-padding $gl-btn-padding 0;
text-align: right;
> svg {
float: right;
height: 100%;
.illustration {
height: inherit;
width: initial;
}
}
}

View file

@ -49,13 +49,6 @@
word-wrap: normal;
}
// Multi-line code blocks should scroll horizontally
pre {
code {
white-space: pre;
}
}
kbd {
display: inline-block;
padding: 3px 5px;
@ -166,6 +159,10 @@
overflow-x: auto;
border-radius: 2px;
// Multi-line code blocks should scroll horizontally
code {
white-space: pre;
}
&.plain-readme {
background: none;
@ -303,11 +300,10 @@ body {
}
.page-title-empty {
margin-top: 0;
margin: 12px 0;
line-height: 1.3;
font-size: 1.25em;
font-weight: $gl-font-weight-bold;
margin: 12px 0;
}
h1,

View file

@ -251,7 +251,7 @@ $gl-padding-top: 10px;
$gl-sidebar-padding: 22px;
$gl-bar-padding: 3px;
$input-horizontal-padding: 12px;
$browserScrollbarSize: 10px;
$browser-scrollbar-size: 10px;
/*
* Misc
@ -405,6 +405,8 @@ $status-icon-size: 22px;
$award-emoji-menu-shadow: rgba(0, 0, 0, 0.175);
$award-emoji-positive-add-bg: #fed159;
$award-emoji-positive-add-lines: #bb9c13;
$award-emoji-width: 376px;
$award-emoji-width-xs: 300px;
/*
* Search Box

View file

@ -125,7 +125,7 @@ $dark-il: #de935f;
.diff-line-num.new,
.line_content.new {
@include diff_background($dark-new-bg, $dark-new-idiff, $dark-border);
@include diff-background($dark-new-bg, $dark-new-idiff, $dark-border);
&::before,
a {
@ -135,7 +135,7 @@ $dark-il: #de935f;
.diff-line-num.old,
.line_content.old {
@include diff_background($dark-old-bg, $dark-old-idiff, $dark-border);
@include diff-background($dark-old-bg, $dark-old-idiff, $dark-border);
&::before,
a {

View file

@ -125,7 +125,7 @@ $monokai-gi: #a6e22e;
.diff-line-num.new,
.line_content.new {
@include diff_background($monokai-new-bg, $monokai-new-idiff, $monokai-diff-border);
@include diff-background($monokai-new-bg, $monokai-new-idiff, $monokai-diff-border);
&::before,
a {
@ -135,7 +135,7 @@ $monokai-gi: #a6e22e;
.diff-line-num.old,
.line_content.old {
@include diff_background($monokai-old-bg, $monokai-old-idiff, $monokai-diff-border);
@include diff-background($monokai-old-bg, $monokai-old-idiff, $monokai-diff-border);
&::before,
a {

View file

@ -4,7 +4,7 @@
@mixin matchLine {
@mixin match-line {
color: $black-transparent;
background-color: $white-normal;
}
@ -45,7 +45,7 @@
&.match .line_content,
.new-nonewline.line_content,
.old-nonewline.line_content {
@include matchLine;
@include match-line;
}
.diff-line-num {
@ -121,7 +121,7 @@
}
&.match {
@include matchLine;
@include match-line;
}
&.hll:not(.empty-cell) {

View file

@ -129,7 +129,7 @@ $solarized-dark-il: #2aa198;
.diff-line-num.new,
.line_content.new {
@include diff_background($solarized-dark-new-bg, $solarized-dark-new-idiff, $solarized-dark-border);
@include diff-background($solarized-dark-new-bg, $solarized-dark-new-idiff, $solarized-dark-border);
&::before,
a {
@ -139,7 +139,7 @@ $solarized-dark-il: #2aa198;
.diff-line-num.old,
.line_content.old {
@include diff_background($solarized-dark-old-bg, $solarized-dark-old-idiff, $solarized-dark-border);
@include diff-background($solarized-dark-old-bg, $solarized-dark-old-idiff, $solarized-dark-border);
&::before,
a {

View file

@ -90,7 +90,7 @@ $solarized-light-vg: #268bd2;
$solarized-light-vi: #268bd2;
$solarized-light-il: #2aa198;
@mixin matchLine {
@mixin match-line {
color: $black-transparent;
background: $solarized-light-matchline-bg;
}
@ -125,7 +125,7 @@ $solarized-light-il: #2aa198;
&.match .line_content,
&.old-nonewline .line_content,
&.new-nonewline .line_content {
@include matchLine;
@include match-line;
}
td.diff-line-num.hll:not(.empty-cell),
@ -136,7 +136,7 @@ $solarized-light-il: #2aa198;
.diff-line-num.new,
.line_content.new {
@include diff_background($solarized-light-new-bg,
@include diff-background($solarized-light-new-bg,
$solarized-light-new-idiff, $solarized-light-border);
&::before,
@ -147,7 +147,7 @@ $solarized-light-il: #2aa198;
.diff-line-num.old,
.line_content.old {
@include diff_background($solarized-light-old-bg, $solarized-light-old-idiff, $solarized-light-border);
@include diff-background($solarized-light-old-bg, $solarized-light-old-idiff, $solarized-light-border);
&::before,
a {
@ -168,7 +168,7 @@ $solarized-light-il: #2aa198;
}
.line_content.match {
@include matchLine;
@include match-line;
}
&:not(.diff-expanded) + .diff-expanded,

View file

@ -70,7 +70,7 @@ $white-gc-color: #999;
$white-gc-bg: #eaf2f5;
@mixin matchLine {
@mixin match-line {
color: $black-transparent;
background-color: $gray-light;
}
@ -105,7 +105,7 @@ pre.code,
&.match .line_content,
.new-nonewline.line_content,
.old-nonewline.line_content {
@include matchLine;
@include match-line;
}
.diff-line-num {
@ -185,7 +185,7 @@ pre.code,
}
&.match {
@include matchLine;
@include match-line;
}
&.hll:not(.empty-cell) {

View file

@ -682,25 +682,6 @@ $ide-commit-header-height: 48px;
flex: 1;
}
.drag-handle {
position: absolute;
top: 0;
bottom: 0;
width: 4px;
&:hover {
background-color: $white-normal;
}
&.drag-right {
right: 0;
}
&.drag-left {
left: 0;
}
}
.ide-commit-list-container {
display: flex;
flex: 1;

View file

@ -164,6 +164,13 @@
display: none;
}
}
&:not(.is-collapsed) {
.board-list-component {
display: flex;
flex-direction: column;
}
}
}
.board-inner {

View file

@ -11,15 +11,24 @@
}
.divergence-graph {
$graph-side-width: 80px;
$graph-separator-width: 1px;
padding: 0 6px;
.graph-side {
position: relative;
width: 80px;
width: $graph-side-width;
height: 22px;
padding: 5px 0 13px;
float: left;
&.full {
width: $graph-side-width * 2 + $graph-separator-width;
display: flex;
justify-content: center;
}
.bar {
position: absolute;
height: 4px;
@ -57,7 +66,7 @@
.graph-separator {
position: relative;
width: 1px;
width: $graph-separator-width;
height: 18px;
margin: 5px 0 0;
float: left;

View file

@ -34,7 +34,6 @@
.detail-page-header-actions {
align-self: center;
flex-shrink: 0;
flex: 0 0 auto;
@include media-breakpoint-down(xs) {

View file

@ -602,7 +602,7 @@
}
}
@mixin diff_background($background, $idiff, $border) {
@mixin diff-background($background, $idiff, $border) {
background: $background;
&.line_content span.idiff {
@ -1038,12 +1038,30 @@
}
.diff-tree-list {
width: 320px;
position: -webkit-sticky;
position: sticky;
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px;
top: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px;
max-height: calc(100vh - #{$top-pos});
padding-right: $gl-padding;
z-index: 202;
.with-performance-bar & {
$performance-bar-top-pos: $performance-bar-height + $top-pos;
top: $performance-bar-top-pos;
max-height: calc(100vh - #{$performance-bar-top-pos});
}
.drag-handle {
bottom: 16px;
transform: translateX(-6px);
}
}
.diff-files-holder {
flex: 1;
min-width: 0;
z-index: 201;
}
.compare-versions-container {
@ -1051,23 +1069,12 @@
}
.tree-list-holder {
position: -webkit-sticky;
position: sticky;
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px;
top: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px;
max-height: calc(100vh - #{$top-pos});
padding-right: $gl-padding;
height: 100%;
.file-row {
margin-left: 0;
margin-right: 0;
}
.with-performance-bar & {
$performance-bar-top-pos: $performance-bar-height + $top-pos;
top: $performance-bar-top-pos;
max-height: calc(100vh - #{$performance-bar-top-pos});
}
}
.tree-list-scroll {

View file

@ -182,9 +182,8 @@
.template-selector-dropdowns-wrap {
display: inline-block;
margin-left: 8px;
vertical-align: top;
margin: 5px 0 0 8px;
vertical-align: top;
@media(max-width: map-get($grid-breakpoints, md)-1) {
display: block;

View file

@ -1,20 +1,51 @@
.import-jobs-from-col,
.import-jobs-to-col {
width: 40%;
width: 39%;
}
.import-jobs-status-col {
width: 20%;
width: 15%;
}
.btn-import {
.loading-icon {
display: none;
}
.import-jobs-cta-col {
width: 1%;
}
&.is-loading {
.loading-icon {
display: inline-block;
}
.import-project-name-input {
border-radius: 0 $border-radius-default $border-radius-default 0;
position: relative;
left: -1px;
max-width: 300px;
}
.import-namespace-select {
width: auto !important;
> .select2-choice {
border-radius: $border-radius-default 0 0 $border-radius-default;
position: relative;
left: 1px;
}
}
.import-slash-divider {
background-color: $gray-lightest;
border: 1px solid $border-color;
}
.import-row {
height: 55px;
}
.import-table {
.import-jobs-from-col,
.import-jobs-to-col,
.import-jobs-status-col,
.import-jobs-cta-col {
border-bottom-width: 1px;
padding-left: $gl-padding;
}
}
.import-projects-loading-icon {
margin-top: $gl-padding-32;
}

View file

@ -735,9 +735,11 @@
.mr-version-controls {
position: relative;
z-index: 103;
z-index: 203;
background: $gray-light;
color: $gl-text-color;
margin-top: -1px;
border-top: 1px solid $border-color;
.mr-version-menus-container {
display: flex;
@ -789,7 +791,6 @@
position: sticky;
top: $header-height + $mr-tabs-height;
width: 100%;
border-top: 1px solid $border-color;
&.is-fileTreeOpen {
margin-left: -16px;
@ -808,12 +809,9 @@
.merge-request-tabs-holder {
top: $header-height;
z-index: 200;
z-index: 300;
background-color: $white-light;
@include media-breakpoint-down(md) {
border-bottom: 1px solid $border-color;
}
border-bottom: 1px solid $border-color;
@include media-breakpoint-up(sm) {
position: sticky;
@ -1019,3 +1017,8 @@
z-index: 99999;
background: $black-transparent;
}
.source-branch-removal-status {
padding-left: 50px;
padding-bottom: $gl-padding;
}

View file

@ -494,11 +494,6 @@ $note-form-margin-left: 72px;
.discussion-notes {
margin-left: 0;
border-left: 0;
.notes {
position: relative;
@include vertical-line(52px);
}
}
.note-wrapper {
@ -550,6 +545,11 @@ $note-form-margin-left: 72px;
.note-header-info {
padding-bottom: 0;
}
.timeline-content {
overflow-x: auto;
overflow-y: hidden;
}
}
.unresolved {
@ -597,7 +597,6 @@ $note-form-margin-left: 72px;
.note-headline-meta {
display: inline-block;
white-space: nowrap;
.system-note-message {
white-space: normal;
@ -607,6 +606,10 @@ $note-form-margin-left: 72px;
color: $gl-text-color-disabled;
}
.note-timestamp {
white-space: nowrap;
}
a:hover {
text-decoration: underline;
}

View file

@ -704,8 +704,8 @@
.scrolling-tabs-container {
.scrolling-tabs {
margin-top: $gl-padding-8;
margin-bottom: $gl-padding-8 - $browserScrollbarSize;
padding-bottom: $browserScrollbarSize;
margin-bottom: $gl-padding-8 - $browser-scrollbar-size;
padding-bottom: $browser-scrollbar-size;
flex-wrap: wrap;
border-bottom: 0;
}
@ -713,7 +713,7 @@
.fade-left,
.fade-right {
top: 0;
height: calc(100% - #{$browserScrollbarSize});
height: calc(100% - #{$browser-scrollbar-size});
.fa {
top: 50%;

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
class Admin::UsersController < Admin::ApplicationController
include RoutableActions
before_action :user, except: [:index, :new, :create]
before_action :check_impersonation_availability, only: :impersonate
@ -177,11 +179,13 @@ class Admin::UsersController < Admin::ApplicationController
user == current_user
end
# rubocop: disable CodeReuse/ActiveRecord
def user
@user ||= User.find_by!(username: params[:id])
@user ||= find_routable!(User, params[:id])
end
def build_canonical_path(user)
url_for(safe_params.merge(id: user.to_param))
end
# rubocop: enable CodeReuse/ActiveRecord
def redirect_back_or_admin_user(options = {})
redirect_back_or_default(default: default_route, options: options)

View file

@ -137,6 +137,8 @@ class ApplicationController < ActionController::Base
if response.status == 422 && response.body.present? && response.content_type == 'application/json'.freeze
payload[:response] = response.body
end
payload[:queue_duration] = request.env[::Gitlab::Middleware::RailsQueueDuration::GITLAB_RAILS_QUEUE_DURATION_KEY]
end
##

View file

@ -3,7 +3,7 @@
module SendFileUpload
def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, proxy: false, disposition: 'attachment')
if attachment
response_disposition = ::Gitlab::ContentDisposition.format(disposition: 'attachment', filename: attachment)
response_disposition = ::Gitlab::ContentDisposition.format(disposition: disposition, filename: attachment)
# Response-Content-Type will not override an existing Content-Type in
# Google Cloud Storage, so the metadata needs to be cleared on GCS for

View file

@ -25,8 +25,6 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
private
def group_milestones
groups = GroupsFinder.new(current_user, all_available: false).execute
DashboardGroupMilestone.build_collection(groups, params)
end
@ -45,6 +43,6 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
end
def groups
@groups ||= GroupsFinder.new(current_user, state_all: true).execute
@groups ||= GroupsFinder.new(current_user, all_available: false).execute
end
end

View file

@ -13,9 +13,10 @@ class HelpController < ApplicationController
# Remove YAML frontmatter so that it doesn't look weird
@help_index = File.read(Rails.root.join('doc', 'README.md')).sub(YAML_FRONT_MATTER_REGEXP, '')
# Prefix Markdown links with `help/` unless they are external links
# See http://rubular.com/r/X3baHTbPO2
@help_index.gsub!(%r{(?<delim>\]\()(?!.+://)(?!/)(?<link>[^\)\(]+\))}) do
# Prefix Markdown links with `help/` unless they are external links.
# '//' not necessarily part of URL, e.g., mailto:mail@example.com
# See https://rubular.com/r/DFHZl5w8d3bpzV
@help_index.gsub!(%r{(?<delim>\]\()(?!\w+:)(?!/)(?<link>[^\)\(]+\))}) do
"#{$~[:delim]}#{Gitlab.config.gitlab.relative_url_root}/help/#{$~[:link]}"
end
end

Some files were not shown because too many files have changed in this diff Show more