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

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

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