Commit f3131210 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-10-04

# Conflicts:
#	app/assets/javascripts/jobs/components/job_app.vue
#	app/assets/javascripts/jobs/job_details_bundle.js
#	app/assets/javascripts/jobs/store/getters.js
#	app/views/projects/_import_project_pane.html.haml
#	app/views/projects/_new_project_fields.html.haml
#	app/views/projects/jobs/show.html.haml
#	app/views/projects/project_templates/_built_in_templates.html.haml
#	app/views/shared/_visibility_radios.html.haml
#	config/routes/project.rb
#	config/routes/user.rb
#	lib/api/commits.rb
#	locale/gitlab.pot
#	spec/javascripts/jobs/components/job_app_spec.js

[ci skip]
parents b9f35f4e 630c9a1f
{
"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
......
...@@ -10,24 +10,6 @@ ...@@ -10,24 +10,6 @@
Capybara/CurrentPathExpectation: Capybara/CurrentPathExpectation:
Enabled: false Enabled: false
# Offense count: 23
FactoryBot/DynamicAttributeDefinedStatically:
Exclude:
- 'spec/factories/broadcast_messages.rb'
- 'spec/factories/ci/builds.rb'
- 'spec/factories/ci/runners.rb'
- 'spec/factories/clusters/applications/helm.rb'
- 'spec/factories/clusters/platforms/kubernetes.rb'
- 'spec/factories/emails.rb'
- 'spec/factories/gpg_keys.rb'
- 'spec/factories/group_members.rb'
- 'spec/factories/merge_requests.rb'
- 'spec/factories/notes.rb'
- 'spec/factories/oauth_access_grants.rb'
- 'spec/factories/project_members.rb'
- 'spec/factories/todos.rb'
- 'spec/factories/uploads.rb'
# Offense count: 167 # Offense count: 167
# Cop supports --auto-correct. # Cop supports --auto-correct.
Layout/EmptyLinesAroundArguments: Layout/EmptyLinesAroundArguments:
...@@ -53,20 +35,6 @@ Layout/IndentArray: ...@@ -53,20 +35,6 @@ Layout/IndentArray:
Layout/IndentHash: Layout/IndentHash:
Enabled: false Enabled: false
# Offense count: 11
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment.
Layout/SpaceBeforeFirstArg:
Exclude:
- 'config/routes/project.rb'
- 'db/migrate/20170506185517_add_foreign_key_pipeline_schedules_and_pipelines.rb'
- 'features/steps/project/source/browse_files.rb'
- 'features/steps/project/source/markdown_render.rb'
- 'lib/api/runners.rb'
- 'spec/features/search/user_uses_search_filters_spec.rb'
- 'spec/routing/project_routing_spec.rb'
- 'spec/services/system_note_service_spec.rb'
# Offense count: 93 # Offense count: 93
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
...@@ -74,15 +42,6 @@ Layout/SpaceBeforeFirstArg: ...@@ -74,15 +42,6 @@ Layout/SpaceBeforeFirstArg:
Layout/SpaceInLambdaLiteral: Layout/SpaceInLambdaLiteral:
Enabled: false Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets.
# SupportedStyles: space, no_space, compact
# SupportedStylesForEmptyBrackets: space, no_space
Layout/SpaceInsideArrayLiteralBrackets:
Exclude:
- 'spec/lib/gitlab/import_export/relation_factory_spec.rb'
# Offense count: 327 # Offense count: 327
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
...@@ -96,14 +55,6 @@ Layout/SpaceInsideBlockBraces: ...@@ -96,14 +55,6 @@ Layout/SpaceInsideBlockBraces:
Layout/SpaceInsideParens: Layout/SpaceInsideParens:
Enabled: false Enabled: false
# Offense count: 14
# Cop supports --auto-correct.
Layout/SpaceInsidePercentLiteralDelimiters:
Exclude:
- 'lib/gitlab/git_access.rb'
- 'lib/gitlab/health_checks/fs_shards_check.rb'
- 'spec/lib/gitlab/health_checks/fs_shards_check_spec.rb'
# Offense count: 26 # Offense count: 26
Lint/DuplicateMethods: Lint/DuplicateMethods:
Exclude: Exclude:
...@@ -135,31 +86,11 @@ Lint/InterpolationCheck: ...@@ -135,31 +86,11 @@ Lint/InterpolationCheck:
Lint/MissingCopEnableDirective: Lint/MissingCopEnableDirective:
Enabled: false Enabled: false
# Offense count: 2
Lint/NestedPercentLiteral:
Exclude:
- 'lib/gitlab/git/repository.rb'
- 'spec/support/shared_examples/email_format_shared_examples.rb'
# Offense count: 1 # Offense count: 1
Lint/ReturnInVoidContext: Lint/ReturnInVoidContext:
Exclude: Exclude:
- 'app/models/project.rb' - 'app/models/project.rb'
# Offense count: 1
# Configuration parameters: IgnoreImplicitReferences.
Lint/ShadowedArgument:
Exclude:
- 'lib/gitlab/database/sha_attribute.rb'
# Offense count: 3
# Cop supports --auto-correct.
Lint/UnneededRequireStatement:
Exclude:
- 'db/post_migrate/20161221153951_rename_reserved_project_names.rb'
- 'db/post_migrate/20170313133418_rename_more_reserved_project_names.rb'
- 'lib/declarative_policy.rb'
# Offense count: 9 # Offense count: 9
Lint/UriEscapeUnescape: Lint/UriEscapeUnescape:
Exclude: Exclude:
...@@ -199,16 +130,6 @@ Naming/HeredocDelimiterCase: ...@@ -199,16 +130,6 @@ Naming/HeredocDelimiterCase:
Naming/HeredocDelimiterNaming: Naming/HeredocDelimiterNaming:
Enabled: false Enabled: false
# Offense count: 1
Performance/UnfreezeString:
Exclude:
- 'features/steps/project/commits/commits.rb'
# Offense count: 1
# Cop supports --auto-correct.
Performance/UriDefaultParser:
Exclude:
- 'lib/gitlab/url_sanitizer.rb'
# Offense count: 3821 # Offense count: 3821
# Configuration parameters: Prefixes. # Configuration parameters: Prefixes.
......
...@@ -94,7 +94,7 @@ GEM ...@@ -94,7 +94,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)
...@@ -148,7 +148,7 @@ GEM ...@@ -148,7 +148,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)
...@@ -208,7 +208,7 @@ GEM ...@@ -208,7 +208,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)
...@@ -516,7 +516,7 @@ GEM ...@@ -516,7 +516,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)
...@@ -653,9 +653,9 @@ GEM ...@@ -653,9 +653,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)
...@@ -882,7 +882,7 @@ GEM ...@@ -882,7 +882,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)
...@@ -912,7 +912,7 @@ GEM ...@@ -912,7 +912,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)
...@@ -971,7 +971,7 @@ GEM ...@@ -971,7 +971,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
......
...@@ -97,7 +97,7 @@ GEM ...@@ -97,7 +97,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)
...@@ -151,7 +151,7 @@ GEM ...@@ -151,7 +151,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)
...@@ -211,7 +211,7 @@ GEM ...@@ -211,7 +211,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)
...@@ -434,7 +434,7 @@ GEM ...@@ -434,7 +434,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)
...@@ -519,7 +519,7 @@ GEM ...@@ -519,7 +519,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)
...@@ -657,9 +657,9 @@ GEM ...@@ -657,9 +657,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)
...@@ -890,7 +890,7 @@ GEM ...@@ -890,7 +890,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)
...@@ -918,7 +918,7 @@ GEM ...@@ -918,7 +918,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)
...@@ -977,7 +977,7 @@ GEM ...@@ -977,7 +977,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)
......
...@@ -23,6 +23,7 @@ const Api = { ...@@ -23,6 +23,7 @@ const Api = {
dockerfilePath: '/api/:version/templates/dockerfiles/:key', dockerfilePath: '/api/:version/templates/dockerfiles/:key',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json', usersPath: '/api/:version/users.json',
userStatusPath: '/api/:version/user/status',
commitPath: '/api/:version/projects/:id/repository/commits', commitPath: '/api/:version/projects/:id/repository/commits',
commitPipelinesPath: '/:project_id/commit/:sha/pipelines', commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
...@@ -268,6 +269,15 @@ const Api = { ...@@ -268,6 +269,15 @@ const Api = {
}); });
}, },
postUserStatus({ emoji, message }) {
const url = Api.buildUrl(this.userStatusPath);
return axios.put(url, {
emoji,
message,
});
},
templates(key, params = {}) { templates(key, params = {}) {
const url = Api.buildUrl(this.templatesPath).replace(':key', key); const url = Api.buildUrl(this.templatesPath).replace(':key', key);
......
...@@ -42,10 +42,11 @@ export class AwardsHandler { ...@@ -42,10 +42,11 @@ export class AwardsHandler {
} }
bindEvents() { bindEvents() {
const $parentEl = this.targetContainerEl ? $(this.targetContainerEl) : $(document);
// If the user shows intent let's pre-build the menu // If the user shows intent let's pre-build the menu
this.registerEventListener( this.registerEventListener(
'one', 'one',
$(document), $parentEl,
'mouseenter focus', 'mouseenter focus',
this.toggleButtonSelector, this.toggleButtonSelector,
'mouseenter focus', 'mouseenter focus',
...@@ -58,7 +59,7 @@ export class AwardsHandler { ...@@ -58,7 +59,7 @@ export class AwardsHandler {
} }
}, },
); );
this.registerEventListener('on', $(document), 'click', this.toggleButtonSelector, e => { this.registerEventListener('on', $parentEl, 'click', this.toggleButtonSelector, e => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.showEmojiMenu($(e.currentTarget)); this.showEmojiMenu($(e.currentTarget));
...@@ -76,7 +77,7 @@ export class AwardsHandler { ...@@ -76,7 +77,7 @@ export class AwardsHandler {
}); });
const emojiButtonSelector = `.js-awards-block .js-emoji-btn, .${this.menuClass} .js-emoji-btn`; const emojiButtonSelector = `.js-awards-block .js-emoji-btn, .${this.menuClass} .js-emoji-btn`;
this.registerEventListener('on', $(document), 'click', emojiButtonSelector, e => { this.registerEventListener('on', $parentEl, 'click', emojiButtonSelector, e => {
e.preventDefault(); e.preventDefault();
const $target = $(e.currentTarget); const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji'); const $glEmojiElement = $target.find('gl-emoji');
...@@ -168,7 +169,8 @@ export class AwardsHandler { ...@@ -168,7 +169,8 @@ export class AwardsHandler {
</div> </div>
`; `;
document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup); const targetEl = this.targetContainerEl ? this.targetContainerEl : document.body;
targetEl.insertAdjacentHTML('beforeend', emojiMenuMarkup);
this.addRemainingEmojiMenuCategories(); this.addRemainingEmojiMenuCategories();
this.setupSearch(); this.setupSearch();
...@@ -250,6 +252,12 @@ export class AwardsHandler { ...@@ -250,6 +252,12 @@ export class AwardsHandler {
} }
positionMenu($menu, $addBtn) { positionMenu($menu, $addBtn) {
if (this.targetContainerEl) {
return $menu.css({
top: `${$addBtn.outerHeight()}px`,
});
}
const position = $addBtn.data('position'); const position = $addBtn.data('position');
// The menu could potentially be off-screen or in a hidden overflow element // The menu could potentially be off-screen or in a hidden overflow element
// So we position the element absolute in the body // So we position the element absolute in the body
...@@ -424,9 +432,7 @@ export class AwardsHandler { ...@@ -424,9 +432,7 @@ export class AwardsHandler {
users = origTitle.trim().split(FROM_SENTENCE_REGEX); users = origTitle.trim().split(FROM_SENTENCE_REGEX);
} }
users.unshift('You'); users.unshift('You');
return awardBlock return awardBlock.attr('title', this.toSentence(users)).tooltip('_fixTitle');
.attr('title', this.toSentence(users))
.tooltip('_fixTitle');
} }
createAwardButtonForVotesBlock(votesBlock, emojiName) { createAwardButtonForVotesBlock(votesBlock, emojiName) {
...@@ -609,13 +615,11 @@ export class AwardsHandler { ...@@ -609,13 +615,11 @@ export class AwardsHandler {
let awardsHandlerPromise = null; let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) { export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) { if (!awardsHandlerPromise || reload) {
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then( awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(Emoji => {
Emoji => { const awardsHandler = new AwardsHandler(Emoji);
const awardsHandler = new AwardsHandler(Emoji); awardsHandler.bindEvents();
awardsHandler.bindEvents(); return awardsHandler;
return awardsHandler; });
},
);
} }
return awardsHandlerPromise; return awardsHandlerPromise;
} }
<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: {
...@@ -126,21 +128,23 @@ export default { ...@@ -126,21 +128,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;
},
}; };
...@@ -244,6 +244,7 @@ export function getDiffPositionByLineCode(diffFiles) { ...@@ -244,6 +244,7 @@ export function getDiffPositionByLineCode(diffFiles) {
oldLine, oldLine,
newLine, newLine,
lineCode, lineCode,
positionType: 'text',
}; };
} }
}); });
...@@ -259,11 +260,57 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD ...@@ -259,11 +260,57 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD
const { lineCode, ...diffPositionCopy } = diffPosition; const { lineCode, ...diffPositionCopy } = diffPosition;
if (discussion.original_position && discussion.position) { if (discussion.original_position && discussion.position) {
const originalRefs = convertObjectPropsToCamelCase(discussion.original_position.formatter); const originalRefs = convertObjectPropsToCamelCase(discussion.original_position);
const refs = convertObjectPropsToCamelCase(discussion.position.formatter); const refs = convertObjectPropsToCamelCase(discussion.position);
return _.isEqual(refs, diffPositionCopy) || _.isEqual(originalRefs, diffPositionCopy); return _.isEqual(refs, diffPositionCopy) || _.isEqual(originalRefs, diffPositionCopy);
} }
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>
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { highCountTrim } from '~/lib/utils/text_utility'; import { highCountTrim } from '~/lib/utils/text_utility';
import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue';
import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue';
/** /**
* Updates todo counter when todos are toggled. * Updates todo counter when todos are toggled.
...@@ -17,3 +21,54 @@ export default function initTodoToggle() { ...@@ -17,3 +21,54 @@ export default function initTodoToggle() {
$todoPendingCount.toggleClass('hidden', parsedCount === 0); $todoPendingCount.toggleClass('hidden', parsedCount === 0);
}); });
} }
document.addEventListener('DOMContentLoaded', () => {
const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger');
const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper');
if (setStatusModalTriggerEl || setStatusModalWrapperEl) {
Vue.use(Translate);
// eslint-disable-next-line no-new
new Vue({
el: setStatusModalTriggerEl,
data() {
const { hasStatus } = this.$options.el.dataset;
return {
hasStatus: hasStatus === 'true',
};
},
render(createElement) {
return createElement(SetStatusModalTrigger, {
props: {
hasStatus: this.hasStatus,
},
});
},
});
// eslint-disable-next-line no-new
new Vue({
el: setStatusModalWrapperEl,
data() {
const { currentEmoji, currentMessage } = this.$options.el.dataset;
return {
currentEmoji,
currentMessage,
};
},
render(createElement) {
const { currentEmoji, currentMessage } = this;
return createElement(SetStatusModalWrapper, {
props: {
currentEmoji,
currentMessage,
},
});
},
});
}
});
...@@ -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);
}
} }
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
v-if="mergeRequest" v-if="mergeRequest"
:href="mergeRequest.path" :href="mergeRequest.path"
class="js-link-commit link-commit" class="js-link-commit link-commit"
>{{ mergeRequest.iid }}</a> >!{{ mergeRequest.iid }}</a>
</p> </p>
<p class="build-light-text append-bottom-0"> <p class="build-light-text append-bottom-0">
......
...@@ -2,9 +2,12 @@ ...@@ -2,9 +2,12 @@
import { mapGetters, mapState } from 'vuex'; import { mapGetters, mapState } from 'vuex';
import CiHeader from '~/vue_shared/components/header_ci_component.vue'; import CiHeader from '~/vue_shared/components/header_ci_component.vue';
import Callout from '~/vue_shared/components/callout.vue'; import Callout from '~/vue_shared/components/callout.vue';
<<<<<<< HEAD
// ee-only start // ee-only start
import SharedRunner from 'ee/jobs/components/shared_runner_limit_block.vue'; import SharedRunner from 'ee/jobs/components/shared_runner_limit_block.vue';
// ee-only end // ee-only end
=======
>>>>>>> upstream/master
import EnvironmentsBlock from './environments_block.vue'; import EnvironmentsBlock from './environments_block.vue';
import ErasedBlock from './erased_block.vue'; import ErasedBlock from './erased_block.vue';
import StuckBlock from './stuck_block.vue'; import StuckBlock from './stuck_block.vue';
...@@ -17,7 +20,10 @@ ...@@ -17,7 +20,10 @@
EnvironmentsBlock, EnvironmentsBlock,
ErasedBlock, ErasedBlock,
StuckBlock, StuckBlock,
<<<<<<< HEAD
SharedRunner, SharedRunner,
=======
>>>>>>> upstream/master
}, },
props: { props: {
runnerHelpUrl: { runnerHelpUrl: {
...@@ -35,7 +41,10 @@ ...@@ -35,7 +41,10 @@
'jobHasStarted', 'jobHasStarted',
'hasEnvironment', 'hasEnvironment',
'isJobStuck', 'isJobStuck',
<<<<<<< HEAD
'shouldRenderSharedRunnerLimitWarning', 'shouldRenderSharedRunnerLimitWarning',
=======
>>>>>>> upstream/master
]), ]),
}, },
}; };
...@@ -80,6 +89,7 @@ ...@@ -80,6 +89,7 @@
:runners-path="runnerHelpUrl" :runners-path="runnerHelpUrl"
/> />
<<<<<<< HEAD
<shared-runner <shared-runner
v-if="shouldRenderSharedRunnerLimitWarning" v-if="shouldRenderSharedRunnerLimitWarning"
class="js-shared-runner-limit" class="js-shared-runner-limit"
...@@ -88,6 +98,8 @@ ...@@ -88,6 +98,8 @@
:runners-path="runnerHelpUrl" :runners-path="runnerHelpUrl"
/> />
=======
>>>>>>> upstream/master
<environments-block <environments-block
v-if="hasEnvironment" v-if="hasEnvironment"
:deployment-status="job.deployment_status" :deployment-status="job.deployment_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 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 JobApp from './components/job_app.vue'; import JobApp from './components/job_app.vue';
<<<<<<< HEAD
import DetailsBlock from './components/sidebar_details_block.vue'; import DetailsBlock from './components/sidebar_details_block.vue';
=======
import Sidebar from './components/sidebar.vue';
>>>>>>> upstream/master
import createStore from './store'; import createStore from './store';
export default () => { export default () => {
...@@ -13,6 +18,7 @@ export default () => { ...@@ -13,6 +18,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
...@@ -44,17 +50,25 @@ export default () => { ...@@ -44,17 +50,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) =>
......
...@@ -38,10 +38,13 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status); ...@@ -38,10 +38,13 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status);
export const isJobStuck = state => export const isJobStuck = state =>
state.job.status.group === 'pending' && state.job.runners && state.job.runners.available === false; state.job.status.group === 'pending' && state.job.runners && state.job.runners.available === false;
<<<<<<< HEAD
// ee-only start // ee-only start
export const shouldRenderSharedRunnerLimitWarning = state => export const shouldRenderSharedRunnerLimitWarning = state =>
state.job.runner && state.job.runner.quota && state.job.ruuner.quota.used; state.job.runner && state.job.runner.quota && state.job.ruuner.quota.used;
// ee-only end // ee-only end
=======
>>>>>>> upstream/master
// 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 () => {};
...@@ -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 });
......
...@@ -126,8 +126,8 @@ export const unresolvedDiscussionsIdsByDiff = (state, getters) => ...@@ -126,8 +126,8 @@ export const unresolvedDiscussionsIdsByDiff = (state, getters) =>
const filenameComparison = a.diff_file.file_path.localeCompare(b.diff_file.file_path); const filenameComparison = a.diff_file.file_path.localeCompare(b.diff_file.file_path);
// Get the line numbers, to compare within the same file // Get the line numbers, to compare within the same file
const aLines = [a.position.formatter.new_line, a.position.formatter.old_line]; const aLines = [a.position.new_line, a.position.old_line];
const bLines = [b.position.formatter.new_line, b.position.formatter.old_line]; const bLines = [b.position.new_line, b.position.old_line];
return filenameComparison < 0 || return filenameComparison < 0 ||
(filenameComparison === 0 && (filenameComparison === 0 &&
......
...@@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => {
const statusEmojiField = document.getElementById('js-status-emoji-field'); const statusEmojiField = document.getElementById('js-status-emoji-field');
const statusMessageField = document.getElementById('js-status-message-field'); const statusMessageField = document.getElementById('js-status-message-field');
const toggleNoEmojiPlaceholder = (isVisible) => { const toggleNoEmojiPlaceholder = isVisible => {
const placeholderElement = document.getElementById('js-no-emoji-placeholder'); const placeholderElement = document.getElementById('js-no-emoji-placeholder');
placeholderElement.classList.toggle('hidden', !isVisible); placeholderElement.classList.toggle('hidden', !isVisible);
}; };
...@@ -69,5 +69,5 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -69,5 +69,5 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
}) })
.catch(() => createFlash('Failed to load emoji list!')); .catch(() => createFlash('Failed to load emoji list.'));
}); });
import { AwardsHandler } from '~/awards_handler';
class EmojiMenuInModal extends AwardsHandler {
constructor(emoji, toggleButtonSelector, menuClass, selectEmojiCallback, targetContainerEl) {
super(emoji);
this.selectEmojiCallback = selectEmojiCallback;
this.toggleButtonSelector = toggleButtonSelector;
this.menuClass = menuClass;
this.targetContainerEl = targetContainerEl;
this.bindEvents();
}
postEmoji($emojiButton, awardUrl, selectedEmoji, callback) {
this.selectEmojiCallback(selectedEmoji, this.emoji.glEmojiTag(selectedEmoji));
callback();
}
}
export default EmojiMenuInModal;
import Vue from 'vue';
export default new Vue();
<script>
import { s__ } from '~/locale';
import eventHub from './event_hub';
export default {
props: {
hasStatus: {
type: Boolean,
required: true,
},
},
computed: {
buttonText() {
return this.hasStatus ? s__('SetStatusModal|Edit status') : s__('SetStatusModal|Set status');
},
},
methods: {
openModal() {
eventHub.$emit('openModal');
},
},
};
</script>
<template>
<button
type="button"
class="btn menu-item"
@click="openModal"
>
{{ buttonText }}
</button>
</template>
<script>
import $ from 'jquery';
import createFlash from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
import GfmAutoComplete from '~/gfm_auto_complete';
import { __, s__ } from '~/locale';
import Api from '~/api';
import eventHub from './event_hub';
import EmojiMenuInModal from './emoji_menu_in_modal';
const emojiMenuClass = 'js-modal-status-emoji-menu';
export default {
components: {
Icon,
},
props: {
currentEmoji: {
type: String,
required: true,
},
currentMessage: {
type: String,
required: true,
},
},
data() {
return {
defaultEmojiTag: '',
emoji: this.currentEmoji,
emojiMenu: null,
emojiTag: '',
isEmojiMenuVisible: false,
message: this.currentMessage,
modalId: 'set-user-status-modal',
noEmoji: true,
};
},
computed: {
isDirty() {
return this.message.length || this.emoji.length;
},
},
mounted() {
eventHub.$on('openModal', this.openModal);
},
beforeDestroy() {
this.emojiMenu.destroy();
},
methods: {
openModal() {
this.$root.$emit('bv::show::modal', this.modalId);
},
closeModal() {
this.$root.$emit('bv::hide::modal', this.modalId);
},
setupEmojiListAndAutocomplete() {
const toggleEmojiMenuButtonSelector = '#set-user-status-modal .js-toggle-emoji-menu';
const emojiAutocomplete = new GfmAutoComplete();
emojiAutocomplete.setup($(this.$refs.statusMessageField), { emojis: true });
import(/* webpackChunkName: 'emoji' */ '~/emoji')
.then(Emoji => {
if (this.emoji) {
this.emojiTag = Emoji.glEmojiTag(this.emoji);
}
this.noEmoji = this.emoji === '';
this.defaultEmojiTag = Emoji.glEmojiTag('speech_balloon');
this.emojiMenu = new EmojiMenuInModal(
Emoji,
toggleEmojiMenuButtonSelector,
emojiMenuClass,
this.setEmoji,
this.$refs.userStatusForm,
);
})
.catch(() => createFlash(__('Failed to load emoji list.')));
},
showEmojiMenu() {
this.isEmojiMenuVisible = true;
this.emojiMenu.showEmojiMenu($(this.$refs.toggleEmojiMenuButton));
},
hideEmojiMenu() {
if (!this.isEmojiMenuVisible) {
return;
}
this.isEmojiMenuVisible = false;
this.emojiMenu.hideMenuElement($(`.${emojiMenuClass}`));
},
setDefaultEmoji() {
const { emojiTag } = this;
const hasStatusMessage = this.message;
if (hasStatusMessage && emojiTag) {
return;
}
if (hasStatusMessage) {
this.noEmoji = false;
this.emojiTag = this.defaultEmojiTag;
} else if (emojiTag === this.defaultEmojiTag) {
this.noEmoji = true;
this.clearEmoji();
}
},
setEmoji(emoji, emojiTag) {
this.emoji = emoji;
this.noEmoji = false;
this.clearEmoji();
this.emojiTag = emojiTag;
},
clearEmoji() {
if (this.emojiTag) {
this.emojiTag = '';
}
},
clearStatusInputs() {
this.emoji = '';
this.message = '';
this.noEmoji = true;
this.clearEmoji();
this.hideEmojiMenu();
},
removeStatus() {
this.clearStatusInputs();
this.setStatus();
},
setStatus() {
const { emoji, message } = this;
Api.postUserStatus({
emoji,
message,
})
.then(this.onUpdateSuccess)
.catch(this.onUpdateFail);
},
onUpdateSuccess() {
this.closeModal();
window.location.reload();
},
onUpdateFail() {
createFlash(
s__("SetStatusModal|Sorry, we weren't able to set your status. Please try again later."),
);
this.closeModal();
},
},
};
</script>
<template>
<gl-ui-modal
:title="s__('SetStatusModal|Set a status')"
:modal-id="modalId"
:ok-title="s__('SetStatusModal|Set status')"
:cancel-title="s__('SetStatusModal|Remove status')"
ok-variant="success"
class="set-user-status-modal"
@shown="setupEmojiListAndAutocomplete"
@hide="hideEmojiMenu"
@ok="setStatus"
@cancel="removeStatus"
>
<div>
<input
v-model="emoji"
class="js-status-emoji-field"
type="hidden"
name="user[status][emoji]"
/>
<div
ref="userStatusForm"
class="form-group position-relative m-0"
>
<div class="input-group">
<span class="input-group-btn">
<button
ref="toggleEmojiMenuButton"
v-gl-tooltip.bottom
:title="s__('SetStatusModal|Add status emoji')"
:aria-label="s__('SetStatusModal|Add status emoji')"
name="button"
type="button"
class="js-toggle-emoji-menu emoji-menu-toggle-button btn"
@click="showEmojiMenu"
>
<span v-html="emojiTag"></span>
<span
v-show="noEmoji"
class="js-no-emoji-placeholder no-emoji-placeholder position-relative"
>
<icon
name="emoji_slightly_smiling_face"
css-classes="award-control-icon-neutral"
/>
<icon
name="emoji_smiley"
css-classes="award-control-icon-positive"
/>
<icon
name="emoji_smile"
css-classes="award-control-icon-super-positive"
/>
</span>
</button>
</span>
<input
ref="statusMessageField"
v-model="message"
:placeholder="s__('SetStatusModal|What\'s your status?')"
type="text"
class="form-control form-control input-lg js-status-message-field"
name="user[status][message]"
@keyup="setDefaultEmoji"
@keyup.enter.prevent
@click="hideEmojiMenu"
/>
<span
v-show="isDirty"
class="input-group-btn"
>
<button
v-gl-tooltip.bottom
:title="s__('SetStatusModal|Clear status')"
:aria-label="s__('SetStatusModal|Clear status')"
name="button"
type="button"
class="js-clear-user-status-button clear-user-status btn"
@click="clearStatusInputs()"
>
<icon name="close" />
</button>
</span>
</div>
</div>
</div>
</gl-ui-modal>
</template>
...@@ -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>
......
...@@ -529,9 +529,10 @@ ...@@ -529,9 +529,10 @@
} }
.header-user { .header-user {
.dropdown-menu { &.show .dropdown-menu {
width: auto; width: auto;
min-width: unset; min-width: unset;
max-height: 323px;
margin-top: 4px; margin-top: 4px;
color: $gl-text-color; color: $gl-text-color;
left: auto; left: auto;
...@@ -542,6 +543,18 @@ ...@@ -542,6 +543,18 @@
.user-name { .user-name {
display: block; display: block;
} }
.user-status-emoji {
margin-right: 0;
display: block;
vertical-align: text-top;
max-width: 148px;
font-size: 12px;
gl-emoji {
font-size: $gl-font-size;
}
}
} }
svg { svg {
...@@ -573,3 +586,24 @@ ...@@ -573,3 +586,24 @@
} }
} }
} }
.set-user-status-modal {
.modal-body {
min-height: unset;
}
.input-lg {
max-width: unset;
}
.no-emoji-placeholder,
.clear-user-status {
svg {
fill: $gl-text-color-secondary;
}
}
.emoji-menu-toggle-button {
@include emoji-menu-toggle-button;
}
}
...@@ -356,3 +356,59 @@ ...@@ -356,3 +356,59 @@
border-radius: 50%; border-radius: 50%;
} }
} }
@mixin emoji-menu-toggle-button {
line-height: 1;
padding: 0;
min-width: 16px;
color: $gray-darkest;
fill: $gray-darkest;
.fa {
position: relative;
font-size: 16px;
}
svg {
@include btn-svg;
margin: 0;
}
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
top: 0;
left: 0;
opacity: 0;
}
&:hover,
&.is-active {
.danger-highlight {
color: $red-500;
}
.link-highlight {
color: $blue-600;
fill: $blue-600;
}
.award-control-icon-neutral {
opacity: 0;
}
.award-control-icon-positive {
opacity: 1;
}
}
&.is-active {
.award-control-icon-positive {
opacity: 0;
}
.award-control-icon-super-positive {
opacity: 1;
}
}
}
...@@ -322,7 +322,8 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%); ...@@ -322,7 +322,8 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
$monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', $monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
'Courier New', 'andale mono', 'lucida console', monospace; 'Courier New', 'andale mono', 'lucida console', monospace;
$regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, $regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
/* /*
* Dropdowns * Dropdowns
...@@ -666,5 +667,4 @@ Modals ...@@ -666,5 +667,4 @@ Modals
$modal-body-height: 134px; $modal-body-height: 134px;
$modal-border-color: #e9ecef; $modal-border-color: #e9ecef;
$priority-label-empty-state-width: 114px; $priority-label-empty-state-width: 114px;
...@@ -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 {
......
...@@ -262,23 +262,6 @@ ...@@ -262,23 +262,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;
...@@ -315,15 +298,11 @@ ...@@ -315,15 +298,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 {
......
...@@ -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 {
......
...@@ -519,59 +519,7 @@ ul.notes { ...@@ -519,59 +519,7 @@ ul.notes {
} }
.note-action-button { .note-action-button {
line-height: 1; @include emoji-menu-toggle-button;
padding: 0;
min-width: 16px;
color: $gray-darkest;
fill: $gray-darkest;
.fa {
position: relative;
font-size: 16px;
}
svg {
@include btn-svg;
margin: 0;
}
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
top: 0;
left: 0;
opacity: 0;
}
&:hover,
&.is-active {
.danger-highlight {
color: $red-500;
}
.link-highlight {
color: $blue-600;
fill: $blue-600;
}
.award-control-icon-neutral {
opacity: 0;
}
.award-control-icon-positive {
opacity: 1;
}
}
&.is-active {
.award-control-icon-positive {
opacity: 0;
}
.award-control-icon-super-positive {
opacity: 1;
}
}
} }
.discussion-toggle-button { .discussion-toggle-button {
......
...@@ -81,14 +81,14 @@ ...@@ -81,14 +81,14 @@
// Middle dot divider between each element in a list of items. // Middle dot divider between each element in a list of items.
.middle-dot-divider { .middle-dot-divider {
&::after { &::after {
content: "\00B7"; // Middle Dot content: '\00B7'; // Middle Dot
padding: 0 6px; padding: 0 6px;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
&:last-child { &:last-child {
&::after { &::after {
content: ""; content: '';
padding: 0; padding: 0;
} }
} }
...@@ -191,7 +191,6 @@ ...@@ -191,7 +191,6 @@
@include media-breakpoint-down(xs) { @include media-breakpoint-down(xs) {
width: auto; width: auto;
} }
} }
.profile-crop-image-container { .profile-crop-image-container {
...@@ -215,7 +214,6 @@ ...@@ -215,7 +214,6 @@
} }
} }
.user-profile { .user-profile {
.cover-controls a { .cover-controls a {
margin-left: 5px; margin-left: 5px;
...@@ -415,7 +413,7 @@ table.u2f-registrations { ...@@ -415,7 +413,7 @@ table.u2f-registrations {
} }
&.unverified { &.unverified {
@include status-color($gray-dark, color("gray"), $common-gray-dark); @include status-color($gray-dark, color('gray'), $common-gray-dark);
} }
} }
} }
...@@ -428,7 +426,7 @@ table.u2f-registrations { ...@@ -428,7 +426,7 @@ table.u2f-registrations {
} }
.emoji-menu-toggle-button { .emoji-menu-toggle-button {
@extend .note-action-button; @include emoji-menu-toggle-button;
.no-emoji-placeholder { .no-emoji-placeholder {
position: relative; position: relative;
......
...@@ -831,6 +831,14 @@ ...@@ -831,6 +831,14 @@
} }
} }
.repository-language-bar-tooltip-language {
font-weight: $gl-font-weight-bold;
}
.repository-language-bar-tooltip-share {
color: $theme-gray-400;
}
pre.light-well { pre.light-well {
border-color: $well-light-border; border-color: $well-light-border;
} }
......
...@@ -69,8 +69,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -69,8 +69,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end end
end end
def reset_runners_token def reset_registration_token
@application_setting.reset_runners_registration_token! @application_setting.reset_runners_registration_token!
flash[:notice] = 'New runners registration token has been generated!' flash[:notice] = 'New runners registration token has been generated!'
redirect_to admin_runners_path redirect_to admin_runners_path
end end
......
...@@ -10,6 +10,13 @@ module Groups ...@@ -10,6 +10,13 @@ module Groups
define_secret_variables define_secret_variables
end end
def reset_registration_token
@group.reset_runners_token!
flash[:notice] = 'New runners registration token has been generated!'
redirect_to group_settings_ci_cd_path
end
private private
def define_secret_variables def define_secret_variables
......
...@@ -38,6 +38,13 @@ module Projects ...@@ -38,6 +38,13 @@ module Projects
end end
end end
def reset_registration_token
@project.reset_runners_token!
flash[:notice] = 'New runners registration token has been generated!'
redirect_to namespace_project_settings_ci_cd_path
end
private private
def update_params def update_params
......
...@@ -13,8 +13,4 @@ module ClustersHelper ...@@ -13,8 +13,4 @@ module ClustersHelper
render 'projects/clusters/gcp_signup_offer_banner' render 'projects/clusters/gcp_signup_offer_banner'
end end
end end
def rbac_clusters_feature_enabled?
Feature.enabled?(:rbac_clusters)
end
end end
...@@ -13,6 +13,7 @@ module RepositoryLanguagesHelper ...@@ -13,6 +13,7 @@ module RepositoryLanguagesHelper
content_tag :div, nil, content_tag :div, nil,
class: "progress-bar has-tooltip", class: "progress-bar has-tooltip",
style: "width: #{lang.share}%; background-color:#{lang.color}", style: "width: #{lang.share}%; background-color:#{lang.color}",
title: lang.name data: { html: true },
title: "<span class=\"repository-language-bar-tooltip-language\">#{escape_javascript(lang.name)}</span>&nbsp;<span class=\"repository-language-bar-tooltip-share\">#{lang.share.round(1)}%</span>"
end end
end end
...@@ -640,6 +640,10 @@ module Ci ...@@ -640,6 +640,10 @@ module Ci
end end
end end
def default_branch?
ref == project.default_branch
end
private private
def ci_yaml_from_repo def ci_yaml_from_repo
......
...@@ -64,10 +64,10 @@ class InstanceConfiguration ...@@ -64,10 +64,10 @@ class InstanceConfiguration
end end
def ssh_algorithm_md5(ssh_file_content) def ssh_algorithm_md5(ssh_file_content)
OpenSSL::Digest::MD5.hexdigest(ssh_file_content).scan(/../).join(':') Gitlab::SSHPublicKey.new(ssh_file_content).fingerprint
end end
def ssh_algorithm_sha256(ssh_file_content) def ssh_algorithm_sha256(ssh_file_content)
OpenSSL::Digest::SHA256.hexdigest(ssh_file_content) Gitlab::SSHPublicKey.new(ssh_file_content).fingerprint('SHA256')
end end
end end
...@@ -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
......
...@@ -2,106 +2,103 @@ ...@@ -2,106 +2,103 @@
- @no_container = true - @no_container = true
%div{ class: container_class } %div{ class: container_class }
.bs-callout .row
%p .col-sm-6
= (_"A 'Runner' is a process which runs a job. You can set up as many Runners as you need.") .bs-callout
%br %p
= _('Runners can be placed on separate users, servers, even on your local machine.') = (_"A 'Runner' is a process which runs a job. You can set up as many Runners as you need.")
%br %br
= _('Runners can be placed on separate users, servers, even on your local machine.')
%br
%div %div
%span= _('Each Runner can be in one of the following states:') %span= _('Each Runner can be in one of the following states:')
%ul %ul
%li %li
%span.badge.badge-success shared %span.badge.badge-success shared
\- \-
= _('Runner runs jobs from all unassigned projects') = _('Runner runs jobs from all unassigned projects')
%li %li
%span.badge.badge-success group %span.badge.badge-success group
\- \-
= _('Runner runs jobs from all unassigned projects in its group') = _('Runner runs jobs from all unassigned projects in its group')
%li %li
%span.badge.badge-info specific %span.badge.badge-info specific
\- \-
= _('Runner runs jobs from assigned projects') = _('Runner runs jobs from assigned projects')
%li %li
%span.badge.badge-warning locked %span.badge.badge-warning locked
\- \-
= _('Runner cannot be assigned to other projects') = _('Runner cannot be assigned to other projects')
%li %li
%span.badge.badge-danger paused %span.badge.badge-danger paused
\- \-
= _('Runner will not receive any new jobs') = _('Runner will not receive any new jobs')
.bs-callout.clearfix .col-sm-6
.float-left .bs-callout
%p = render partial: 'ci/runner/how_to_setup_runner',
= _('You can reset runners registration token by pressing a button below.') locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token,
.prepend-top-10 type: 'shared',
= button_to _('Reset runners registration token'), reset_runners_token_admin_application_settings_path, reset_token_url: reset_registration_token_admin_application_settings_path }
method: :put, class: 'btn btn-default',
data: { confirm: _('Are you sure you want to reset registration token?') }
= render partial: 'ci/runner/how_to_setup_shared_runner', .row
locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token } .col-sm-9
= form_tag admin_runners_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do
.filtered-search-wrapper
.filtered-search-box
= dropdown_tag(custom_icon('icon_history'),
options: { wrapper_class: 'filtered-search-history-dropdown-wrapper',
toggle_class: 'filtered-search-history-dropdown-toggle-button',
dropdown_class: 'filtered-search-history-dropdown',
content_class: 'filtered-search-history-dropdown-content',
title: _('Recent searches') }) do
.js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } }
.filtered-search-box-input-container.droplab-dropdown
.scroll-container
%ul.tokens-container.list-unstyled
%li.input-token
%input.form-control.filtered-search{ { id: 'filtered-search-runners', placeholder: _('Search or filter results...') } }
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
= button_tag class: %w[btn btn-link] do
= sprite_icon('search')
%span
= _('Press Enter or click to search')
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
= button_tag class: %w[btn btn-link] do
-# Encapsulate static class name `{{icon}}` inside #{} to bypass
-# haml lint's ClassAttributeWithStaticValue
%svg
%use{ 'xlink:href': "#{'{{icon}}'}" }
%span.js-filter-hint
{{hint}}
%span.js-filter-tag.dropdown-light-content
{{tag}}
.bs-callout #js-dropdown-admin-runner-status.filtered-search-input-dropdown-menu.dropdown-menu
%p %ul{ data: { dropdown: true } }
= _('Runners currently online: %{active_runners_count}') % { active_runners_count: @active_runners_count } - Ci::Runner::AVAILABLE_STATUSES.each do |status|
%li.filter-dropdown-item{ data: { value: status } }
= button_tag class: %w[btn btn-link] do
= status.titleize
.row-content-block.second-block #js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu
= form_tag admin_runners_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do %ul{ data: { dropdown: true } }
.filtered-search-wrapper - Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
.filtered-search-box %li.filter-dropdown-item{ data: { value: runner_type } }
= dropdown_tag(custom_icon('icon_history'), = button_tag class: %w[btn btn-link] do
options: { wrapper_class: 'filtered-search-history-dropdown-wrapper', = runner_type.titleize
toggle_class: 'filtered-search-history-dropdown-toggle-button',
dropdown_class: 'filtered-search-history-dropdown',
content_class: 'filtered-search-history-dropdown-content',
title: _('Recent searches') }) do
.js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } }
.filtered-search-box-input-container.droplab-dropdown
.scroll-container
%ul.tokens-container.list-unstyled
%li.input-token
%input.form-control.filtered-search{ { id: 'filtered-search-runners', placeholder: _('Search or filter results...') } }
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
= button_tag class: %w[btn btn-link] do
= sprite_icon('search')
%span
= _('Press Enter or click to search')
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
= button_tag class: %w[btn btn-link] do
-# Encapsulate static class name `{{icon}}` inside #{} to bypass
-# haml lint's ClassAttributeWithStaticValue
%svg
%use{ 'xlink:href': "#{'{{icon}}'}" }
%span.js-filter-hint
{{hint}}
%span.js-filter-tag.dropdown-light-content
{{tag}}
#js-dropdown-admin-runner-status.filtered-search-input-dropdown-menu.dropdown-menu = button_tag class: %w[clear-search hidden] do
%ul{ data: { dropdown: true } } = icon('times')
- Ci::Runner::AVAILABLE_STATUSES.each do |status| .filter-dropdown-container
%li.filter-dropdown-item{ data: { value: status } } = render 'sort_dropdown'
= button_tag class: %w[btn btn-link] do
= status.titleize
#js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu .col-sm-3.text-right-lg
%ul{ data: { dropdown: true } } = _('Runners currently online: %{active_runners_count}') % { active_runners_count: @active_runners_count }
- Ci::Runner::AVAILABLE_TYPES.each do |runner_type|
%li.filter-dropdown-item{ data: { value: runner_type } }
= button_tag class: %w[btn btn-link] do
= runner_type.titleize
= button_tag class: %w[clear-search hidden] do
= icon('times')
.filter-dropdown-container
= render 'sort_dropdown'
- if @runners.any? - if @runners.any?
.runners-content.content-list .runners-content.content-list
......
...@@ -13,5 +13,9 @@ ...@@ -13,5 +13,9 @@
= _("Use the following registration token during setup:") = _("Use the following registration token during setup:")
%code#registration_token= registration_token %code#registration_token= registration_token
= clipboard_button(target: '#registration_token', title: _("Copy token to clipboard"), class: "btn-transparent btn-clipboard") = clipboard_button(target: '#registration_token', title: _("Copy token to clipboard"), class: "btn-transparent btn-clipboard")
.prepend-top-10.append-bottom-10
= button_to _("Reset runners registration token"), reset_token_url,
method: :put, class: 'btn btn-default',
data: { confirm: _("Are you sure you want to reset registration token?") }
%li %li
= _("Start the Runner!") = _("Start the Runner!")
.bs-callout.help-callout
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: registration_token, type: 'shared' }
.bs-callout.help-callout
.append-bottom-10
%h4= _('Set up a specific Runner automatically')
%p
- link_to_help_page = link_to(_('Learn more about Kubernetes'),
help_page_path('user/project/clusters/index'),
target: '_blank',
rel: 'noopener noreferrer')
= _('You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page }
%ol
%li
= _('Click the button below to begin the install process by navigating to the Kubernetes page')
%li
= _('Select an existing Kubernetes cluster or create a new one')
%li
= _('From the Kubernetes cluster details view, install Runner from the applications list')
= link_to _('Install Runner on Kubernetes'),
project_clusters_path(@project),
class: 'btn btn-info'
%hr
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: registration_token, type: 'specific' }
...@@ -11,7 +11,9 @@ ...@@ -11,7 +11,9 @@
-# https://gitlab.com/gitlab-org/gitlab-ce/issues/45894 -# https://gitlab.com/gitlab-org/gitlab-ce/issues/45894
- if can?(current_user, :admin_pipeline, @group) - if can?(current_user, :admin_pipeline, @group)
= render partial: 'ci/runner/how_to_setup_runner', = render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: @group.runners_token, type: 'group' } locals: { registration_token: @group.runners_token,
type: 'group',
reset_token_url: reset_registration_token_group_settings_ci_cd_path }
- if @group.runners.empty? - if @group.runners.empty?
%h4.underlined-title %h4.underlined-title
......
...@@ -5,7 +5,14 @@ ...@@ -5,7 +5,14 @@
.user-name.bold .user-name.bold
= current_user.name = current_user.name
= current_user.to_reference = current_user.to_reference
- if current_user.status
.user-status-emoji.str-truncated.has-tooltip{ title: current_user.status.message_html, data: { html: 'true', placement: 'bottom' } }
= emoji_icon current_user.status.emoji
= current_user.status.message_html.html_safe
%li.divider %li.divider
- if can?(current_user, :update_user_status, current_user)
%li
.js-set-status-modal-trigger{ data: { has_status: current_user.status.present? ? 'true' : 'false' } }
- if current_user_menu?(:profile) - if current_user_menu?(:profile)
%li %li
= link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username } = link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username }
......
...@@ -74,3 +74,6 @@ ...@@ -74,3 +74,6 @@
%span.sr-only= _('Toggle navigation') %span.sr-only= _('Toggle navigation')
= sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right') = sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right')
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left') = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
- if can?(current_user, :update_user_status, current_user)
.js-set-status-modal-wrapper{ data: { current_emoji: current_user.status.present? ? current_user.status.emoji : '', current_message: current_user.status.present? ? current_user.status.message : '' } }
...@@ -25,7 +25,12 @@ ...@@ -25,7 +25,12 @@
= render 'bitbucket_import_modal' = render 'bitbucket_import_modal'
- if bitbucket_server_import_enabled? - if bitbucket_server_import_enabled?
%div %div
<<<<<<< HEAD
= 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 = 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
=======
= 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
>>>>>>> upstream/master
= icon('bitbucket-square', text: 'Bitbucket Server') = icon('bitbucket-square', text: 'Bitbucket Server')
%div %div
- if gitlab_import_enabled? - if gitlab_import_enabled?
...@@ -54,7 +59,11 @@ ...@@ -54,7 +59,11 @@
- if git_import_enabled? - if git_import_enabled?
%div %div
<<<<<<< HEAD
%button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active', track_label: "#{track_label}" , track_event: "click_button", track_property: "repo_url" } } %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active', track_label: "#{track_label}" , track_event: "click_button", track_property: "repo_url" } }
=======
%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" } } }
>>>>>>> upstream/master
= icon('git', text: 'Repo by URL') = icon('git', text: 'Repo by URL')
- if manifest_import_enabled? - if manifest_import_enabled?
......
...@@ -61,5 +61,9 @@ ...@@ -61,5 +61,9 @@
.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.
<<<<<<< HEAD
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4, data: { track_label: "#{track_label}", track_event: "click_button", track_property: "create_project", track_value: "" } = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4, data: { track_label: "#{track_label}", track_event: "click_button", track_property: "create_project", track_value: "" }
=======
= 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: "" }
>>>>>>> upstream/master
= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "cancel" } = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "cancel" }
...@@ -61,15 +61,14 @@ ...@@ -61,15 +61,14 @@
%p.form-text.text-muted %p.form-text.text-muted
= s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end } = s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end }
- if rbac_clusters_feature_enabled? .form-group
.form-group .form-check
.form-check = provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true
= provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
= provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' .form-text.text-muted
.form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank'
= link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank'
.form-group .form-group
= field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true
...@@ -37,14 +37,13 @@ ...@@ -37,14 +37,13 @@
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
- if rbac_clusters_feature_enabled? .form-group
.form-group .form-check
.form-check = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
= platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' .form-text.text-muted
.form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
.form-group .form-group
= field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success' = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
...@@ -25,15 +25,14 @@ ...@@ -25,15 +25,14 @@
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold' = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
- if rbac_clusters_feature_enabled? .form-group
.form-group .form-check
.form-check = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input' }, 'rbac', 'abac'
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input' }, 'rbac', 'abac' = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
= platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' .form-text.text-muted
.form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank'
= link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank'
.form-group .form-group
= field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success' = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success'
...@@ -26,14 +26,13 @@ ...@@ -26,14 +26,13 @@
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold' = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
- if rbac_clusters_feature_enabled? .form-group
.form-group .form-check
.form-check = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold'
= platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' .form-text.text-muted
.form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
.form-group .form-group
= field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success' = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
...@@ -49,15 +49,16 @@ ...@@ -49,15 +49,16 @@
.environments-container .environments-container
- if @deployments.blank? - if @deployments.blank?
.blank-state-row .empty-state
.blank-state-center .text-content
%h2.blank-state-title %h4.state-title
You don't have any deployments right now. You don't have any deployments right now.
%p.blank-state-text %p.blank-state-text
Define environments in the deploy stage(s) in Define environments in the deploy stage(s) in
%code .gitlab-ci.yml %code .gitlab-ci.yml
to track deployments here. to track deployments here.
= link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success" .text-center
= link_to _("Read more"), help_page_path("ci/environments"), class: "btn btn-success"
- else - else
.table-holder .table-holder
.ci-table.environments{ role: 'grid' } .ci-table.environments{ role: 'grid' }
......
%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,6 +9,7 @@ ...@@ -9,6 +9,7 @@
%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
<<<<<<< HEAD
- if @build.stuck? - if @build.stuck?
- unless @build.any_runners_online? - unless @build.any_runners_online?
.bs-callout.bs-callout-warning.js-build-stuck .bs-callout.bs-callout-warning.js-build-stuck
...@@ -59,6 +60,8 @@ ...@@ -59,6 +60,8 @@
Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)} Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
- else - else
Job has been erased #{time_ago_with_tooltip(@build.erased_at)} Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
=======
>>>>>>> upstream/master
- if @build.running? || @build.has_trace? - if @build.running? || @build.has_trace?
.build-trace-container.prepend-top-default .build-trace-container.prepend-top-default
...@@ -95,7 +98,7 @@ ...@@ -95,7 +98,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 }
......
...@@ -10,10 +10,17 @@ ...@@ -10,10 +10,17 @@
= 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 }
<<<<<<< HEAD
%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" } } 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 } } data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } }
=======
%input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } }
%span
= _("Use template")
%a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } }
>>>>>>> upstream/master
= _("Preview") = _("Preview")
%h3 %h3
= _('Specific Runners') = _('Specific Runners')
= render partial: 'ci/runner/how_to_setup_specific_runner', .bs-callout.help-callout
locals: { registration_token: @project.runners_token } .append-bottom-10
%h4= _('Set up a specific Runner automatically')
%p
- link_to_help_page = link_to(_('Learn more about Kubernetes'),
help_page_path('user/project/clusters/index'),
target: '_blank',
rel: 'noopener noreferrer')
= _('You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page }
%ol
%li
= _('Click the button below to begin the install process by navigating to the Kubernetes page')
%li
= _('Select an existing Kubernetes cluster or create a new one')
%li
= _('From the Kubernetes cluster details view, install Runner from the applications list')
= link_to _('Install Runner on Kubernetes'),
project_clusters_path(@project),
class: 'btn btn-info'
%hr
= render partial: 'ci/runner/how_to_setup_runner',
locals: { registration_token: @project.runners_token,
type: 'specific',
reset_token_url: reset_registration_token_namespace_project_settings_ci_cd_path }
- if @project_runners.any? - if @project_runners.any?
%h4.underlined-title Runners activated for this project %h4.underlined-title Runners activated for this project
......
...@@ -3,16 +3,6 @@ ...@@ -3,16 +3,6 @@
= form_for @project, url: project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings') do |f| = form_for @project, url: project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings') do |f|
= form_errors(@project) = form_errors(@project)
%fieldset.builds-feature %fieldset.builds-feature
.form-group.append-bottom-default.js-secret-runner-token
= f.label :runners_token, _("Runner token"), class: 'label-bold'
.form-control.js-secret-value-placeholder
= '*' * 20
= f.text_field :runners_token, class: "form-control hide js-secret-value", placeholder: 'xEeFCaDAB89'
%p.form-text.text-muted= _("The secure token used by the Runner to checkout the project")
%button.btn.btn-info.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: 'false' } }
= _('Reveal value')
%hr
.form-group .form-group
%h5.prepend-top-0 %h5.prepend-top-0
= _("Git strategy for pipelines") = _("Git strategy for pipelines")
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
%button.btn.js-settings-toggle{ type: 'button' } %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand') = expanded ? _('Collapse') : _('Expand')
%p %p
= _("Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.") = _("Customize your pipeline configuration, view your pipeline status and coverage report.")
.settings-content .settings-content
= render 'form' = render 'form'
......
...@@ -3,8 +3,12 @@ ...@@ -3,8 +3,12 @@
- 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)] }
<<<<<<< HEAD
= 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}" } data: { track_label: "blank_project", track_event: "activate_form_input", track_property: "#{model_method}", track_value: "#{level}" }
=======
= 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}" }
>>>>>>> upstream/master
= 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
......
---
title: Show percentage of language detection on the language bar
merge_request: 22056
author: Johann Hubert Sonntagbauer
type: added
---
title: Instance Configuration page now displays correct SSH fingerprints
merge_request: 22081
author:
type: fixed
---
title: Simplify runner registration token resetting
merge_request: 21658
author:
type: changed
---
title: Support db migration and initialization for Auto DevOps
merge_request: 21955
author:
type: added
---
title: Set user status from within user menu
merge_request: 21643
author:
type: added
---
title: Remove 'rbac_clusters' feature flag
merge_request: 22096
author:
type: changed
---
title: Includes commit stats in POST project commits API
merge_request: 21968
author: Jacopo Beschi @jacopo-beschi
type: fixed
---
title: Fix loading issue on some merge request discussion
merge_request: 21982
author:
type: fixed
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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