Commit 5381985b authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'master-ce' into scheduled-manual-jobs

parents 9c13a512 dfb9ac3a
{
"presets": [["latest", { "es2015": { "modules": false } }], "stage-2"],
"env": {
"karma": {
"plugins": ["rewire"]
},
"coverage": {
"plugins": [
[
"istanbul",
{
"exclude": ["spec/javascripts/**/*", "app/assets/javascripts/locale/**/app.js"]
}
],
[
"transform-define",
{
"process.env.BABEL_ENV": "coverage"
}
],
"rewire"
]
}
}
}
const BABEL_ENV = process.env.BABEL_ENV || process.env.NODE_ENV || null;
const presets = [
[
'@babel/preset-env',
{
modules: false,
targets: {
ie: '11',
},
},
],
];
// include stage 3 proposals
const plugins = [
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-syntax-import-meta',
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-json-strings',
];
// add code coverage tooling if necessary
if (BABEL_ENV === 'coverage') {
plugins.push([
'babel-plugin-istanbul',
{
exclude: ['spec/javascripts/**/*', 'app/assets/javascripts/locale/**/app.js'],
},
]);
}
// add rewire support when running tests
if (BABEL_ENV === 'karma' || BABEL_ENV === 'coverage') {
plugins.push('babel-plugin-rewire');
}
module.exports = { presets, plugins };
......@@ -6,7 +6,8 @@
/doc/ @axil @marcia
# Frontend maintainers should see everything in `app/assets/`
app/assets/ @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann
app/assets/ @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann
*.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann
# Someone from the database team should review changes in `db/`
db/ @abrandl @NikolayS
......
......@@ -445,7 +445,6 @@ Style/Dir:
# Cop supports --auto-correct.
Style/EachWithObject:
Exclude:
- 'config/initializers/gollum.rb'
- 'lib/expand_variables.rb'
- 'lib/gitlab/ci/ansi2html.rb'
- 'lib/gitlab/ee_compat_check.rb'
......
......@@ -2,6 +2,20 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 11.3.2 (2018-10-03)
### Fixed (4 changes)
- Fix NULL pipeline import problem and pipeline user mapping issue. !21875
- Fix migration to avoid an exception during upgrade. !22055
- Fixes admin runners table not wrapping content.
- Fix Error 500 when forking projects with Gravatar disabled.
### Other (1 change)
- Removes the 'required' attribute from the 'project name' field. !21770
## 11.3.1 (2018-09-26)
### Security (6 changes)
......
......@@ -80,11 +80,9 @@ gem 'gitlab_omniauth-ldap', '~> 2.0.4', require: 'omniauth-ldap'
gem 'net-ldap'
# Git Wiki
# Required manually in config/initializers/gollum.rb to control load order
# Only used to compute wiki page slugs
gem 'gitlab-gollum-lib', '~> 4.2', require: false
gem 'gitlab-gollum-rugged_adapter', '~> 0.4.4', require: false
# Language detection
gem 'github-linguist', '~> 5.3.3', require: 'linguist'
......@@ -134,6 +132,7 @@ gem 'seed-fu', '~> 2.3.7'
gem 'html-pipeline', '~> 2.8'
gem 'deckar01-task_list', '2.0.0'
gem 'gitlab-markup', '~> 1.6.4'
gem 'github-markup', '~> 1.7.0', require: 'github/markup'
gem 'redcarpet', '~> 3.4'
gem 'commonmarker', '~> 0.17'
gem 'RedCloth', '~> 4.3.2'
......
......@@ -86,7 +86,7 @@ GEM
bindata (2.4.3)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootsnap (1.3.1)
bootsnap (1.3.2)
msgpack (~> 1.0)
bootstrap_form (2.7.0)
brakeman (4.2.1)
......@@ -140,7 +140,7 @@ GEM
creole (0.5.0)
css_parser (1.5.0)
addressable
daemons (1.2.3)
daemons (1.2.6)
database_cleaner (1.5.3)
debug_inspector (0.0.2)
debugger-ruby_core_source (1.3.8)
......@@ -187,7 +187,7 @@ GEM
escape_utils (1.1.1)
et-orbi (1.0.3)
tzinfo
eventmachine (1.0.8)
eventmachine (1.2.7)
excon (0.62.0)
execjs (2.6.0)
expression_parser (0.9.0)
......@@ -295,9 +295,6 @@ GEM
rouge (~> 3.1)
sanitize (~> 4.6.4)
stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4.1)
mime-types (>= 1.15)
rugged (~> 0.25)
gitlab-grit (2.8.2)
charlock_holmes (~> 0.6)
diff-lcs (~> 1.1)
......@@ -491,7 +488,7 @@ GEM
mime-types-data (3.2016.0521)
mimemagic (0.3.0)
mini_magick (4.8.0)
mini_mime (1.0.0)
mini_mime (1.0.1)
mini_portile2 (2.3.0)
minitest (5.7.0)
mousetrap-rails (1.4.6)
......@@ -627,9 +624,9 @@ GEM
pry-byebug (3.4.3)
byebug (>= 9.0, < 9.1)
pry (~> 0.10)
pry-rails (0.3.5)
pry (>= 0.9.10)
public_suffix (3.0.2)
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (3.0.3)
pyu-ruby-sasl (0.0.3.3)
rack (1.6.10)
rack-accept (0.4.5)
......@@ -856,7 +853,7 @@ GEM
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
slack-notifier (1.5.1)
spring (2.0.1)
spring (2.0.2)
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
......@@ -886,7 +883,7 @@ GEM
test_after_commit (1.1.0)
activerecord (>= 3.2)
text (1.3.1)
thin (1.7.0)
thin (1.7.2)
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3)
......@@ -945,7 +942,7 @@ GEM
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
webpack-rails (0.9.10)
webpack-rails (0.9.11)
railties (>= 3.2.0)
wikicloth (0.8.1)
builder
......@@ -1030,9 +1027,9 @@ DEPENDENCIES
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.118.1)
github-linguist (~> 5.3.3)
github-markup (~> 1.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
gitlab-gollum-rugged_adapter (~> 0.4.4)
gitlab-markup (~> 1.6.4)
gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4)
......
......@@ -89,7 +89,7 @@ GEM
bindata (2.4.3)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootsnap (1.3.1)
bootsnap (1.3.2)
msgpack (~> 1.0)
bootstrap_form (2.7.0)
brakeman (4.2.1)
......@@ -143,7 +143,7 @@ GEM
creole (0.5.0)
css_parser (1.5.0)
addressable
daemons (1.2.3)
daemons (1.2.6)
database_cleaner (1.5.3)
debug_inspector (0.0.2)
debugger-ruby_core_source (1.3.8)
......@@ -190,7 +190,7 @@ GEM
escape_utils (1.1.1)
et-orbi (1.0.3)
tzinfo
eventmachine (1.0.8)
eventmachine (1.2.7)
excon (0.62.0)
execjs (2.6.0)
expression_parser (0.9.0)
......@@ -298,9 +298,6 @@ GEM
rouge (~> 3.1)
sanitize (~> 4.6.4)
stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4.1)
mime-types (>= 1.15)
rugged (~> 0.25)
gitlab-grit (2.8.2)
charlock_holmes (~> 0.6)
diff-lcs (~> 1.1)
......@@ -410,7 +407,7 @@ GEM
json (~> 1.8)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
i18n (1.0.1)
i18n (1.1.0)
concurrent-ruby (~> 1.0)
icalendar (2.4.1)
ice_nine (0.11.2)
......@@ -494,7 +491,7 @@ GEM
mime-types-data (3.2016.0521)
mimemagic (0.3.0)
mini_magick (4.8.0)
mini_mime (1.0.0)
mini_mime (1.0.1)
mini_portile2 (2.3.0)
minitest (5.7.0)
mousetrap-rails (1.4.6)
......@@ -631,9 +628,9 @@ GEM
pry-byebug (3.4.3)
byebug (>= 9.0, < 9.1)
pry (~> 0.10)
pry-rails (0.3.5)
pry (>= 0.9.10)
public_suffix (3.0.2)
pry-rails (0.3.6)
pry (>= 0.10.4)
public_suffix (3.0.3)
pyu-ruby-sasl (0.0.3.3)
rack (2.0.5)
rack-accept (0.4.5)
......@@ -864,7 +861,7 @@ GEM
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
slack-notifier (1.5.1)
spring (2.0.1)
spring (2.0.2)
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
......@@ -892,7 +889,7 @@ GEM
temple (0.8.0)
test-prof (0.2.5)
text (1.3.1)
thin (1.7.0)
thin (1.7.2)
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3)
......@@ -951,7 +948,7 @@ GEM
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
webpack-rails (0.9.10)
webpack-rails (0.9.11)
railties (>= 3.2.0)
websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0)
......@@ -1039,9 +1036,9 @@ DEPENDENCIES
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.118.1)
github-linguist (~> 5.3.3)
github-markup (~> 1.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
gitlab-gollum-rugged_adapter (~> 0.4.4)
gitlab-markup (~> 1.6.4)
gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4)
......
<script>
import $ from 'jquery';
import { Button } from '@gitlab-org/gitlab-ui';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
import ListIssue from '../models/issue';
......@@ -10,6 +11,7 @@ export default {
name: 'BoardNewIssue',
components: {
ProjectSelect,
'gl-button': Button,
},
props: {
groupId: {
......@@ -123,21 +125,23 @@ export default {
:group-id="groupId"
/>
<div class="clearfix prepend-top-10">
<button
<gl-button
ref="submit-button"
:disabled="disabled"
class="btn btn-success float-left"
class="float-left"
variant="success"
type="submit"
>
Submit issue
</button>
<button
class="btn btn-default float-right"
</gl-button>
<gl-button
class="float-right"
type="button"
variant="default"
@click="cancel"
>
Cancel
</button>
</gl-button>
</div>
</form>
</div>
......
......@@ -5,22 +5,22 @@ import { __ } from '~/locale';
import createFlash from '~/flash';
import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue';
import ChangedFiles from './changed_files.vue';
import DiffFile from './diff_file.vue';
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';
export default {
name: 'DiffsApp',
components: {
Icon,
CompareVersions,
ChangedFiles,
DiffFile,
NoChanges,
HiddenFilesWarning,
CommitWidget,
TreeList,
},
props: {
endpoint: {
......@@ -58,6 +58,7 @@ export default {
plainDiffPath: state => state.diffs.plainDiffPath,
emailPatchPath: state => state.diffs.emailPatchPath,
}),
...mapState('diffs', ['showTreeList']),
...mapGetters('diffs', ['isParallelView']),
...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
targetBranch() {
......@@ -88,6 +89,9 @@ export default {
canCurrentUserFork() {
return this.currentUser.canFork === true && this.currentUser.canCreateMergeRequest;
},
showCompareVersions() {
return this.mergeRequestDiffs && this.mergeRequestDiff;
},
},
watch: {
diffViewType() {
......@@ -102,6 +106,8 @@ export default {
this.adjustView();
},
isLoading: 'adjustView',
showTreeList: 'adjustView',
},
mounted() {
this.setBaseConfig({ endpoint: this.endpoint, projectPath: this.projectPath });
......@@ -152,10 +158,11 @@ export default {
}
},
adjustView() {
if (this.shouldShow && this.isParallelView) {
window.mrTabs.expandViewContainer();
} else {
window.mrTabs.resetViewContainer();
if (this.shouldShow) {
this.$nextTick(() => {
window.mrTabs.resetViewContainer();
window.mrTabs.expandViewContainer(this.showTreeList);
});
}
},
},
......@@ -177,7 +184,7 @@ export default {
class="diffs tab-pane"
>
<compare-versions
v-if="!commit && mergeRequestDiffs.length > 1"
v-if="showCompareVersions"
:merge-request-diffs="mergeRequestDiffs"
:merge-request-diff="mergeRequestDiff"
:start-version="startVersion"
......@@ -215,22 +222,26 @@ export default {
:commit="commit"
/>
<changed-files
:diff-files="diffFiles"
/>
<div
v-if="diffFiles.length > 0"
class="files"
>
<diff-file
v-for="file in diffFiles"
:key="file.newPath"
:file="file"
:can-current-user-fork="canCurrentUserFork"
/>
<div class="files d-flex prepend-top-default">
<div
v-show="showTreeList"
class="diff-tree-list"
>
<tree-list />
</div>
<div
v-if="diffFiles.length > 0"
class="diff-files-holder"
>
<diff-file
v-for="file in diffFiles"
:key="file.newPath"
:file="file"
:can-current-user-fork="canCurrentUserFork"
/>
</div>
<no-changes v-else />
</div>
<no-changes v-else />
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '~/lib/utils/text_utility';
import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
import { contentTop } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import ChangedFilesDropdown from './changed_files_dropdown.vue';
import changedFilesMixin from '../mixins/changed_files';
export default {
components: {
Icon,
ChangedFilesDropdown,
ClipboardButton,
},
mixins: [changedFilesMixin],
data() {
return {
isStuck: false,
maxWidth: 'auto',
offsetTop: 0,
};
},
computed: {
...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
sumAddedLines() {
return this.sumValues('addedLines');
},
sumRemovedLines() {
return this.sumValues('removedLines');
},
whitespaceVisible() {
return !getParameterValues('w')[0];
},
toggleWhitespaceText() {
if (this.whitespaceVisible) {
return __('Hide whitespace changes');
}
return __('Show whitespace changes');
},
toggleWhitespacePath() {
if (this.whitespaceVisible) {
return mergeUrlParams({ w: 1 }, window.location.href);
}
return mergeUrlParams({ w: 0 }, window.location.href);
},
top() {
return `${this.offsetTop}px`;
},
},
created() {
document.addEventListener('scroll', this.handleScroll);
this.offsetTop = contentTop();
},
beforeDestroy() {
document.removeEventListener('scroll', this.handleScroll);
},
methods: {
...mapActions('diffs', ['setInlineDiffViewType', 'setParallelDiffViewType', 'expandAllFiles']),
pluralize,
handleScroll() {
if (!this.updating) {
this.$nextTick(this.updateIsStuck);
this.updating = true;
}
},
updateIsStuck() {
if (!this.$refs.wrapper) {
return;
}
const scrollPosition = window.scrollY;
this.isStuck = scrollPosition + this.offsetTop >= this.$refs.placeholder.offsetTop;
this.updating = false;
},
sumValues(key) {
return this.diffFiles.reduce((total, file) => total + file[key], 0);
},
},
};
</script>
<template>
<span>
<div ref="placeholder"></div>
<div
ref="wrapper"
:style="{ top }"
:class="{'is-stuck': isStuck}"
class="content-block oneline-block diff-files-changed diff-files-changed-merge-request
files-changed js-diff-files-changed"
>
<div class="files-changed-inner">
<div
class="inline-parallel-buttons d-none d-md-block"
>
<a
v-if="areAllFilesCollapsed"
class="btn btn-default"
@click="expandAllFiles"
>
{{ __('Expand all') }}
</a>
<a
:href="toggleWhitespacePath"
class="btn btn-default"
>
{{ toggleWhitespaceText }}
</a>
<div class="btn-group">
<button
id="inline-diff-btn"
:class="{ active: isInlineView }"
type="button"
class="btn js-inline-diff-button"
data-view-type="inline"
@click="setInlineDiffViewType"
>
{{ __('Inline') }}
</button>
<button
id="parallel-diff-btn"
:class="{ active: isParallelView }"
type="button"
class="btn js-parallel-diff-button"
data-view-type="parallel"
@click="setParallelDiffViewType"
>
{{ __('Side-by-side') }}
</button>
</div>
</div>
<div class="commit-stat-summary dropdown">
<changed-files-dropdown
:diff-files="diffFiles"
/>
<span
class="js-diff-stats-additions-deletions-expanded
diff-stats-additions-deletions-expanded"
>
with
<strong class="cgreen">
{{ pluralize(`${sumAddedLines} addition`, sumAddedLines) }}
</strong>
and
<strong class="cred">
{{ pluralize(`${sumRemovedLines} deletion`, sumRemovedLines) }}
</strong>
</span>
<div
class="js-diff-stats-additions-deletions-collapsed
diff-stats-additions-deletions-collapsed float-right d-sm-none"
>
<strong class="cgreen">
+{{ sumAddedLines }}
</strong>
<strong class="cred">
-{{ sumRemovedLines }}
</strong>
</div>
</div>
</div>
</div>
</span>
</template>
<script>
import Icon from '~/vue_shared/components/icon.vue';
import changedFilesMixin from '../mixins/changed_files';
export default {
components: {
Icon,
},
mixins: [changedFilesMixin],
data() {
return {
searchText: '',
};
},
computed: {
filteredDiffFiles() {
return this.diffFiles.filter(file =>
file.filePath.toLowerCase().includes(this.searchText.toLowerCase()),
);
},
},
methods: {
clearSearch() {
this.searchText = '';
},
},
};
</script>
<template>
<span>
Showing
<button
class="diff-stats-summary-toggler"
data-toggle="dropdown"
type="button"
aria-expanded="false"
>
<span>
{{ n__('%d changed file', '%d changed files', diffFiles.length) }}
</span>
<icon
class="caret-icon"
name="chevron-down"
/>
</button>
<div class="dropdown-menu diff-file-changes">
<div class="dropdown-input">
<input
v-model="searchText"
type="search"
class="dropdown-input-field"
placeholder="Search files"
autocomplete="off"
/>
<i
v-if="searchText.length === 0"
aria-hidden="true"
data-hidden="true"
class="fa fa-search dropdown-input-search">
</i>
<i
v-else
role="button"
class="fa fa-times dropdown-input-search"
@click.stop.prevent="clearSearch"
></i>
</div>
<div class="dropdown-content">
<ul>
<li
v-for="diffFile in filteredDiffFiles"
:key="diffFile.name"
>
<a
:href="`#${diffFile.fileHash}`"
:title="diffFile.newPath"
class="diff-changed-file"
>
<icon
:name="fileChangedIcon(diffFile)"
:size="16"
:class="fileChangedClass(diffFile)"
class="diff-file-changed-icon append-right-8"
/>
<span class="diff-changed-file-content append-right-8">
<strong
v-if="diffFile.blob && diffFile.blob.name"
class="diff-changed-file-name"
>
{{ diffFile.blob.name }}
</strong>
<strong
v-else
class="diff-changed-blank-file-name"
>
{{ s__('Diffs|No file name available') }}
</strong>
<span class="diff-changed-file-path prepend-top-5">
{{ truncatedDiffPath(diffFile.blob.path) }}
</span>
</span>
<span class="diff-changed-stats">
<span class="cgreen">
+{{ diffFile.addedLines }}
</span>
<span class="cred">
-{{ diffFile.removedLines }}
</span>
</span>
</a>
</li>
<li
v-show="filteredDiffFiles.length === 0"
class="dropdown-menu-empty-item"
>
<a>
{{ __('No files found') }}
</a>
</li>
</ul>
</div>
</div>
</span>
</template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import Tooltip from '@gitlab-org/gitlab-ui/dist/directives/tooltip';
import { __ } from '~/locale';
import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue';
import CompareVersionsDropdown from './compare_versions_dropdown.vue';
export default {
components: {
CompareVersionsDropdown,
Icon,
},
directives: {
Tooltip,
},
props: {
mergeRequestDiffs: {
......@@ -26,30 +35,119 @@ export default {
},
},
computed: {
...mapState('diffs', ['commit', 'showTreeList']),
...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
comparableDiffs() {
return this.mergeRequestDiffs.slice(1);
},
isWhitespaceVisible() {
return !getParameterValues('w')[0];
},
toggleWhitespaceText() {
if (this.isWhitespaceVisible) {
return __('Hide whitespace changes');
}
return __('Show whitespace changes');
},
toggleWhitespacePath() {
if (this.isWhitespaceVisible) {
return mergeUrlParams({ w: 1 }, window.location.href);
}
return mergeUrlParams({ w: 0 }, window.location.href);
},
showDropdowns() {
return !this.commit && this.mergeRequestDiffs.length;
},
},
methods: {
...mapActions('diffs', [
'setInlineDiffViewType',
'setParallelDiffViewType',
'expandAllFiles',
'toggleShowTreeList',
]),
},
};
</script>
<template>
<div class="mr-version-controls">
<div class="mr-version-menus-container content-block">
Changes between
<compare-versions-dropdown
:other-versions="mergeRequestDiffs"
:merge-request-version="mergeRequestDiff"
:show-commit-count="true"
class="mr-version-dropdown"
/>
and
<compare-versions-dropdown
:other-versions="comparableDiffs"
:start-version="startVersion"
:target-branch="targetBranch"
class="mr-version-compare-dropdown"
/>
<div
class="mr-version-menus-container content-block"
>
<button
v-tooltip.hover
type="button"
class="btn btn-default append-right-8 js-toggle-tree-list"
:class="{
active: showTreeList
}"
:title="__('Toggle file browser')"
@click="toggleShowTreeList"
>
<icon
name="hamburger"
/>
</button>
<div
v-if="showDropdowns"
class="d-flex align-items-center compare-versions-container"
>
Changes between
<compare-versions-dropdown
:other-versions="mergeRequestDiffs"
:merge-request-version="mergeRequestDiff"
:show-commit-count="true"
class="mr-version-dropdown"
/>
and
<compare-versions-dropdown
:other-versions="comparableDiffs"
:start-version="startVersion"
:target-branch="targetBranch"
class="mr-version-compare-dropdown"
/>
</div>
<div
class="inline-parallel-buttons d-none d-md-flex ml-auto"
>
<a
v-if="areAllFilesCollapsed"
class="btn btn-default"
@click="expandAllFiles"
>
{{ __('Expand all') }}
</a>
<a
:href="toggleWhitespacePath"
class="btn btn-default"
>
{{ toggleWhitespaceText }}
</a>
<div class="btn-group prepend-left-8">
<button
id="inline-diff-btn"
:class="{ active: isInlineView }"
type="button"
class="btn js-inline-diff-button"
data-view-type="inline"
@click="setInlineDiffViewType"
>
{{ __('Inline') }}
</button>
<button
id="parallel-diff-btn"
:class="{ active: isParallelView }"
type="button"
class="btn js-parallel-diff-button"
data-view-type="parallel"
@click="setParallelDiffViewType"
>
{{ __('Side-by-side') }}
</button>
</div>
</div>
</div>
</div>
</template>
......@@ -108,7 +108,7 @@ export default {
<template>
<span class="dropdown inline">
<a
class="dropdown-toggle btn btn-default"
class="dropdown-menu-toggle btn btn-default w-100"
data-toggle="dropdown"
aria-expanded="false"
>
......@@ -118,6 +118,7 @@ export default {
<Icon
:size="12"
name="angle-down"
class="position-absolute"
/>
</a>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
......@@ -163,3 +164,10 @@ export default {
</div>
</span>
</template>
<style>
.dropdown {
min-width: 0;
max-height: 170px;
}
</style>
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { __, sprintf } from '~/locale';
import createFlash from '~/flash';
......@@ -28,6 +28,7 @@ export default {
};
},
computed: {
...mapState('diffs', ['currentDiffFileId']),
...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
isCollapsed() {
return this.file.collapsed || false;
......@@ -101,6 +102,9 @@ export default {
<template>
<div
:id="file.fileHash"
:class="{
'is-active': currentDiffFileId === file.fileHash
}"
class="diff-file file-holder"
>
<diff-file-header
......@@ -168,3 +172,20 @@ export default {
</div>
</div>
</template>
<style>
@keyframes shadow-fade {
from {
box-shadow: 0 0 4px #919191;
}
to {
box-shadow: 0 0 0 #dfdfdf;
}
}
.diff-file.is-active {
box-shadow: 0 0 0 #dfdfdf;
animation: shadow-fade 1.2s 0.1s 1;
}
</style>
......@@ -166,18 +166,16 @@ export default {
:title="diffFile.oldPath"
class="file-title-name"
data-container="body"
>
{{ diffFile.oldPath }}
</strong>
v-html="diffFile.oldPathHtml"
></strong>
<strong
v-tooltip
:title="diffFile.newPath"
class="file-title-name"
data-container="body"
>
{{ diffFile.newPath }}
</strong>
v-html="diffFile.newPathHtml"
></strong>
</span>
<strong
......
<script>
export default {
props: {
file: {
type: Object,
required: true,
},
},
};
</script>
<template>
<span
v-once
class="file-row-stats"
>
<span class="cgreen">
+{{ file.addedLines }}
</span>
<span class="cred">
-{{ file.removedLines }}
</span>
</span>
</template>
<style>
.file-row-stats {
font-size: 12px;
}
</style>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
import FileRowStats from './file_row_stats.vue';
export default {
components: {
Icon,
FileRow,
},
data() {
return {
search: '',
};
},
computed: {
...mapState('diffs', ['tree', 'addedLines', 'removedLines']),
...mapGetters('diffs', ['allBlobs', 'diffFilesLength']),
filteredTreeList() {
const search = this.search.toLowerCase().trim();
if (search === '') return this.tree;
return this.allBlobs.filter(f => f.name.toLowerCase().indexOf(search) >= 0);
},
},
methods: {
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
clearSearch() {
this.search = '';
},
},
FileRowStats,
};
</script>
<template>
<div class="tree-list-holder d-flex flex-column">
<div class="append-bottom-8 position-relative tree-list-search">
<icon
name="search"
class="position-absolute tree-list-icon"
/>
<input
v-model="search"
:placeholder="s__('MergeRequest|Filter files')"
type="search"
class="form-control"
/>
<button
v-show="search"
:aria-label="__('Clear search')"
type="button"
class="position-absolute tree-list-icon tree-list-clear-icon border-0 p-0"
@click="clearSearch"
>
<icon
name="close"
/>
</button>
</div>
<div
class="tree-list-scroll"
>
<template v-if="filteredTreeList.length">
<file-row
v-for="file in filteredTreeList"
:key="file.key"
:file="file"
:level="0"
:hide-extra-on-tree="true"
:extra-component="$options.FileRowStats"
:show-changed-icon="true"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="scrollToFile"
/>
</template>
<p
v-else
class="prepend-top-20 append-bottom-20 text-center"
>
{{ s__('MergeRequest|No files found') }}
</p>
</div>
<div
v-once
class="pt-3 pb-3 text-center"
>
{{ n__('%d changed file', '%d changed files', diffFilesLength) }}
<div>
<span class="cgreen">
{{ n__('%d addition', '%d additions', addedLines) }}
</span>
<span class="cred">
{{ n__('%d deleted', '%d deletions', removedLines) }}
</span>
</div>
</div>
</div>
</template>
......@@ -29,3 +29,5 @@ export const LENGTH_OF_AVATAR_TOOLTIP = 17;
export const LINES_TO_BE_RENDERED_DIRECTLY = 100;
export const MAX_LINES_TO_BE_RENDERED = 2000;
export const MR_TREE_SHOW_KEY = 'mr_tree_show';
export default {
props: {
diffFiles: {
type: Array,
required: true,
},
},
methods: {
fileChangedIcon(diffFile) {
if (diffFile.deletedFile) {
return 'file-deletion';
} else if (diffFile.newFile) {
return 'file-addition';
}
return 'file-modified';
},
fileChangedClass(diffFile) {
if (diffFile.deletedFile) {
return 'cred';
} else if (diffFile.newFile) {
return 'cgreen';
}
return '';
},
truncatedDiffPath(path) {
const maxLength = 60;
if (path.length > maxLength) {
const start = path.length - maxLength;
const end = start + maxLength;
return `...${path.slice(start, end)}`;
}
return path;
},
},
};
......@@ -12,6 +12,7 @@ import {
PARALLEL_DIFF_VIEW_TYPE,
INLINE_DIFF_VIEW_TYPE,
DIFF_VIEW_COOKIE_NAME,
MR_TREE_SHOW_KEY,
} from '../constants';
export const setBaseConfig = ({ commit }, options) => {
......@@ -195,5 +196,23 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => {
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
};
export const toggleTreeOpen = ({ commit }, path) => {
commit(types.TOGGLE_FOLDER_OPEN, path);
};
export const scrollToFile = ({ state, commit }, path) => {
const { fileHash } = state.treeEntries[path];
document.location.hash = fileHash;
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
setTimeout(() => commit(types.UPDATE_CURRENT_DIFF_FILE_ID, ''), 1000);
};
export const toggleShowTreeList = ({ commit, state }) => {
commit(types.TOGGLE_SHOW_TREE_LIST);
localStorage.setItem(MR_TREE_SHOW_KEY, state.showTreeList);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -110,5 +110,9 @@ export const shouldRenderInlineCommentRow = state => line => {
export const getDiffFileByHash = state => fileHash =>
state.diffFiles.find(file => file.fileHash === fileHash);
export const allBlobs = state => Object.values(state.treeEntries).filter(f => f.type === 'blob');
export const diffFilesLength = state => state.diffFiles.length;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Cookies from 'js-cookie';
import { getParameterValues } from '~/lib/utils/url_utility';
import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants';
import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME, MR_TREE_SHOW_KEY } from '../../constants';
const viewTypeFromQueryString = getParameterValues('view')[0];
const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME);
const defaultViewType = INLINE_DIFF_VIEW_TYPE;
const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY);
export default () => ({
isLoading: true,
......@@ -17,4 +18,8 @@ export default () => ({
mergeRequestDiff: null,
diffLineCommentForms: {},
diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
tree: [],
treeEntries: {},
showTreeList: storedTreeShow === null ? true : storedTreeShow === 'true',
currentDiffFileId: '',
});
......@@ -11,3 +11,6 @@ export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';
export const RENDER_FILE = 'RENDER_FILE';
export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE';
export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE';
export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN';
export const TOGGLE_SHOW_TREE_LIST = 'TOGGLE_SHOW_TREE_LIST';
export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID';
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { sortTree } from '~/ide/stores/utils';
import {
findDiffFile,
addLineReferences,
......@@ -7,6 +8,7 @@ import {
addContextLines,
prepareDiffData,
isDiscussionApplicableToLine,
generateTreeList,
} from './utils';
import * as types from './mutation_types';
......@@ -23,9 +25,12 @@ export default {
[types.SET_DIFF_DATA](state, data) {
const diffData = convertObjectPropsToCamelCase(data, { deep: true });
prepareDiffData(diffData);
const { tree, treeEntries } = generateTreeList(diffData.diffFiles);
Object.assign(state, {
...diffData,
tree: sortTree(tree),
treeEntries,
});
},
......@@ -163,4 +168,13 @@ export default {
}
}
},
[types.TOGGLE_FOLDER_OPEN](state, path) {
state.treeEntries[path].opened = !state.treeEntries[path].opened;
},
[types.TOGGLE_SHOW_TREE_LIST](state) {
state.showTreeList = !state.showTreeList;
},
[types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) {
state.currentDiffFileId = fileId;
},
};
......@@ -267,3 +267,49 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD
return latestDiff && discussion.active && lineCode === discussion.line_code;
}
export const generateTreeList = files =>
files.reduce(
(acc, file) => {
const { fileHash, addedLines, removedLines, newFile, deletedFile, newPath } = file;
const split = newPath.split('/');
split.forEach((name, i) => {
const parent = acc.treeEntries[split.slice(0, i).join('/')];
const path = `${parent ? `${parent.path}/` : ''}${name}`;
if (!acc.treeEntries[path]) {
const type = path === newPath ? 'blob' : 'tree';
acc.treeEntries[path] = {
key: path,
path,
name,
type,
tree: [],
};
const entry = acc.treeEntries[path];
if (type === 'blob') {
Object.assign(entry, {
changed: true,
tempFile: newFile,
deleted: deletedFile,
fileHash,
addedLines,
removedLines,
});
} else {
Object.assign(entry, {
opened: true,
});
}
(parent ? parent.tree : acc.tree).push(entry);
}
});
return acc;
},
{ treeEntries: {}, tree: [] },
);
......@@ -2,12 +2,14 @@
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
import { Button } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
'gl-button': Button,
},
directives: {
tooltip,
......@@ -26,15 +28,16 @@ export default {
};
</script>
<template>
<a
<gl-button
v-tooltip
:href="monitoringUrl"
:title="title"
:aria-label="title"
class="btn monitoring-url d-none d-sm-none d-md-block"
class="monitoring-url d-none d-sm-none d-md-block"
data-container="body"
rel="noopener noreferrer nofollow"
variant="default"
>
<icon name="chart" />
</a>
</gl-button>
</template>
......@@ -51,7 +51,11 @@ export default class DropdownHint extends FilteredSearchDropdown {
FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
}
FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container);
const key = token.replace(':', '');
const { uppercaseTokenName } = this.tokenKeys.searchByKey(key);
FilteredSearchDropdownManager.addWordToInput(key, '', false, {
uppercaseTokenName,
});
}
this.dismissDropdown();
this.dispatchInputEvent();
......
......@@ -143,7 +143,9 @@ export default class DropdownUtils {
const dataValue = selected.getAttribute('data-value');
if (dataValue) {
FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true, {
capitalizeTokenValue: selected.hasAttribute('data-capitalize'),
});
}
// Return boolean based on whether it was set
......
......@@ -91,6 +91,11 @@ export default class FilteredSearchDropdownManager {
gl: DropdownEmoji,
element: this.container.querySelector('#js-dropdown-my-reaction'),
},
wip: {
reference: null,
gl: DropdownNonUser,
element: this.container.querySelector('#js-dropdown-wip'),
},
status: {
reference: null,
gl: NullDropdown,
......@@ -136,10 +141,16 @@ export default class FilteredSearchDropdownManager {
return endpoint;
}
static addWordToInput(tokenName, tokenValue = '', clicked = false) {
static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) {
const {
uppercaseTokenName = false,
capitalizeTokenValue = false,
} = options;
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue, {
uppercaseTokenName,
capitalizeTokenValue,
});
input.value = '';
if (clicked) {
......
......@@ -405,7 +405,10 @@ export default class FilteredSearchManager {
if (isLastVisualTokenValid) {
tokens.forEach((t) => {
input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`);
FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`, {
uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(t.key),
capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(t.key),
});
});
const fragments = searchToken.split(':');
......@@ -421,7 +424,10 @@ export default class FilteredSearchManager {
FilteredSearchVisualTokens.addSearchVisualToken(searchTerms);
}
FilteredSearchVisualTokens.addFilterVisualToken(tokenKey);
FilteredSearchVisualTokens.addFilterVisualToken(tokenKey, null, {
uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(tokenKey),
capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(tokenKey),
});
input.value = input.value.replace(`${tokenKey}:`, '');
}
} else {
......@@ -429,7 +435,10 @@ export default class FilteredSearchManager {
const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g;
if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') {
FilteredSearchVisualTokens.addFilterVisualToken(searchToken);
const tokenKey = FilteredSearchVisualTokens.getLastTokenPartial();
FilteredSearchVisualTokens.addFilterVisualToken(searchToken, null, {
capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(tokenKey),
});
// Trim the last space as seen in the if statement above
input.value = input.value.replace(searchToken, '').trim();
......@@ -480,7 +489,7 @@ export default class FilteredSearchManager {
FilteredSearchVisualTokens.addFilterVisualToken(
condition.tokenKey,
condition.value,
canEdit,
{ canEdit },
);
} else {
// Sanitize value since URL converts spaces into +
......@@ -506,10 +515,15 @@ export default class FilteredSearchManager {
hasFilteredSearch = true;
const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue);
const { uppercaseTokenName, capitalizeTokenValue } = match;
FilteredSearchVisualTokens.addFilterVisualToken(
sanitizedKey,
`${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`,
canEdit,
{
canEdit,
uppercaseTokenName,
capitalizeTokenValue,
},
);
} else if (!match && keyParam === 'assignee_id') {
const id = parseInt(value, 10);
......@@ -517,7 +531,7 @@ export default class FilteredSearchManager {
hasFilteredSearch = true;
const tokenName = 'assignee';
const canEdit = this.canEdit && this.canEdit(tokenName);
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit);
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit });
}
} else if (!match && keyParam === 'author_id') {
const id = parseInt(value, 10);
......@@ -525,7 +539,7 @@ export default class FilteredSearchManager {
hasFilteredSearch = true;
const tokenName = 'author';
const canEdit = this.canEdit && this.canEdit(tokenName);
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit);
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit });
}
} else if (!match && keyParam === 'search') {
hasFilteredSearch = true;
......@@ -561,15 +575,17 @@ export default class FilteredSearchManager {
this.saveCurrentSearchQuery();
const { tokens, searchToken }
= this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys());
const tokenKeys = this.filteredSearchTokenKeys.getKeys();
const { tokens, searchToken } = this.tokenizer.processTokens(searchQuery, tokenKeys);
const currentState = state || getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`);
tokens.forEach((token) => {
const condition = this.filteredSearchTokenKeys
.searchByConditionKeyValue(token.key, token.value.toLowerCase());
const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
const tokenConfig = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
const { param } = tokenConfig;
// Replace hyphen with underscore to use as request parameter
// e.g. 'my-reaction' => 'my_reaction'
const underscoredKey = token.key.replace('-', '_');
......@@ -581,6 +597,10 @@ export default class FilteredSearchManager {
} else {
let tokenValue = token.value;
if (tokenConfig.lowercaseValueOnSubmit) {
tokenValue = tokenValue.toLowerCase();
}
if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
(tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
tokenValue = tokenValue.slice(1, tokenValue.length - 1);
......
......@@ -23,6 +23,16 @@ export default class FilteredSearchTokenKeys {
return this.conditions;
}
shouldUppercaseTokenName(tokenKey) {
const token = this.searchByKey(tokenKey.toLowerCase());
return token && token.uppercaseTokenName;
}
shouldCapitalizeTokenValue(tokenKey) {
const token = this.searchByKey(tokenKey.toLowerCase());
return token && token.capitalizeTokenValue;
}
searchByKey(key) {
return this.tokenKeys.find(tokenKey => tokenKey.key === key) || null;
}
......@@ -55,4 +65,21 @@ export default class FilteredSearchTokenKeys {
return this.conditions
.find(condition => condition.tokenKey === key && condition.value === value) || null;
}
addExtraTokensForMergeRequests() {
const wipToken = {
key: 'wip',
type: 'string',
param: '',
symbol: '',
icon: 'admin',
tag: 'Yes or No',
lowercaseValueOnSubmit: true,
uppercaseTokenName: true,
capitalizeTokenValue: true,
};
this.tokenKeys.push(wipToken);
this.tokenKeysWithAlternative.push(wipToken);
}
}
......@@ -55,12 +55,18 @@ export default class FilteredSearchVisualTokens {
}
}
static createVisualTokenElementHTML(canEdit = true) {
static createVisualTokenElementHTML(options = {}) {
const {
canEdit = true,
uppercaseTokenName = false,
capitalizeTokenValue = false,
} = options;
return `
<div class="${canEdit ? 'selectable' : 'hidden'}" role="button">
<div class="name"></div>
<div class="${uppercaseTokenName ? 'text-uppercase' : ''} name"></div>
<div class="value-container">
<div class="value"></div>
<div class="${capitalizeTokenValue ? 'text-capitalize' : ''} value"></div>
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
......@@ -182,16 +188,26 @@ export default class FilteredSearchVisualTokens {
}
}
static addVisualTokenElement(name, value, isSearchTerm, canEdit) {
static addVisualTokenElement(name, value, options = {}) {
const {
isSearchTerm = false,
canEdit,
uppercaseTokenName,
capitalizeTokenValue,
} = options;
const li = document.createElement('li');
li.classList.add('js-visual-token');
li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token');
if (value) {
li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML(canEdit);
li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML({
canEdit,
uppercaseTokenName,
capitalizeTokenValue,
});
FilteredSearchVisualTokens.renderVisualTokenValue(li, name, value);
} else {
li.innerHTML = '<div class="name"></div>';
li.innerHTML = `<div class="${uppercaseTokenName ? 'text-uppercase' : ''} name"></div>`;
}
li.querySelector('.name').innerText = name;
......@@ -212,20 +228,32 @@ export default class FilteredSearchVisualTokens {
}
}
static addFilterVisualToken(tokenName, tokenValue, canEdit) {
static addFilterVisualToken(tokenName, tokenValue, {
canEdit,
uppercaseTokenName = false,
capitalizeTokenValue = false,
} = {}) {
const { lastVisualToken, isLastVisualTokenValid }
= FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const { addVisualTokenElement } = FilteredSearchVisualTokens;
if (isLastVisualTokenValid) {
addVisualTokenElement(tokenName, tokenValue, false, canEdit);
addVisualTokenElement(tokenName, tokenValue, {
canEdit,
uppercaseTokenName,
capitalizeTokenValue,
});
} else {
const previousTokenName = lastVisualToken.querySelector('.name').innerText;
const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
tokensContainer.removeChild(lastVisualToken);
const value = tokenValue || tokenName;
addVisualTokenElement(previousTokenName, value, false, canEdit);
addVisualTokenElement(previousTokenName, value, {
canEdit,
uppercaseTokenName,
capitalizeTokenValue,
});
}
}
......@@ -235,7 +263,9 @@ export default class FilteredSearchVisualTokens {
if (lastVisualToken && lastVisualToken.classList.contains('filtered-search-term')) {
lastVisualToken.querySelector('.name').innerText += ` ${searchTerm}`;
} else {
FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, true);
FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, {
isSearchTerm: true,
});
}
}
......@@ -306,7 +336,9 @@ export default class FilteredSearchVisualTokens {
let value;
if (token.classList.contains('filtered-search-token')) {
FilteredSearchVisualTokens.addFilterVisualToken(nameElement.innerText);
FilteredSearchVisualTokens.addFilterVisualToken(nameElement.innerText, null, {
uppercaseTokenName: nameElement.classList.contains('text-uppercase'),
});
const valueContainerElement = token.querySelector('.value-container');
value = valueContainerElement.dataset.originalValue;
......
......@@ -3,7 +3,7 @@ import $ from 'jquery';
import { mapActions } from 'vuex';
import { __ } from '~/locale';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import ChangedFileIcon from '../changed_file_icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
export default {
components: {
......
<script>
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import FileIcon from '../../../vue_shared/components/file_icon.vue';
import ChangedFileIcon from '../changed_file_icon.vue';
import ChangedFileIcon from '../../../vue_shared/components/changed_file_icon.vue';
const MAX_PATH_LENGTH = 60;
......
......@@ -3,8 +3,8 @@ import { mapGetters } from 'vuex';
import { n__, __, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
import NewDropdown from './new_dropdown/index.vue';
import ChangedFileIcon from './changed_file_icon.vue';
import MrFileIcon from './mr_file_icon.vue';
export default {
......
......@@ -3,8 +3,8 @@ import { mapActions } from 'vuex';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
import FileStatusIcon from './repo_file_status_icon.vue';
import ChangedFileIcon from './changed_file_icon.vue';
export default {
components: {
......
......@@ -24,7 +24,6 @@ export default class Job extends LogOutputBehaviours {
this.$document = $(document);
this.$window = $(window);
this.logBytes = 0;
this.updateDropdown = this.updateDropdown.bind(this);
this.$buildTrace = $('#build-trace');
this.$buildRefreshAnimation = $('.js-build-refresh');
......@@ -35,18 +34,12 @@ export default class Job extends LogOutputBehaviours {
clearTimeout(this.timeout);
this.initSidebar();
this.populateJobs(this.buildStage);
this.updateStageDropdownText(this.buildStage);
this.sidebarOnResize();
this.$document
.off('click', '.js-sidebar-build-toggle')
.on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.$document
.off('click', '.stage-item')
.on('click', '.stage-item', this.updateDropdown);
this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
this.$window
......@@ -194,20 +187,4 @@ export default class Job extends LogOutputBehaviours {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
}
// eslint-disable-next-line class-methods-use-this
populateJobs(stage) {
$('.build-job').hide();
$(`.build-job[data-stage="${stage}"]`).show();
}
// eslint-disable-next-line class-methods-use-this
updateStageDropdownText(stage) {
$('.stage-selection').text(stage);
}
updateDropdown(e) {
e.preventDefault();
const stage = e.currentTarget.text;
this.updateStageDropdownText(stage);
this.populateJobs(stage);
}
}
......@@ -12,12 +12,16 @@
type: Object,
required: true,
},
iconStatus: {
type: Object,
required: true,
},
},
computed: {
environment() {
let environmentText;
switch (this.deploymentStatus.status) {
case 'latest':
case 'last':
environmentText = sprintf(
__('This job is the most recent deployment to %{link}.'),
{ link: this.environmentLink },
......@@ -32,7 +36,7 @@
),
{
environmentLink: this.environmentLink,
deploymentLink: this.deploymentLink,
deploymentLink: this.deploymentLink(`#${this.lastDeployment.iid}`),
},
false,
);
......@@ -56,11 +60,11 @@
if (this.hasLastDeployment) {
environmentText = sprintf(
__(
'This job is creating a deployment to %{environmentLink} and will overwrite the last %{deploymentLink}.',
'This job is creating a deployment to %{environmentLink} and will overwrite the %{deploymentLink}.',
),
{
environmentLink: this.environmentLink,
deploymentLink: this.deploymentLink,
deploymentLink: this.deploymentLink(__('latest deployment')),
},
false,
);
......@@ -78,41 +82,57 @@
return environmentText;
},
environmentLink() {
return sprintf(
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${this.deploymentStatus.environment.path}">`,
name: _.escape(this.deploymentStatus.environment.name),
endLink: '</a>',
},
false,
);
if (this.hasEnvironment) {
return sprintf(
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${
this.deploymentStatus.environment.environment_path
}" class="js-environment-link">`,
name: _.escape(this.deploymentStatus.environment.name),
endLink: '</a>',
},
false,
);
}
return '';
},
deploymentLink() {
hasLastDeployment() {
return this.hasEnvironment && this.deploymentStatus.environment.last_deployment;
},
lastDeployment() {
return this.hasLastDeployment ? this.deploymentStatus.environment.last_deployment : {};
},
hasEnvironment() {
return !_.isEmpty(this.deploymentStatus.environment);
},
lastDeploymentPath() {
return !_.isEmpty(this.lastDeployment.deployable) ? this.lastDeployment.deployable.build_path : '';
},
},
methods: {
deploymentLink(name) {
return sprintf(
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${this.lastDeployment.path}">`,
name: _.escape(this.lastDeployment.name),
startLink: `<a href="${this.lastDeploymentPath}" class="js-job-deployment-link">`,
name,
endLink: '</a>',
},
false,
);
},
hasLastDeployment() {
return this.deploymentStatus.environment.last_deployment;
},
lastDeployment() {
return this.deploymentStatus.environment.last_deployment;
},
},
};
</script>
<template>
<div class="prepend-top-default js-environment-container">
<div class="environment-information">
<ci-icon :status="deploymentStatus.icon" />
<p v-html="environment"></p>
<ci-icon :status="iconStatus"/>
<p
class="inline append-bottom-0"
v-html="environment"
></p>
</div>
</div>
</template>
<script>
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import callout from '../../vue_shared/components/callout.vue';
export default {
name: 'JobHeaderSection',
components: {
ciHeader,
callout,
},
props: {
job: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
},
data() {
return {
actions: this.getActions(),
};
},
computed: {
status() {
return this.job && this.job.status;
},
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length;
},
shouldRenderReason() {
return !!(this.job.status && this.job.callout_message);
},
/**
* When job has not started the key will be `false`
* When job started the key will be a string with a date.
*/
jobStarted() {
return !this.job.started === false;
},
headerTime() {
return this.jobStarted ? this.job.started : this.job.created_at;
},
},
watch: {
job() {
this.actions = this.getActions();
},
},
methods: {
getActions() {
const actions = [];
if (this.job.new_issue_path) {
actions.push({
label: 'New issue',
path: this.job.new_issue_path,
cssClass: 'js-new-issue btn btn-success btn-inverted d-none d-md-block d-lg-block d-xl-block',
type: 'link',
});
}
return actions;
},
},
};
</script>
<template>
<header>
<div class="js-build-header build-header top-area">
<ci-header
v-if="shouldRenderContent"
:status="status"
:item-id="job.id"
:time="headerTime"
:user="job.user"
:actions="actions"
:has-sidebar-button="true"
:should-render-triggered-label="jobStarted"
item-name="Job"
/>
<gl-loading-icon
v-if="isLoading"
:size="2"
class="prepend-top-default append-bottom-default"
/>
</div>
<callout
v-if="shouldRenderReason"
:message="job.callout_message"
/>
</header>
</template>
<script>
import { mapGetters, mapState } from 'vuex';
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
import Callout from '~/vue_shared/components/callout.vue';
import EnvironmentsBlock from './environments_block.vue';
import ErasedBlock from './erased_block.vue';
import StuckBlock from './stuck_block.vue';
export default {
name: 'JobPageApp',
components: {
CiHeader,
Callout,
EnvironmentsBlock,
ErasedBlock,
StuckBlock,
},
props: {
runnerHelpUrl: {
type: String,
required: false,
default: null,
},
},
computed: {
...mapState(['isLoading', 'job']),
...mapGetters([
'headerActions',
'headerTime',
'shouldRenderCalloutMessage',
'jobHasStarted',
'hasEnvironment',
'isJobStuck',
]),
},
};
</script>
<template>
<div>
<gl-loading-icon
v-if="isLoading"
:size="2"
class="prepend-top-20"
/>
<template v-else>
<!-- Header Section -->
<header>
<div class="js-build-header build-header top-area">
<ci-header
:status="job.status"
:item-id="job.id"
:time="headerTime"
:user="job.user"
:actions="headerActions"
:has-sidebar-button="true"
:should-render-triggered-label="jobHasStarted"
:item-name="__('Job')"
/>
</div>
<callout
v-if="shouldRenderCalloutMessage"
:message="job.callout_message"
/>
</header>
<!-- EO Header Section -->
<!-- Body Section -->
<stuck-block
v-if="isJobStuck"
class="js-job-stuck"
:has-no-runners-for-project="job.runners.available"
:tags="job.tags"
:runners-path="runnerHelpUrl"
/>
<environments-block
v-if="hasEnvironment"
:deployment-status="job.deployment_status"
:icon-status="job.status"
/>
<erased-block
v-if="job.erased"
:user="job.erased_by"
:erased-at="job.erased_at"
/>
<!--job log -->
<!-- EO job log -->
<!--empty state -->
<!-- EO empty state -->
<!-- EO Body Section -->
</template>
</div>
</template>
<script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
......@@ -16,26 +17,39 @@
type: Array,
required: true,
},
jobId: {
type: Number,
required: true,
},
},
methods: {
isJobActive(currentJobId) {
return this.jobId === currentJobId;
},
tooltipText(job) {
return `${_.escape(job.name)} - ${job.status.tooltip}`;
},
},
};
</script>
<template>
<div class="builds-container">
<div class="js-jobs-container builds-container">
<div
v-for="job in jobs"
:key="job.id"
class="build-job"
:class="{ retried: job.retried, active: isJobActive(job.id) }"
>
<a
v-for="job in jobs"
:key="job.id"
v-tooltip
:href="job.path"
:title="job.tooltip"
:class="{ active: job.active, retried: job.retried }"
:href="job.status.details_path"
:title="tooltipText(job)"
data-container="body"
>
<icon
v-if="job.active"
v-if="isJobActive(job.id)"
name="arrow-right"
class="js-arrow-right"
class="js-arrow-right icon-arrow-right"
/>
<ci-icon :status="job.status" />
......
<script>
import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { sprintf, __ } from '~/locale';
import { __ } from '~/locale';
export default {
components: {
......@@ -10,30 +10,14 @@
Icon,
},
props: {
pipelineId: {
type: Number,
required: true,
},
pipelinePath: {
type: String,
required: true,
},
pipelineRef: {
type: String,
required: true,
},
pipelineRefPath: {
type: String,
pipeline: {
type: Object,
required: true,
},
stages: {
type: Array,
required: true,
},
pipelineStatus: {
type: Object,
required: true,
},
},
data() {
return {
......@@ -41,57 +25,73 @@
};
},
computed: {
pipelineLink() {
return sprintf(__('Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}'), {
pipelineLinkStart: `<a href=${this.pipelinePath} class="js-pipeline-path link-commit">`,
pipelineId: this.pipelineId,
pipelineLinkEnd: '</a>',
pipelineLinkRefStart: `<a href=${this.pipelineRefPath} class="link-commit ref-name">`,
pipelineRef: this.pipelineRef,
pipelineLinkRefEnd: '</a>',
}, false);
hasRef() {
return !_.isEmpty(this.pipeline.ref);
},
},
watch: {
// When the component is initially mounted it may start with an empty stages array.
// Once the prop is updated, we set the first stage as the selected one
stages(newVal) {
if (newVal.length) {
this.selectedStage = newVal[0].name;
}
},
},
methods: {
onStageClick(stage) {
// todo: consider moving into store
this.selectedStage = stage.name;
// update dropdown with jobs
// jobs container is a new component.
this.$emit('requestSidebarStageDropdown', stage);
this.selectedStage = stage.name;
},
},
};
</script>
<template>
<div class="block-last">
<ci-icon :status="pipelineStatus" />
<div class="block-last dropdown">
<ci-icon
:status="pipeline.details.status"
class="vertical-align-middle"
/>
{{ __('Pipeline') }}
<a
:href="pipeline.path"
class="js-pipeline-path link-commit"
>
#{{ pipeline.id }}
</a>
<template v-if="hasRef">
{{ __('from') }}
<a
:href="pipeline.ref.path"
class="link-commit ref-name"
>
{{ pipeline.ref.name }}
</a>
</template>
<p v-html="pipelineLink"></p>
<button
type="button"
data-toggle="dropdown"
class="js-selected-stage dropdown-menu-toggle prepend-top-8"
>
{{ selectedStage }}
<i class="fa fa-chevron-down" ></i>
</button>
<div class="dropdown">
<button
type="button"
data-toggle="dropdown"
<ul class="dropdown-menu">
<li
v-for="stage in stages"
:key="stage.name"
>
{{ selectedStage }}
<icon name="chevron-down" />
</button>
<ul class="dropdown-menu">
<li
v-for="(stage, index) in stages"
:key="index"
<button
type="button"
class="js-stage-item stage-item"
@click="onStageClick(stage)"
>
<button
type="button"
class="stage-item"
@click="onStageClick(stage)"
>
{{ stage.name }}
</button>
</li>
</ul>
</div>
{{ stage.name }}
</button>
</li>
</ul>
</div>
</template>
import { mapState } from 'vuex';
import _ from 'underscore';
import { mapState, mapActions } from 'vuex';
import Vue from 'vue';
import Job from '../job';
import JobHeader from './components/header.vue';
import DetailsBlock from './components/sidebar_details_block.vue';
import JobApp from './components/job_app.vue';
import Sidebar from './components/sidebar.vue';
import createStore from './store';
export default () => {
......@@ -13,6 +14,7 @@ export default () => {
const store = createStore();
store.dispatch('setJobEndpoint', dataset.endpoint);
store.dispatch('fetchJob');
// Header
......@@ -20,17 +22,18 @@ export default () => {
new Vue({
el: '#js-build-header-vue',
components: {
JobHeader,
JobApp,
},
store,
computed: {
...mapState(['job', 'isLoading']),
},
render(createElement) {
return createElement('job-header', {
return createElement('job-app', {
props: {
isLoading: this.isLoading,
job: this.job,
runnerHelpUrl: dataset.runnerHelpUrl,
},
});
},
......@@ -43,17 +46,25 @@ export default () => {
new Vue({
el: detailsBlockElement,
components: {
DetailsBlock,
Sidebar,
},
store,
computed: {
...mapState(['job', 'isLoading']),
...mapState(['job']),
},
watch: {
job(newVal, oldVal) {
if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
this.fetchStages();
}
},
},
methods: {
...mapActions(['fetchStages']),
},
store,
render(createElement) {
return createElement('details-block', {
return createElement('sidebar', {
props: {
isLoading: this.isLoading,
job: this.job,
runnerHelpUrl: dataset.runnerHelpUrl,
terminalPath: detailsBlockDataset.terminalPath,
},
......
......@@ -62,7 +62,9 @@ export const fetchJob = ({ state, dispatch }) => {
});
};
export const receiveJobSuccess = ({ commit }, data) => commit(types.RECEIVE_JOB_SUCCESS, data);
export const receiveJobSuccess = ({ commit }, data) => {
commit(types.RECEIVE_JOB_SUCCESS, data);
};
export const receiveJobError = ({ commit }) => {
commit(types.RECEIVE_JOB_ERROR);
flash(__('An error occurred while fetching the job.'));
......@@ -137,8 +139,11 @@ export const fetchStages = ({ state, dispatch }) => {
dispatch('requestStages');
axios
.get(state.stagesEndpoint)
.then(({ data }) => dispatch('receiveStagesSuccess', data))
.get(state.job.pipeline.path)
.then(({ data }) => {
dispatch('receiveStagesSuccess', data.details.stages);
dispatch('fetchJobsForStage', data.details.stages[0]);
})
.catch(() => dispatch('receiveStagesError'));
};
export const receiveStagesSuccess = ({ commit }, data) =>
......@@ -152,16 +157,23 @@ export const receiveStagesError = ({ commit }) => {
* Jobs list on sidebar - depend on stages dropdown
*/
export const requestJobsForStage = ({ commit }) => commit(types.REQUEST_JOBS_FOR_STAGE);
export const setSelectedStage = ({ commit }, stage) => commit(types.SET_SELECTED_STAGE, stage);
// On stage click, set selected stage + fetch job
export const fetchJobsForStage = ({ state, dispatch }, stage) => {
dispatch('setSelectedStage', stage);
export const fetchJobsForStage = ({ dispatch }, stage) => {
dispatch('requestJobsForStage');
axios
.get(state.stageJobsEndpoint)
.then(({ data }) => dispatch('receiveJobsForStageSuccess', data))
.get(stage.dropdown_path, {
params: {
retried: 1,
},
})
.then(({ data }) => {
const retriedJobs = data.retried.map(job => Object.assign({}, job, { retried: true }));
const jobs = data.latest_statuses.concat(retriedJobs);
dispatch('receiveJobsForStageSuccess', jobs);
})
.catch(() => dispatch('receiveJobsForStageError'));
};
export const receiveJobsForStageSuccess = ({ commit }, data) =>
......
import _ from 'underscore';
import { __ } from '~/locale';
export const headerActions = state => {
if (state.job.new_issue_path) {
return [
{
label: __('New issue'),
path: state.job.new_issue_path,
cssClass:
'js-new-issue btn btn-success btn-inverted d-none d-md-block d-lg-block d-xl-block',
type: 'link',
},
];
}
return [];
};
export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at);
export const shouldRenderCalloutMessage = state =>
!_.isEmpty(state.job.status) && !_.isEmpty(state.job.callout_message);
/**
* When job has not started the key will be `false`
* When job started the key will be a string with a date.
*/
export const jobHasStarted = state => !(state.job.started === false);
export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);
/**
* When the job is pending and there are no available runners
* we need to render the stuck block;
*
* @returns {Boolean}
*/
export const isJobStuck = state =>
state.job.status.group === 'pending' && state.job.runners && state.job.runners.available === false;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -2,6 +2,7 @@ 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);
......@@ -9,5 +10,6 @@ Vue.use(Vuex);
export default () => new Vuex.Store({
actions,
mutations,
getters,
state: state(),
});
......@@ -88,6 +88,7 @@ export const handleLocationHash = () => {
const fixedDiffStats = document.querySelector('.js-diff-files-changed');
const fixedNav = document.querySelector('.navbar-gitlab');
const performanceBar = document.querySelector('#js-peek');
const topPadding = 8;
let adjustment = 0;
if (fixedNav) adjustment -= fixedNav.offsetHeight;
......@@ -108,6 +109,10 @@ export const handleLocationHash = () => {
adjustment -= performanceBar.offsetHeight;
}
if (isInMRPage()) {
adjustment -= topPadding;
}
window.scrollBy(0, adjustment);
};
......@@ -381,8 +386,11 @@ export const objectToQueryString = (params = {}) =>
.map(param => `${param}=${params[param]}`)
.join('&');
export const buildUrlWithCurrentLocation = param =>
(param ? `${window.location.pathname}${param}` : window.location.pathname);
export const buildUrlWithCurrentLocation = param => {
if (param) return `${window.location.pathname}${param}`;
return window.location.pathname;
};
/**
* Based on the current location and the string parameters provided
......
......@@ -194,9 +194,7 @@ export default class MergeRequestTabs {
if (bp.getBreakpointSize() !== 'lg') {
this.shrinkView();
}
if (this.diffViewType() === 'parallel') {
this.expandViewContainer();
}
this.expandViewContainer();
this.destroyPipelinesView();
this.commitsTab.classList.remove('active');
} else if (action === 'pipelines') {
......@@ -355,7 +353,7 @@ export default class MergeRequestTabs {
localTimeAgo($('.js-timeago', 'div#diffs'));
syntaxHighlight($('#diffs .js-syntax-highlight'));
if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) {
if (this.isDiffAction(this.currentAction)) {
this.expandViewContainer();
}
this.diffsLoaded = true;
......@@ -408,19 +406,23 @@ export default class MergeRequestTabs {
}
diffViewType() {
return $('.inline-parallel-buttons a.active').data('viewType');
return $('.inline-parallel-buttons button.active').data('viewType');
}
isDiffAction(action) {
return action === 'diffs' || action === 'new/diffs';
}
expandViewContainer() {
expandViewContainer(removeLimited = true) {
const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs');
if (this.fixedLayoutPref === null) {
this.fixedLayoutPref = $wrapper.hasClass('container-limited');
}
$wrapper.removeClass('container-limited');
if (this.diffViewType() === 'parallel' || removeLimited) {
$wrapper.removeClass('container-limited');
} else {
$wrapper.addClass('container-limited');
}
}
resetViewContainer() {
......
<script>
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
ClipboardButton,
Icon,
},
props: {
diffFile: {
type: Object,
required: true,
},
},
computed: {
titleTag() {
return this.diffFile.discussionPath ? 'a' : 'span';
},
},
};
</script>
<template>
<div class="file-header-content">
<div
v-if="diffFile.submodule"
>
<span>
<icon name="archive" />
<strong
class="file-title-name"
v-html="diffFile.submoduleLink"
></strong>
<clipboard-button
:text="diffFile.submoduleLink"
title="Copy file path to clipboard"
css-class="btn-default btn-transparent btn-clipboard"
/>
</span>
</div>
<template v-else>
<component
:is="titleTag"
ref="titleWrapper"
:href="diffFile.discussionPath"
>
<span v-html="diffFile.blobIcon"></span>
<span v-if="diffFile.renamedFile">
<strong
:title="diffFile.oldPath"
class="file-title-name has-tooltip"
data-container="body"
>
{{ diffFile.oldPath }}
</strong>
&rarr;
<strong
:title="diffFile.newPath"
class="file-title-name has-tooltip"
data-container="body"
>
{{ diffFile.newPath }}
</strong>
</span>
<strong
v-else
:title="diffFile.oldPath"
class="file-title-name has-tooltip"
data-container="body"
>
{{ diffFile.filePath }}
<span v-if="diffFile.deletedFile">
deleted
</span>
</strong>
</component>
<clipboard-button
:text="diffFile.filePath"
title="Copy file path to clipboard"
css-class="btn-default btn-transparent btn-clipboard"
/>
<small
v-if="diffFile.modeChanged"
ref="fileMode"
>
{{ diffFile.aMode }}{{ diffFile.bMode }}
</small>
</template>
</div>
</template>
......@@ -191,6 +191,7 @@ export default {
if (note.placeholderType === SYSTEM_NOTE) {
return placeholderSystemNote;
}
return placeholderNote;
}
......@@ -201,7 +202,7 @@ export default {
return noteableNote;
},
componentData(note) {
return note.isPlaceholderNote ? this.discussion.notes[0] : note;
return note.isPlaceholderNote ? note.notes[0] : note;
},
toggleDiscussionHandler() {
this.toggleDiscussion({ discussionId: this.discussion.id });
......
......@@ -4,6 +4,8 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered
import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => {
IssuableFilteredSearchTokenKeys.addExtraTokensForMergeRequests();
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
isGroupDecendent: true,
......
......@@ -7,10 +7,13 @@ import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
document.addEventListener('DOMContentLoaded', () => {
IssuableFilteredSearchTokenKeys.addExtraTokensForMergeRequests();
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
});
new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new
......
......@@ -51,10 +51,10 @@ export default {
<template>
<div class="block">
<issuable-time-tracker
:time_estimate="store.timeEstimate"
:time_spent="store.totalTimeSpent"
:human_time_estimate="store.humanTimeEstimate"
:human_time_spent="store.humanTotalTimeSpent"
:time-estimate="store.timeEstimate"
:time-spent="store.totalTimeSpent"
:human-time-estimate="store.humanTimeEstimate"
:human-time-spent="store.humanTotalTimeSpent"
:root-path="store.rootPath"
/>
</div>
......
......@@ -19,24 +19,20 @@ export default {
TimeTrackingHelpState,
},
props: {
// eslint-disable-next-line vue/prop-name-casing
time_estimate: {
timeEstimate: {
type: Number,
required: true,
},
// eslint-disable-next-line vue/prop-name-casing
time_spent: {
timeSpent: {
type: Number,
required: true,
},
// eslint-disable-next-line vue/prop-name-casing
human_time_estimate: {
humanTimeEstimate: {
type: String,
required: false,
default: '',
},
// eslint-disable-next-line vue/prop-name-casing
human_time_spent: {
humanTimeSpent: {
type: String,
required: false,
default: '',
......@@ -52,18 +48,6 @@ export default {
};
},
computed: {
timeSpent() {
return this.time_spent;
},
timeEstimate() {
return this.time_estimate;
},
timeEstimateHumanReadable() {
return this.human_time_estimate;
},
timeSpentHumanReadable() {
return this.human_time_spent;
},
hasTimeSpent() {
return !!this.timeSpent;
},
......@@ -94,10 +78,12 @@ export default {
this.showHelp = show;
},
update(data) {
this.time_estimate = data.time_estimate;
this.time_spent = data.time_spent;
this.human_time_estimate = data.human_time_estimate;
this.human_time_spent = data.human_time_spent;
const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent } = data;
this.timeEstimate = timeEstimate;
this.timeSpent = timeSpent;
this.humanTimeEstimate = humanTimeEstimate;
this.humanTimeSpent = humanTimeSpent;
},
},
};
......@@ -114,8 +100,8 @@ export default {
:show-help-state="showHelpState"
:show-spent-only-state="showSpentOnlyState"
:show-estimate-only-state="showEstimateOnlyState"
:time-spent-human-readable="timeSpentHumanReadable"
:time-estimate-human-readable="timeEstimateHumanReadable"
:time-spent-human-readable="humanTimeSpent"
:time-estimate-human-readable="humanTimeEstimate"
/>
<div class="title hide-collapsed">
{{ __('Time tracking') }}
......@@ -145,11 +131,11 @@ export default {
<div class="time-tracking-content hide-collapsed">
<time-tracking-estimate-only-pane
v-if="showEstimateOnlyState"
:time-estimate-human-readable="timeEstimateHumanReadable"
:time-estimate-human-readable="humanTimeEstimate"
/>
<time-tracking-spent-only-pane
v-if="showSpentOnlyState"
:time-spent-human-readable="timeSpentHumanReadable"
:time-spent-human-readable="humanTimeSpent"
/>
<time-tracking-no-tracking-pane
v-if="showNoTimeTrackingState"
......@@ -158,8 +144,8 @@ export default {
v-if="showComparisonState"
:time-estimate="timeEstimate"
:time-spent="timeSpent"
:time-spent-human-readable="timeSpentHumanReadable"
:time-estimate-human-readable="timeEstimateHumanReadable"
:time-spent-human-readable="humanTimeSpent"
:time-estimate-human-readable="humanTimeEstimate"
/>
<transition name="help-state-toggle">
<time-tracking-help-state
......
......@@ -7,6 +7,8 @@ export default class SidebarMilestone {
if (!el) return;
const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
......@@ -15,10 +17,10 @@ export default class SidebarMilestone {
},
render: createElement => createElement('timeTracker', {
props: {
time_estimate: parseInt(el.dataset.timeEstimate, 10),
time_spent: parseInt(el.dataset.timeSpent, 10),
human_time_estimate: el.dataset.humanTimeEstimate,
human_time_spent: el.dataset.humanTimeSpent,
timeEstimate: parseInt(timeEstimate, 10),
timeSpent: parseInt(timeSpent, 10),
humanTimeEstimate,
humanTimeSpent,
rootPath: '/',
},
}),
......
......@@ -3,7 +3,7 @@ import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
import { getCommitIconMap } from '../utils';
import { getCommitIconMap } from '~/ide/utils';
export default {
components: {
......@@ -32,6 +32,11 @@ export default {
required: false,
default: false,
},
size: {
type: Number,
required: false,
default: 12,
},
},
computed: {
changedIcon() {
......@@ -42,7 +47,7 @@ export default {
return `${getCommitIconMap(this.file).icon}${suffix}`;
},
changedIconClass() {
return `ide-${this.changedIcon} float-left`;
return `${this.changedIcon} float-left d-block`;
},
tooltipTitle() {
if (!this.showTooltip) return undefined;
......@@ -78,13 +83,30 @@ export default {
:title="tooltipTitle"
data-container="body"
data-placement="right"
class="ide-file-changed-icon"
class="file-changed-icon ml-auto"
>
<icon
v-if="showIcon"
:name="changedIcon"
:size="12"
:size="size"
:css-classes="changedIconClass"
/>
</span>
</template>
<style>
.file-addition,
.file-addition-solid {
color: #1aaa55;
}
.file-modified,
.file-modified-solid {
color: #fc9403;
}
.file-deletion,
.file-deletion-solid {
color: #db3b21;
}
</style>
<script>
import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
export default {
name: 'FileRow',
components: {
FileIcon,
Icon,
ChangedFileIcon,
},
props: {
file: {
......@@ -22,6 +24,16 @@ export default {
required: false,
default: null,
},
hideExtraOnTree: {
type: Boolean,
required: false,
default: false,
},
showChangedIcon: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -65,6 +77,9 @@ export default {
toggleTreeOpen(path) {
this.$emit('toggleTreeOpen', path);
},
clickedFile(path) {
this.$emit('clickFile', path);
},
clickFile() {
// Manual Action if a tree is selected/opened
if (this.isTree && this.hasUrlAtCurrentRoute()) {
......@@ -72,6 +87,8 @@ export default {
}
if (this.$router) this.$router.push(`/project${this.file.url}`);
if (this.isBlob) this.clickedFile(this.file.path);
},
scrollIntoView(isInit = false) {
const block = isInit && this.isTree ? 'center' : 'nearest';
......@@ -126,17 +143,24 @@ export default {
class="file-row-name str-truncated"
>
<file-icon
v-if="!showChangedIcon || file.type === 'tree'"
:file-name="file.name"
:loading="file.loading"
:folder="isTree"
:opened="file.opened"
:size="16"
/>
<changed-file-icon
v-else
:file="file"
:size="16"
class="append-right-5"
/>
{{ file.name }}
</span>
<component
:is="extraComponent"
v-if="extraComponent"
v-if="extraComponent && !(hideExtraOnTree && file.type === 'tree')"
:file="file"
:mouse-over="mouseOver"
/>
......@@ -148,8 +172,11 @@ export default {
:key="childFile.key"
:file="childFile"
:level="level + 1"
:hide-extra-on-tree="hideExtraOnTree"
:extra-component="extraComponent"
:show-changed-icon="showChangedIcon"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="clickedFile"
/>
</template>
</div>
......
......@@ -517,21 +517,6 @@ $ide-commit-header-height: 48px;
}
}
.ide-file-addition,
.ide-file-addition-solid {
color: $green-500;
}
.ide-file-modified,
.ide-file-modified-solid {
color: $orange-500;
}
.ide-file-deletion,
.ide-file-deletion-solid {
color: $red-500;
}
.multi-file-commit-list-collapsed {
display: flex;
flex-direction: column;
......@@ -1399,14 +1384,6 @@ $ide-commit-header-height: 48px;
color: $theme-gray-700;
}
.ide-file-changed-icon {
margin-left: auto;
> svg {
display: block;
}
}
.file-row:hover,
.file-row:focus {
.ide-new-btn {
......
......@@ -328,23 +328,6 @@
}
}
.build-dropdown {
margin: $gl-padding 0;
padding: 0;
.dropdown-menu-toggle {
margin-top: #{$gl-padding / 2};
}
svg {
position: relative;
top: 3px;
margin-right: 3px;
width: 14px;
height: 14px;
}
}
.builds-container {
background-color: $white-light;
border-top: 1px solid $border-color;
......@@ -381,15 +364,11 @@
position: absolute;
left: 15px;
top: 20px;
display: none;
display: block;
}
&.active {
font-weight: $gl-font-weight-bold;
.icon-arrow-right {
display: block;
}
}
&.retried {
......
......@@ -223,6 +223,7 @@
}
}
.clipboard-group,
.commit-sha-group {
display: inline-flex;
......
......@@ -571,8 +571,6 @@
}
.files {
margin-top: 1px;
.diff-file:last-child {
margin-bottom: 0;
}
......@@ -987,3 +985,63 @@
.discussion-body .image .frame {
position: relative;
}
.diff-tree-list {
width: 320px;
}
.diff-files-holder {
flex: 1;
min-width: 0;
}
.compare-versions-container {
min-width: 0;
}
.tree-list-holder {
position: sticky;
top: 100px;
max-height: calc(100vh - 100px);
padding-right: $gl-padding;
.file-row {
margin-left: 0;
margin-right: 0;
}
.with-performance-bar & {
top: 135px;
}
}
.tree-list-scroll {
max-height: 100%;
padding-top: $grid-size;
padding-bottom: $grid-size;
border-top: 1px solid $border-color;
border-bottom: 1px solid $border-color;
overflow-y: scroll;
overflow-x: auto;
}
.tree-list-search .form-control {
padding-left: 30px;
}
.tree-list-icon {
top: 50%;
left: 10px;
transform: translateY(-50%);
&,
svg {
fill: $gl-text-color-tertiary;
}
}
.tree-list-clear-icon {
right: 10px;
left: auto;
line-height: 0;
}
......@@ -723,6 +723,17 @@
align-items: center;
padding: 16px;
z-index: 199;
white-space: nowrap;
.dropdown-menu-toggle {
width: auto;
max-width: 170px;
svg {
top: 10px;
right: 8px;
}
}
}
.content-block {
......
......@@ -128,7 +128,7 @@ class IssuableFinder
labels_count = 1 if use_cte_for_search?
finder.execute.reorder(nil).group(:state).count.each do |key, value|
counts[Array(key).last.to_sym] += value / labels_count
counts[count_key(key)] += value / labels_count
end
counts[:all] = counts.values.sum
......@@ -297,6 +297,10 @@ class IssuableFinder
klass.all
end
def count_key(value)
Array(value).last.to_sym
end
# rubocop: disable CodeReuse/ActiveRecord
def by_scope(items)
return items.none if current_user_related? && !current_user
......
......@@ -27,13 +27,17 @@
# updated_before: datetime
#
class MergeRequestsFinder < IssuableFinder
def self.scalar_params
@scalar_params ||= super + [:wip]
end
def klass
MergeRequest
end
def filter_items(_items)
items = by_source_branch(super)
items = by_wip(items)
by_target_branch(items)
end
......@@ -61,5 +65,24 @@ class MergeRequestsFinder < IssuableFinder
items.where(target_branch: target_branch)
end
# rubocop: enable CodeReuse/ActiveRecord
def item_project_ids(items)
items&.reorder(nil)&.select(:target_project_id)
end
def by_wip(items)
if params[:wip] == 'yes'
items.where(wip_match(items.arel_table))
elsif params[:wip] == 'no'
items.where.not(wip_match(items.arel_table))
else
items
end
end
def wip_match(table)
table[:title].matches('WIP:%')
.or(table[:title].matches('WIP %'))
.or(table[:title].matches('[WIP]%'))
end
end
......@@ -261,7 +261,7 @@ class MergeRequest < ActiveRecord::Base
end
end
WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
WIP_REGEX = /\A*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
def self.work_in_progress?(title)
!!(title =~ WIP_REGEX)
......
......@@ -58,7 +58,7 @@ class WikiPage
attr_reader :page
# The attributes Hash used for storing and validating
# new Page values before writing to the Gollum repository.
# new Page values before writing to the raw repository.
attr_accessor :attributes
def hook_attrs
......@@ -111,10 +111,7 @@ class WikiPage
# The processed/formatted content of this page.
def formatted_content
# Assuming @page exists, nil formatted_data means we didn't load it
# before hand (i.e. page was fetched by Gitaly), so we fetch it separately.
# If the page was fetched by Gollum, formatted_data would've been a String.
@attributes[:formatted_content] ||= @page&.formatted_data || @wiki.page_formatted_data(@page)
@attributes[:formatted_content] ||= @wiki.page_formatted_data(@page)
end
# The markup format for the page.
......
......@@ -84,7 +84,7 @@ class DiffFileEntity < Grape::Entity
end
expose :old_path_html do |diff_file|
old_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
old_path, _ = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
old_path
end
......
......@@ -63,6 +63,12 @@ class GitlabUploader < CarrierWave::Uploader::Base
super || file&.filename
end
def relative_path
return path if pathname.relative?
pathname.relative_path_from(Pathname.new(root))
end
def model_valid?
!!model
end
......@@ -115,4 +121,8 @@ class GitlabUploader < CarrierWave::Uploader::Base
# the cache directory.
File.join(work_dir, cache_id, version_name.to_s, for_file)
end
def pathname
@pathname ||= Pathname.new(path)
end
end
......@@ -9,6 +9,8 @@ class JobArtifactUploader < GitlabUploader
storage_options Gitlab.config.artifacts
alias_method :upload, :model
def cached_size
return model.size if model.size.present? && !model.file_changed?
......
......@@ -8,6 +8,8 @@ class LegacyArtifactUploader < GitlabUploader
storage_options Gitlab.config.artifacts
alias_method :upload, :model
def store_dir
dynamic_segment
end
......
......@@ -6,6 +6,8 @@ class LfsObjectUploader < GitlabUploader
storage_options Gitlab.config.lfs
alias_method :upload, :model
def filename
model.oid[4..-1]
end
......
- page_title @application.name, "Applications"
%h3.page-title
Application: #{@application.name}
......@@ -6,23 +7,29 @@
%table.table
%tr
%td
Application Id
= _('Application ID')
%td
%code#application_id= @application.uid
.clipboard-group
.input-group
%input.label.label-monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true }
.input-group-append
= clipboard_button(target: '#application_id', title: _("Copy ID to clipboard"), class: "btn btn btn-default")
%tr
%td
Secret:
= _('Secret')
%td
%code#secret= @application.secret
.clipboard-group
.input-group
%input.label.label-monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true }
.input-group-append
= clipboard_button(target: '#application_id', title: _("Copy secret to clipboard"), class: "btn btn btn-default")
%tr
%td
Callback url
= _('Callback URL')
%td
- @application.redirect_uri.split.each do |uri|
%div
%span.monospace= uri
%tr
%td
Trusted
......
......@@ -10,18 +10,25 @@
%table.table
%tr
%td
= _('Application Id')
= _('Application ID')
%td
%code#application_id= @application.uid
.clipboard-group
.input-group
%input.label.label-monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true }
.input-group-append
= clipboard_button(target: '#application_id', title: _("Copy ID to clipboard"), class: "btn btn btn-default")
%tr
%td
= _('Secret:')
= _('Secret')
%td
%code#secret= @application.secret
.clipboard-group
.input-group
%input.label.label-monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true }
.input-group-append
= clipboard_button(target: '#application_id', title: _("Copy secret to clipboard"), class: "btn btn btn-default")
%tr
%td
= _('Callback url')
= _('Callback URL')
%td
- @application.redirect_uri.split.each do |uri|
%div
......
......@@ -10,7 +10,7 @@
= group_icon(@group, class: "avatar s40 avatar-tile")
.sidebar-context-title
= @group.name
%ul.sidebar-top-level-items
%ul.sidebar-top-level-items.qa-group-sidebar
- if group_sidebar_link?(:overview)
= nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups', 'analytics#show'], html_options: { class: 'home' }) do
= link_to group_path(@group) do
......@@ -109,9 +109,9 @@
= link_to edit_group_path(@group) do
.nav-icon-container
= sprite_icon('settings')
%span.nav-item-name.qa-settings-item
%span.nav-item-name.qa-group-settings-item
= _('Settings')
%ul.sidebar-sub-level-items
%ul.sidebar-sub-level-items.qa-group-sidebar-submenu
= nav_link(path: %w[groups#projects groups#edit badges#index ci_cd#show], html_options: { class: "fly-out-top-item" } ) do
= link_to edit_group_path(@group) do
%strong.fly-out-top-item-name
......
- active_tab = local_assigns.fetch(:active_tab, 'blank')
- track_label = local_assigns.fetch(:track_label, 'import_project')
.project-import
.form-group.import-btn-container.clearfix
......@@ -7,60 +8,63 @@
.import-buttons
- if gitlab_project_import_enabled?
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitlab_export" } do
= icon('gitlab', text: 'GitLab export')
- if github_import_enabled?
%div
= link_to new_import_github_path, class: 'btn js-import-github' do
= link_to new_import_github_path, class: 'btn js-import-github', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "github" } do
= icon('github', text: 'GitHub')
- if bitbucket_import_enabled?
%div
= link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
= link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}",
data: { track_label: "#{track_label}", track_event: "click_button", track_property: "bitbucket_cloud" } do
= icon('bitbucket', text: 'Bitbucket Cloud')
- unless bitbucket_import_configured?
= render 'bitbucket_import_modal'
- if bitbucket_server_import_enabled?
%div
= link_to status_import_bitbucket_server_path, class: "btn import_bitbucket" do
= link_to status_import_bitbucket_server_path, class: "btn import_bitbucket",
data: { track_label: "#{track_label}", track_event: "click_button", track_property: "bitbucket_server" } do
= icon('bitbucket-square', text: 'Bitbucket Server')
%div
- if gitlab_import_enabled?
%div
= link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
= link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}",
data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitlab_com" } do
= icon('gitlab', text: 'GitLab.com')
- unless gitlab_import_configured?
= render 'gitlab_import_modal'
- if google_code_import_enabled?
%div
= link_to new_import_google_code_path, class: 'btn import_google_code' do
= link_to new_import_google_code_path, class: 'btn import_google_code', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "google_code" } do
= icon('google', text: 'Google Code')
- if fogbugz_import_enabled?
%div
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "fogbugz" } do
= icon('bug', text: 'Fogbugz')
- if gitea_import_enabled?
%div
= link_to new_import_gitea_path, class: 'btn import_gitea' do
= link_to new_import_gitea_path, class: 'btn import_gitea', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitea" } do
= custom_icon('go_logo')
Gitea
- if git_import_enabled?
%div
%button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
%button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active', data: { toggle_open_class: 'active', track_label: "#{track_label}" , track_event: "click_button", track_property: "repo_url" } } }
= icon('git', text: 'Repo by URL')
- if manifest_import_enabled?
%div
= link_to new_import_manifest_path, class: 'btn import_manifest' do
= link_to new_import_manifest_path, class: 'btn import_manifest', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "manifest_file" } do
= icon('file-text-o', text: 'Manifest file')
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
= form_for @project, html: { class: 'new_project' } do |f|
%hr
= render "shared/import_form", f: f
= render 'new_project_fields', f: f, project_name_id: "import-url-name", hide_init_with_readme: true
= render 'new_project_fields', f: f, project_name_id: "import-url-name", hide_init_with_readme: true, track_label: track_label
- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility
- ci_cd_only = local_assigns.fetch(:ci_cd_only, false)
- hide_init_with_readme = local_assigns.fetch(:hide_init_with_readme, false)
- track_label = local_assigns.fetch(:track_label, 'blank_project')
.row{ id: project_name_id }
= f.hidden_field :ci_cd_only, value: ci_cd_only
.form-group.project-name.col-sm-12
= f.label :name, class: 'label-bold' do
%span= _("Project name")
= f.text_field :name, placeholder: "My awesome project", class: "form-control input-lg", autofocus: true
= f.text_field :name, placeholder: "My awesome project", class: "form-control input-lg", autofocus: true, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_name", track_value: "" }
.form-group.project-path.col-sm-6
= f.label :namespace_id, class: 'label-bold' do
%span= s_("Project URL")
......@@ -22,7 +23,7 @@
display_path: true,
extra_group: namespace_id),
{},
{ class: 'select2 js-select-namespace qa-project-namespace-select', tabindex: 1})
{ class: 'select2 js-select-namespace qa-project-namespace-select', tabindex: 1, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_path", track_value: "" }})
- else
.input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
......@@ -42,7 +43,7 @@
= f.label :description, class: 'label-bold' do
Project description
%span (optional)
= f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250
= f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_description", track_value: "" }
= f.label :visibility_level, class: 'label-bold' do
Visibility Level
......@@ -53,12 +54,12 @@
.form-group.row.initialize-with-readme-setting
%div{ :class => "col-sm-12" }
.form-check
= check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input'
= check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input', data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "init_with_readme" }
= label_tag 'project[initialize_with_readme]', class: 'form-check-label' do
.option-title
%strong Initialize repository with a README
.option-description
Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.
= f.submit 'Create project', class: "btn btn-success project-submit", tabindex: 4
= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel'
= f.submit 'Create project', class: "btn btn-success project-submit", tabindex: 4, data: { track_label: "#{track_label}", track_event: "click_button", track_property: "create_project", track_value: "" }
= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "cancel" }
......@@ -5,4 +5,4 @@
.project-fields-form
= render 'projects/project_templates/project_fields_form'
= render 'projects/new_project_fields', f: f, project_name_id: "template-project-name", hide_init_with_readme: true
= render 'projects/new_project_fields', f: f, project_name_id: "template-project-name", hide_init_with_readme: true, track_label: "create_from_template"
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.sidebar-container
.blocks-container
#js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } }
- if @build.pipeline.stages_count > 1
.block-last.dropdown.build-dropdown
%div
%span{ class: "ci-status-icon-#{@build.pipeline.status}" }
= ci_icon_for_status(@build.pipeline.status)
Pipeline
= link_to "##{@build.pipeline.id}", project_pipeline_path(@project, @build.pipeline), class: 'link-commit'
from
= link_to "#{@build.pipeline.ref}", project_ref_path(@project, @build.pipeline.ref), class: 'link-commit ref-name'
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.stage-selection More
= icon('chevron-down')
%ul.dropdown-menu
- @build.pipeline.legacy_stages.each do |stage|
%li
%a.stage-item= stage.name
.builds-container
- HasStatus::ORDERED_STATUSES.each do |build_status|
- builds.select{|build| build.status == build_status}.each do |build|
.build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
- tooltip = sanitize(build.tooltip_message.dup)
= link_to(project_job_path(@project, build), data: { toggle: 'tooltip', title: tooltip, container: 'body' }) do
= sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right')
%span{ class: "ci-status-icon-#{build.status}" }
= ci_icon_for_status(build.status)
%span
- if build.name
= build.name
- else
= build.id
- if build.retried?
= sprite_icon('retry', size:16, css_class: 'icon-retry')
......@@ -9,54 +9,6 @@
%div{ class: container_class }
.build-page.js-build-page
#js-build-header-vue
- if @build.stuck?
- unless @build.any_runners_online?
.bs-callout.bs-callout-warning.js-build-stuck
%p
- if @project.any_runners?
This job is stuck, because the project doesn't have any runners online assigned to it.
- elsif @build.tags.any?
This job is stuck, because you don't have any active runners online with any of these tags assigned to them:
- @build.tags.each do |tag|
%span.badge.badge-primary
= tag
- else
This job is stuck, because you don't have any active runners that can run this job.
%br
Go to
= link_to project_runners_path(@build.project, anchor: 'js-runners-settings') do
Runners page
- if @build.starts_environment?
.prepend-top-default.js-environment-container
.environment-information
- if @build.outdated_deployment?
= ci_icon_for_status('success_with_warnings')
- else
= ci_icon_for_status(@build.status)
- environment = environment_for_build(@build.project, @build)
- if @build.success? && @build.last_deployment.present?
- if @build.last_deployment.last?
This job is the most recent deployment to #{environment_link_for_build(@build.project, @build)}.
- else
This job is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}.
View the most recent deployment #{deployment_link(environment.last_deployment)}.
- elsif @build.complete? && !@build.success?
The deployment of this job to #{environment_link_for_build(@build.project, @build)} did not succeed.
- else
This job is creating a deployment to #{environment_link_for_build(@build.project, @build)}
- if environment.try(:last_deployment)
and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')}
- if @build.erased?
.prepend-top-default.js-build-erased
.erased.alert.alert-warning
- if @build.erased_by_user?
Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
- else
Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
- if @build.running? || @build.has_trace?
.build-trace-container.prepend-top-default
......@@ -93,7 +45,7 @@
- else
= render "empty_states"
= render "sidebar", builds: @builds
#js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } }
.js-build-options{ data: javascript_build_options }
......
......@@ -29,15 +29,15 @@
.col-lg-9.js-toggle-container
%ul.nav.nav-tabs.nav-links.gitlab-tabs{ role: 'tablist' }
%li.nav-item{ role: 'presentation' }
%a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' }
%a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab', track_label: 'blank_project', track_event: "click_tab" }, role: 'tab' }
%span.d-none.d-sm-block Blank project
%span.d-block.d-sm-none Blank
%li.nav-item{ role: 'presentation' }
%a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' }
%a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab', track_label: 'create_from_template', track_event: "click_tab" }, role: 'tab' }
%span.d-none.d-sm-block Create from template
%span.d-block.d-sm-none Template
%li.nav-item{ role: 'presentation' }
%a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' }
%a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab', track_label: 'import_project', track_event: "click_tab" }, role: 'tab' }
%span.d-none.d-sm-block Import project
%span.d-block.d-sm-none Import
......
......@@ -10,8 +10,8 @@
= template.description
.controls.d-flex.align-items-center
%label.btn.btn-success.template-button.choose-template.append-right-10.append-bottom-0{ for: template.name }
%input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name }
%input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } }
%span
= _("Use template")
%a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank' }
%a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } }
= _("Preview")
......@@ -3,7 +3,7 @@
- restricted = restricted_visibility_levels.include?(level)
- disabled = disallowed || restricted
.form-check{ class: [('disabled' if disabled), ('restricted' if restricted)] }
= form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled, class: 'form-check-input'
= form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled, class: 'form-check-input', data: { track_label: "blank_project", track_event: "activate_form_input", track_property: "#{model_method}", track_value: "#{level}" }
= form.label "#{model_method}_#{level}", class: 'form-check-label' do
= visibility_level_icon(level)
.option-title
......
......@@ -33,13 +33,13 @@
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
%button.btn.btn-link
%button.btn.btn-link{ type: 'button' }
= sprite_icon('search')
%span
Press Enter or click to search
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link
%button.btn.btn-link{ type: 'button' }
-# Encapsulate static class name `{{icon}}` inside #{} to bypass
-# haml lint's ClassAttributeWithStaticValue
%svg
......@@ -60,7 +60,7 @@
#js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link
%button.btn.btn-link{ type: 'button' }
No Assignee
%li.divider.droplab-item-ignore
- if current_user
......@@ -73,38 +73,46 @@
#js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link
%button.btn.btn-link{ type: 'button' }
No Milestone
%li.filter-dropdown-item{ data: { value: 'upcoming' } }
%button.btn.btn-link
%button.btn.btn-link{ type: 'button' }
Upcoming
%li.filter-dropdown-item{ 'data-value' => 'started' }
%button.btn.btn-link
%button.btn.btn-link{ type: 'button' }
Started
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value
%button.btn.btn-link.js-data-value{ type: 'button' }
{{title}}
#js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link
%button.btn.btn-link{ type: 'button' }
No Label
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link
%button.btn.btn-link{ type: 'button' }
%span.dropdown-label-box{ style: 'background: {{color}}' }
%span.label-title.js-data-value
{{title}}
#js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link
%button.btn.btn-link{ type: 'button' }
%gl-emoji
%span.js-data-value.prepend-left-10
{{name}}
#js-dropdown-wip.filtered-search-input-dropdown-menu.dropdown-menu
%ul.filter-dropdown{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } }
%button.btn.btn-link{ type: 'button' }
= _('Yes')
%li.filter-dropdown-item{ data: { value: 'no', capitalize: true } }
%button.btn.btn-link{ type: 'button' }
= _('No')
= render_if_exists 'shared/issuable/filter_weight', type: type
......
---
title: Adds Web IDE commits to usage ping
merge_request: 22007
author:
type: added
---
title: Support db migration and initialization for Auto DevOps
merge_request: 21955
author:
type: added
---
title: Renders Job show page in new Vue app
merge_request:
author:
type: other
---
title: Removes the 'required' attribute from the 'project name' field
merge_request: 21770
author:
type: other
---
title: Fixes admin runners table not wrapping content
merge_request:
author:
type: fixed
---
title: Fix NULL pipeline import problem and pipeline user mapping issue
merge_request: 21875
author:
type: fixed
---
title: Fix migration to avoid an exception during upgrade
merge_request: 22055
author:
type: fixed
---
title: Fix showing diff file header for renamed files
merge_request: 22089
author:
type: fixed
---
title: Fix rendering placeholder notes
merge_request: 22078
author:
type: fixed
---
title: Add copy to clipboard button for application id and secret
merge_request: 21978
author: George Tsiolis
type: other
---
title: Added search functionality for Work In Progress (WIP) merge requests
merge_request: 18119
author: Chantal Rollison
type: added
---
title: Added tree of changed files to merge request diffs
merge_request: 21833
author:
type: added
---
title: Fix Error 500 when forking projects with Gravatar disabled
merge_request:
author:
type: fixed
......@@ -585,3 +585,17 @@
and are therefore exempt.
:versions: []
:when: 2018-08-30 12:06:35.668181000 Z
- - :approve
- caniuse-lite
- :who: Mike Greiling
:why: CC-BY-4.0 license. Tool only used during build process, code is not present
in compiled/distributed product so attribution not needed.
:versions: []
:when: 2018-10-02 19:23:11.221660000 Z
- - :approve
- node-releases
- :who: Mike Greiling
:why: CC-BY-4.0 license. Tool only used during build process, code is not present
in compiled/distributed product so attribution not needed.
:versions: []
:when: 2018-10-02 19:23:54.840151000 Z
......@@ -3,7 +3,6 @@
# that we can stub it for testing, as it is only called when metrics are
# enabled.
#
# rubocop:disable Metrics/AbcSize
def instrument_classes(instrumentation)
instrumentation.instrument_instance_methods(Gitlab::Shell)
......@@ -48,16 +47,6 @@ def instrument_classes(instrumentation)
instrumentation.instrument_methods(Premailer::Adapter::Nokogiri)
instrumentation.instrument_instance_methods(Premailer::Adapter::Nokogiri)
[
:Blame, :Branch, :BranchCollection, :Blob, :Commit, :Diff, :Repository,
:Tag, :TagCollection, :Tree
].each do |name|
const = Rugged.const_get(name)
instrumentation.instrument_methods(const)
instrumentation.instrument_instance_methods(const)
end
instrumentation.instrument_methods(Banzai::Renderer)
instrumentation.instrument_methods(Banzai::Querying)
......@@ -101,7 +90,6 @@ def instrument_classes(instrumentation)
# Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/30224#note_32306159
instrumentation.instrument_instance_method(MergeRequestDiff, :load_commits)
end
# rubocop:enable Metrics/AbcSize
# With prometheus enabled by default this breaks all specs
# that stubs methods using `any_instance_of` for the models reloaded here.
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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