Merge branch 'master-ce' into scheduled-manual-jobs
This commit is contained in:
commit
5381985bd0
25
.babelrc
25
.babelrc
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
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'
|
||||
|
|
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -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)
|
||||
|
|
5
Gemfile
5
Gemfile
|
@ -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'
|
||||
|
|
25
Gemfile.lock
25
Gemfile.lock
|
@ -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)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<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>
|
||||
|
|
|
@ -1,171 +0,0 @@
|
|||
<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>
|
|
@ -1,126 +0,0 @@
|
|||
<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>
|
|
@ -1,9 +1,18 @@
|
|||
<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>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<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>
|
|
@ -0,0 +1,101 @@
|
|||
<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';
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
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 () => {};
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
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';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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: {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<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,32 +82,45 @@
|
|||
return environmentText;
|
||||
},
|
||||
environmentLink() {
|
||||
return sprintf(
|
||||
'%{startLink}%{name}%{endLink}',
|
||||
{
|
||||
startLink: `<a href="${this.deploymentStatus.environment.path}">`,
|
||||
name: _.escape(this.deploymentStatus.environment.name),
|
||||
endLink: '</a>',
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
deploymentLink() {
|
||||
return sprintf(
|
||||
'%{startLink}%{name}%{endLink}',
|
||||
{
|
||||
startLink: `<a href="${this.lastDeployment.path}">`,
|
||||
name: _.escape(this.lastDeployment.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 '';
|
||||
},
|
||||
hasLastDeployment() {
|
||||
return this.deploymentStatus.environment.last_deployment;
|
||||
return this.hasEnvironment && this.deploymentStatus.environment.last_deployment;
|
||||
},
|
||||
lastDeployment() {
|
||||
return this.deploymentStatus.environment.last_deployment;
|
||||
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.lastDeploymentPath}" class="js-job-deployment-link">`,
|
||||
name,
|
||||
endLink: '</a>',
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -111,8 +128,11 @@
|
|||
<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>
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
<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>
|
|
@ -0,0 +1,99 @@
|
|||
<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>
|
|
@ -1,4 +1,5 @@
|
|||
<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" />
|
||||
|
|
|
@ -0,0 +1,297 @@
|
|||
<script>
|
||||
import _ from 'underscore';
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import DetailRow from './sidebar_detail_row.vue';
|
||||
import ArtifactsBlock from './artifacts_block.vue';
|
||||
import TriggerBlock from './trigger_block.vue';
|
||||
import CommitBlock from './commit_block.vue';
|
||||
import StagesDropdown from './stages_dropdown.vue';
|
||||
import JobsContainer from './jobs_container.vue';
|
||||
|
||||
export default {
|
||||
name: 'JobSidebar',
|
||||
components: {
|
||||
ArtifactsBlock,
|
||||
CommitBlock,
|
||||
DetailRow,
|
||||
Icon,
|
||||
TriggerBlock,
|
||||
StagesDropdown,
|
||||
JobsContainer,
|
||||
},
|
||||
mixins: [timeagoMixin],
|
||||
props: {
|
||||
runnerHelpUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
terminalPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['job', 'isLoading', 'stages', 'jobs']),
|
||||
coverage() {
|
||||
return `${this.job.coverage}%`;
|
||||
},
|
||||
duration() {
|
||||
return timeIntervalInWords(this.job.duration);
|
||||
},
|
||||
queued() {
|
||||
return timeIntervalInWords(this.job.queued);
|
||||
},
|
||||
runnerId() {
|
||||
return `${this.job.runner.description} (#${this.job.runner.id})`;
|
||||
},
|
||||
retryButtonClass() {
|
||||
let className =
|
||||
'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
|
||||
className +=
|
||||
this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
|
||||
return className;
|
||||
},
|
||||
hasTimeout() {
|
||||
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
|
||||
},
|
||||
timeout() {
|
||||
if (this.job.metadata == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let t = this.job.metadata.timeout_human_readable;
|
||||
if (this.job.metadata.timeout_source !== '') {
|
||||
t += ` (from ${this.job.metadata.timeout_source})`;
|
||||
}
|
||||
|
||||
return t;
|
||||
},
|
||||
renderBlock() {
|
||||
return (
|
||||
this.job.merge_request ||
|
||||
this.job.duration ||
|
||||
this.job.finished_data ||
|
||||
this.job.erased_at ||
|
||||
this.job.queued ||
|
||||
this.job.runner ||
|
||||
this.job.coverage ||
|
||||
this.job.tags.length ||
|
||||
this.job.cancel_path
|
||||
);
|
||||
},
|
||||
hasArtifact() {
|
||||
return !_.isEmpty(this.job.artifact);
|
||||
},
|
||||
hasTriggers() {
|
||||
return !_.isEmpty(this.job.trigger);
|
||||
},
|
||||
hasStages() {
|
||||
return (
|
||||
(this.job &&
|
||||
this.job.pipeline &&
|
||||
this.job.pipeline.stages &&
|
||||
this.job.pipeline.stages.length > 0) ||
|
||||
false
|
||||
);
|
||||
},
|
||||
commit() {
|
||||
return this.job.pipeline.commit || {};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchJobsForStage']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<aside
|
||||
class="right-sidebar right-sidebar-expanded build-sidebar"
|
||||
data-offset-top="101"
|
||||
data-spy="affix"
|
||||
>
|
||||
<div class="sidebar-container">
|
||||
<div class="blocks-container">
|
||||
<template v-if="!isLoading">
|
||||
<div class="block">
|
||||
<strong class="inline prepend-top-8">
|
||||
{{ job.name }}
|
||||
</strong>
|
||||
<a
|
||||
v-if="job.retry_path"
|
||||
:class="retryButtonClass"
|
||||
:href="job.retry_path"
|
||||
data-method="post"
|
||||
rel="nofollow"
|
||||
>
|
||||
{{ __('Retry') }}
|
||||
</a>
|
||||
<a
|
||||
v-if="terminalPath"
|
||||
:href="terminalPath"
|
||||
class="js-terminal-link pull-right btn btn-primary
|
||||
btn-inverted visible-md-block visible-lg-block"
|
||||
target="_blank"
|
||||
>
|
||||
{{ __('Debug') }}
|
||||
<icon name="external-link" />
|
||||
</a>
|
||||
<button
|
||||
:aria-label="__('Toggle Sidebar')"
|
||||
type="button"
|
||||
class="btn btn-blank gutter-toggle
|
||||
float-right d-block d-md-none js-sidebar-build-toggle"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
data-hidden="true"
|
||||
class="fa fa-angle-double-right"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="job.retry_path || job.new_issue_path"
|
||||
class="block retry-link"
|
||||
>
|
||||
<a
|
||||
v-if="job.new_issue_path"
|
||||
:href="job.new_issue_path"
|
||||
class="js-new-issue btn btn-success btn-inverted"
|
||||
>
|
||||
{{ __('New issue') }}
|
||||
</a>
|
||||
<a
|
||||
v-if="job.retry_path"
|
||||
:href="job.retry_path"
|
||||
class="js-retry-job btn btn-inverted-secondary"
|
||||
data-method="post"
|
||||
rel="nofollow"
|
||||
>
|
||||
{{ __('Retry') }}
|
||||
</a>
|
||||
</div>
|
||||
<div :class="{ block : renderBlock }">
|
||||
<p
|
||||
v-if="job.merge_request"
|
||||
class="build-detail-row js-job-mr"
|
||||
>
|
||||
<span class="build-light-text">
|
||||
{{ __('Merge Request:') }}
|
||||
</span>
|
||||
<a :href="job.merge_request.path">
|
||||
!{{ job.merge_request.iid }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<detail-row
|
||||
v-if="job.duration"
|
||||
:value="duration"
|
||||
class="js-job-duration"
|
||||
title="Duration"
|
||||
/>
|
||||
<detail-row
|
||||
v-if="job.finished_at"
|
||||
:value="timeFormated(job.finished_at)"
|
||||
class="js-job-finished"
|
||||
title="Finished"
|
||||
/>
|
||||
<detail-row
|
||||
v-if="job.erased_at"
|
||||
:value="timeFormated(job.erased_at)"
|
||||
class="js-job-erased"
|
||||
title="Erased"
|
||||
/>
|
||||
<detail-row
|
||||
v-if="job.queued"
|
||||
:value="queued"
|
||||
class="js-job-queued"
|
||||
title="Queued"
|
||||
/>
|
||||
<detail-row
|
||||
v-if="hasTimeout"
|
||||
:help-url="runnerHelpUrl"
|
||||
:value="timeout"
|
||||
class="js-job-timeout"
|
||||
title="Timeout"
|
||||
/>
|
||||
<detail-row
|
||||
v-if="job.runner"
|
||||
:value="runnerId"
|
||||
class="js-job-runner"
|
||||
title="Runner"
|
||||
/>
|
||||
<detail-row
|
||||
v-if="job.coverage"
|
||||
:value="coverage"
|
||||
class="js-job-coverage"
|
||||
title="Coverage"
|
||||
/>
|
||||
<p
|
||||
v-if="job.tags.length"
|
||||
class="build-detail-row js-job-tags"
|
||||
>
|
||||
<span class="build-light-text">
|
||||
{{ __('Tags:') }}
|
||||
</span>
|
||||
<span
|
||||
v-for="(tag, i) in job.tags"
|
||||
:key="i"
|
||||
class="label label-primary">
|
||||
{{ tag }}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-if="job.cancel_path"
|
||||
class="btn-group prepend-top-5"
|
||||
role="group">
|
||||
<a
|
||||
:href="job.cancel_path"
|
||||
class="js-cancel-job btn btn-sm btn-default"
|
||||
data-method="post"
|
||||
rel="nofollow"
|
||||
>
|
||||
{{ __('Cancel') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<artifacts-block
|
||||
v-if="hasArtifact"
|
||||
:artifact="job.artifact"
|
||||
/>
|
||||
<trigger-block
|
||||
v-if="hasTriggers"
|
||||
:trigger="job.trigger"
|
||||
/>
|
||||
<commit-block
|
||||
:is-last-block="hasStages"
|
||||
:commit="commit"
|
||||
:merge-request="job.merge_request"
|
||||
/>
|
||||
|
||||
<stages-dropdown
|
||||
:stages="stages"
|
||||
:pipeline="job.pipeline"
|
||||
@requestSidebarStageDropdown="fetchJobsForStage"
|
||||
/>
|
||||
|
||||
</template>
|
||||
<gl-loading-icon
|
||||
v-else
|
||||
:size="2"
|
||||
class="prepend-top-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<jobs-container
|
||||
v-if="!isLoading && jobs.length"
|
||||
:jobs="jobs"
|
||||
:job-id="job.id"
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
|
@ -1,276 +0,0 @@
|
|||
<script>
|
||||
import _ from 'underscore';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import DetailRow from './sidebar_detail_row.vue';
|
||||
import ArtifactsBlock from './artifacts_block.vue';
|
||||
import TriggerBlock from './trigger_block.vue';
|
||||
import CommitBlock from './commit_block.vue';
|
||||
|
||||
export default {
|
||||
name: 'SidebarDetailsBlock',
|
||||
components: {
|
||||
ArtifactsBlock,
|
||||
CommitBlock,
|
||||
DetailRow,
|
||||
Icon,
|
||||
TriggerBlock,
|
||||
},
|
||||
mixins: [timeagoMixin],
|
||||
props: {
|
||||
job: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
runnerHelpUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
terminalPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
shouldRenderContent() {
|
||||
return !this.isLoading && Object.keys(this.job).length > 0;
|
||||
},
|
||||
coverage() {
|
||||
return `${this.job.coverage}%`;
|
||||
},
|
||||
duration() {
|
||||
return timeIntervalInWords(this.job.duration);
|
||||
},
|
||||
queued() {
|
||||
return timeIntervalInWords(this.job.queued);
|
||||
},
|
||||
runnerId() {
|
||||
return `${this.job.runner.description} (#${this.job.runner.id})`;
|
||||
},
|
||||
retryButtonClass() {
|
||||
let className =
|
||||
'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
|
||||
className +=
|
||||
this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
|
||||
return className;
|
||||
},
|
||||
hasTimeout() {
|
||||
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
|
||||
},
|
||||
timeout() {
|
||||
if (this.job.metadata == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let t = this.job.metadata.timeout_human_readable;
|
||||
if (this.job.metadata.timeout_source !== '') {
|
||||
t += ` (from ${this.job.metadata.timeout_source})`;
|
||||
}
|
||||
|
||||
return t;
|
||||
},
|
||||
renderBlock() {
|
||||
return (
|
||||
this.job.merge_request ||
|
||||
this.job.duration ||
|
||||
this.job.finished_data ||
|
||||
this.job.erased_at ||
|
||||
this.job.queued ||
|
||||
this.job.runner ||
|
||||
this.job.coverage ||
|
||||
this.job.tags.length ||
|
||||
this.job.cancel_path
|
||||
);
|
||||
},
|
||||
hasArtifact() {
|
||||
return !_.isEmpty(this.job.artifact);
|
||||
},
|
||||
hasTriggers() {
|
||||
return !_.isEmpty(this.job.trigger);
|
||||
},
|
||||
hasStages() {
|
||||
return (
|
||||
this.job &&
|
||||
this.job.pipeline &&
|
||||
this.job.pipeline.stages &&
|
||||
this.job.pipeline.stages.length > 0
|
||||
) || false;
|
||||
},
|
||||
commit() {
|
||||
return this.job.pipeline.commit || {};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div class="block">
|
||||
<strong class="inline prepend-top-8">
|
||||
{{ job.name }}
|
||||
</strong>
|
||||
<a
|
||||
v-if="job.retry_path"
|
||||
:class="retryButtonClass"
|
||||
:href="job.retry_path"
|
||||
data-method="post"
|
||||
rel="nofollow"
|
||||
>
|
||||
{{ __('Retry') }}
|
||||
</a>
|
||||
<a
|
||||
v-if="terminalPath"
|
||||
:href="terminalPath"
|
||||
class="js-terminal-link pull-right btn btn-primary
|
||||
btn-inverted visible-md-block visible-lg-block"
|
||||
target="_blank"
|
||||
>
|
||||
{{ __('Debug') }}
|
||||
<icon name="external-link" />
|
||||
</a>
|
||||
<button
|
||||
:aria-label="__('Toggle Sidebar')"
|
||||
type="button"
|
||||
class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
data-hidden="true"
|
||||
class="fa fa-angle-double-right"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="shouldRenderContent">
|
||||
<div
|
||||
v-if="job.retry_path || job.new_issue_path"
|
||||
class="block retry-link"
|
||||
>
|
||||
<a
|
||||
v-if="job.new_issue_path"
|
||||
:href="job.new_issue_path"
|
||||
class="js-new-issue btn btn-success btn-inverted"
|
||||
>
|
||||
{{ __('New issue') }}
|
||||
</a>
|
||||
<a
|
||||
v-if="job.retry_path"
|
||||
:href="job.retry_path"
|
||||
class="js-retry-job btn btn-inverted-secondary"
|
||||
data-method="post"
|
||||
rel="nofollow"
|
||||
>
|
||||
{{ __('Retry') }}
|
||||
</a>
|
||||
</div>
|
||||
<div :class="{block : renderBlock }">
|
||||
<p
|
||||
v-if="job.merge_request"
|
||||
class="build-detail-row js-job-mr"
|
||||
>
|
||||
<span class="build-light-text">
|
||||
{{ __('Merge Request:') }}
|
||||
</span>
|
||||
<a :href="job.merge_request.path">
|
||||
!{{ job.merge_request.iid }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<detail-row
|
||||
v-if="job.duration"
|
||||
:value="duration"
|
||||
class="js-job-duration"
|
||||
title="Duration"
|
||||
/>
|
||||
<detail-row
|
||||
v-if="job.finished_at"
|
||||
:value="timeFormated(job.finished_at)"
|
||||
class="js-job-finished"
|
||||
title="Finished"
|
||||
/>
|
||||
<detail-row
|
||||
v-if="job.erased_at"
|
||||
:value="timeFormated(job.erased_at)"
|
||||
class="js-job-erased"
|
||||
title="Erased"
|
||||
/>
|
||||
<detail-row
|
||||
v-if="job.queued"
|
||||
:value="queued"
|
||||
class="js-job-queued"
|
||||
title="Queued"
|
||||
/>
|
||||
<detail-row
|
||||
v-if="hasTimeout"
|
||||
:help-url="runnerHelpUrl"
|
||||
:value="timeout"
|
||||
class="js-job-timeout"
|
||||
title="Timeout"
|
||||
/>
|
||||
<detail-row
|
||||
v-if="job.runner"
|
||||
:value="runnerId"
|
||||
class="js-job-runner"
|
||||
title="Runner"
|
||||
/>
|
||||
<detail-row
|
||||
v-if="job.coverage"
|
||||
:value="coverage"
|
||||
class="js-job-coverage"
|
||||
title="Coverage"
|
||||
/>
|
||||
<p
|
||||
v-if="job.tags.length"
|
||||
class="build-detail-row js-job-tags"
|
||||
>
|
||||
<span class="build-light-text">
|
||||
{{ __('Tags:') }}
|
||||
</span>
|
||||
<span
|
||||
v-for="(tag, i) in job.tags"
|
||||
:key="i"
|
||||
class="label label-primary">
|
||||
{{ tag }}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-if="job.cancel_path"
|
||||
class="btn-group prepend-top-5"
|
||||
role="group">
|
||||
<a
|
||||
:href="job.cancel_path"
|
||||
class="js-cancel-job btn btn-sm btn-default"
|
||||
data-method="post"
|
||||
rel="nofollow"
|
||||
>
|
||||
{{ __('Cancel') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<artifacts-block
|
||||
v-if="hasArtifact"
|
||||
:artifact="job.artifact"
|
||||
/>
|
||||
<trigger-block
|
||||
v-if="hasTriggers"
|
||||
:trigger="job.trigger"
|
||||
/>
|
||||
<commit-block
|
||||
:is-last-block="hasStages"
|
||||
:commit="commit"
|
||||
:merge-request="job.merge_request"
|
||||
/>
|
||||
</template>
|
||||
<gl-loading-icon
|
||||
v-if="isLoading"
|
||||
:size="2"
|
||||
class="prepend-top-10"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -1,8 +1,8 @@
|
|||
<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"
|
||||
/>
|
||||
|
||||
<p v-html="pipelineLink"></p>
|
||||
|
||||
<div class="dropdown">
|
||||
<button
|
||||
type="button"
|
||||
data-toggle="dropdown"
|
||||
{{ __('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"
|
||||
>
|
||||
{{ selectedStage }}
|
||||
<icon name="chevron-down" />
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li
|
||||
v-for="(stage, index) in stages"
|
||||
:key="index"
|
||||
{{ pipeline.ref.name }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
|
||||
<ul class="dropdown-menu">
|
||||
<li
|
||||
v-for="stage in stages"
|
||||
:key="stage.name"
|
||||
>
|
||||
<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>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
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,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['job']),
|
||||
},
|
||||
watch: {
|
||||
job(newVal, oldVal) {
|
||||
if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) {
|
||||
this.fetchStages();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchStages']),
|
||||
},
|
||||
store,
|
||||
computed: {
|
||||
...mapState(['job', 'isLoading']),
|
||||
},
|
||||
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) =>
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
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() {
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
<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>
|
||||
→
|
||||
<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>
|
|
@ -1,12 +1,14 @@
|
|||
<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
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
- 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
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
- 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
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
- 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"
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
%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
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Adds Web IDE commits to usage ping
|
||||
merge_request: 22007
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Support db migration and initialization for Auto DevOps
|
||||
merge_request: 21955
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Renders Job show page in new Vue app
|
||||
merge_request:
|
||||
author:
|
||||
type: other
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Removes the 'required' attribute from the 'project name' field
|
||||
merge_request: 21770
|
||||
author:
|
||||
type: other
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Fixes admin runners table not wrapping content
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Fix NULL pipeline import problem and pipeline user mapping issue
|
||||
merge_request: 21875
|
||||
author:
|
||||
type: fixed
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Fix migration to avoid an exception during upgrade
|
||||
merge_request: 22055
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix showing diff file header for renamed files
|
||||
merge_request: 22089
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix rendering placeholder notes
|
||||
merge_request: 22078
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add copy to clipboard button for application id and secret
|
||||
merge_request: 21978
|
||||
author: George Tsiolis
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Added search functionality for Work In Progress (WIP) merge requests
|
||||
merge_request: 18119
|
||||
author: Chantal Rollison
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Added tree of changed files to merge request diffs
|
||||
merge_request: 21833
|
||||
author:
|
||||
type: added
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
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
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue