Commit 07c32a0d authored by Mike Lewis's avatar Mike Lewis

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

# Conflicts:
#   .gitlab/merge_request_templates/Documentation.md
parents f1645c74 fe10964a
......@@ -509,6 +509,7 @@ rspec-mysql:
parallel: 50
.rspec-quarantine: &rspec-quarantine
retry: 0
script:
- export CACHE_CLASSES=true
- scripts/gitaly-test-spawn
......
......@@ -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
......@@ -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
{
"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,
}
}
......@@ -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')
1.19.0
1.20.0
8.3.0
8.3.1
......@@ -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
......
......@@ -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)
......
......@@ -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
......
......@@ -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.
......
......@@ -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>
......
......@@ -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">
......
<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);
},
diffViewerMode() {
return this.diffFile.viewer.name;
},
isTextFile() {
return this.diffFile.viewer.name === 'text';
return this.diffViewerMode === diffViewerModes.text;
},
noPreview() {
return this.diffViewerMode === diffViewerModes.no_preview;
},
errorMessage() {
return this.diffFile.viewer.error;
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>
......@@ -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>
......
......@@ -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>
......
......@@ -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"
......
......@@ -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;
......@@ -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 () => {};
......@@ -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);
......
......@@ -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,
};
......
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
);
};
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');
}
}
......@@ -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;
}
......
......@@ -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"
......
......@@ -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"
......
......@@ -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 },
......
<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>
<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>
<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>
<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>
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;
import Vue from 'vue';
export default new Vue();
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 } });
},
});
}
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 () => {};
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;
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,
});
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';
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);
});
},
};
export default () => ({
reposPath: '',
importPath: '',
jobsPath: '',
currentProjectId: '',
provider: '',
currentUsername: '',
importedProjects: [],
providerRepos: [],
namespaces: [],
reposBeingImported: [],
isLoadingRepos: false,
canSelectNamespace: false,
ciCdOnly: false,
});
......@@ -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"
......
......@@ -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
);
};
......
......@@ -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"
......
......@@ -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>
......
<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>
......
......@@ -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"
......
......@@ -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"
......
......@@ -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">
......
......@@ -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
......
......@@ -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 () => {};
......@@ -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;
......
......@@ -5,6 +5,7 @@ import mutations from '../mutations';
export default () => ({
state: {
discussions: [],
convertedDisscussionIds: [],
targetNoteHash: null,
lastFetchedAt: null,
......
......@@ -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';
......
......@@ -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 });
},
};
import mountImportProjectsTable from '~/import_projects';
document.addEventListener('DOMContentLoaded', () => {
const mountElement = document.getElementById('import-projects-mount-element');
mountImportProjectsTable(mountElement);
});
import mountImportProjectsTable from '~/import_projects';
document.addEventListener('DOMContentLoaded', () => {
const mountElement = document.getElementById('import-projects-mount-element');
mountImportProjectsTable(mountElement);
});
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'),
......
......@@ -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">
......
<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>
......
......@@ -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>
......
......@@ -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>
......
......@@ -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>
<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;
......
<template>
<div class="nothing-here-block">
{{ __('No preview for this file type') }}
</div>
</template>
<template>
<div class="nothing-here-block">
{{ __('This diff was suppressed by a .gitattributes entry.') }}
</div>
</template>
......@@ -136,6 +136,7 @@ export default {
<div
v-else
:class="fileClass"
:title="file.name"
class="file-row"
role="button"
@click="clickFile"
......
......@@ -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>
......@@ -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>
......
<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>
......@@ -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 {
......
......@@ -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 {
......
......@@ -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);
}
......
......@@ -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;
}
}
......@@ -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%;
}
}
......
......@@ -228,7 +228,7 @@
.cur {
.avatar {
@include disableAllAnimation;
@include disable-all-animation;
border: 1px solid $white-light;
}
}
......
......@@ -36,10 +36,6 @@
width: fit-content;
}
tbody {
background-color: $white-light;
}
tr {
th {
border-bottom: solid 2px $gl-gray-100;
......
......@@ -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;
}
}
}
......
......@@ -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,
......
......@@ -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
......
......@@ -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 {
......
......@@ -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 {
......
......@@ -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) {
......
......@@ -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 {
......
......@@ -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,
......
......@@ -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) {
......
......@@ -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;
......
......@@ -164,6 +164,13 @@
display: none;
}
}
&:not(.is-collapsed) {
.board-list-component {
display: flex;
flex-direction: column;
}
}
}
.board-inner {
......
......@@ -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;
......
......@@ -34,7 +34,6 @@
.detail-page-header-actions {
align-self: center;
flex-shrink: 0;
flex: 0 0 auto;
@include media-breakpoint-down(xs) {
......
......@@ -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 {
......
......@@ -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;
......
.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%;
}
.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;
}
}
&.is-loading {
.loading-icon {
display: inline-block;
}
.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;
}
......@@ -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;
}
......@@ -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;
}
......
......@@ -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%;
......
# 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)
......
......@@ -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
##
......
......@@ -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
......
......@@ -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
......@@ -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
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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