Commit 011ffcfd authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 38869-datetime

* master: (85 commits)
  Export old code into es6 modules
  Use relative URL for projects to avoid storing domains
  Fix spec by avoiding monkeypatching
  Return the noteable in Note#touch_noteable
  add CHANGELOG.md entry for !15889
  fix broken empty state assets for environment monitoring page
  removed tab indexes from tag form
  Refactor entrypoint override docs
  Present member collection at the controller level
  Adds i18n
  check the import_status field before doing SQL operations to check the import url
  Fix rubocop offence
  Fix entrypoint overriding documentation
  Remove unnecessary js-issuable-edit
  Use memoization for commits on diffs
  Move Repository#write_ref to Git::Repository#write_ref
  Treat empty markdown and html strings as valid cached text, not missing cache that needs to be updated
  Center cluster title
  Remove noisy notification from QA base page
  Remove unused page from hashed storage QA scenario
  ...
parents 3c19c971 eacf99af
<svg width="24" height="30" viewBox="0 0 24 30" xmlns="http://www.w3.org/2000/svg"><title>cursor</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#1F78D1" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#FFF"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787c-.91 0-1.763.156-2.558.469-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068 0-.009.01-.031.033-.067a.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26.094-.126.168-.24.221-.342.054-.103.114-.235.181-.395.067-.161.125-.33.174-.51-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#1F78D1" fill-rule="nonzero"/></g></svg>
<svg width="48" height="60" viewBox="0 0 48 60" xmlns="http://www.w3.org/2000/svg"><title>cursor_2x</title><g fill="none" fill-rule="evenodd"><path d="M48 24.21C48 37.583 36.522 47.369 24 60 11.478 47.368 0 37.582 0 24.21 0 10.84 10.745 0 24 0s24 10.84 24 24.21z" fill="#1F78D1" fill-rule="nonzero"/><path d="M30.56 50.497c2.915-2.95 5.078-5.268 6.947-7.493 5.703-6.788 8.406-12.53 8.406-18.793 0-12.223-9.815-22.124-21.913-22.124S2.087 11.988 2.087 24.211c0 6.263 2.703 12.005 8.406 18.793 1.87 2.225 4.032 4.544 6.947 7.493 1.022 1.035 4.432 4.426 6.56 6.55 2.128-2.124 5.538-5.515 6.56-6.55z" fill="#FFF"/><path d="M29.103 16.512c-1.58-.625-3.282-.938-5.103-.938-1.821 0-3.527.313-5.116.938-1.58.616-2.84 1.45-3.777 2.504-.928 1.054-1.393 2.192-1.393 3.415 0 1 .317 1.956.951 2.866.643.902 1.545 1.684 2.706 2.344l1.165.67-.362 1.286a9.603 9.603 0 0 1-.937 2.303 13.208 13.208 0 0 0 3.683-2.29l.576-.509.763.08c.616.072 1.196.108 1.741.108 1.821 0 3.522-.308 5.103-.925 1.589-.625 2.848-1.464 3.776-2.517.938-1.054 1.407-2.192 1.407-3.416 0-1.223-.469-2.361-1.407-3.415-.928-1.053-2.187-1.888-3.776-2.504zm5.29 1.62c1.071 1.313 1.607 2.746 1.607 4.3 0 1.553-.536 2.99-1.607 4.312-1.072 1.312-2.527 2.353-4.366 3.12-1.84.76-3.848 1.139-6.027 1.139a18.32 18.32 0 0 1-1.942-.107c-1.768 1.562-3.821 2.643-6.16 3.24-.438.126-.947.224-1.527.295h-.067a.521.521 0 0 1-.362-.147.649.649 0 0 1-.214-.362v-.013c-.027-.036-.032-.09-.014-.16.027-.072.036-.117.027-.135 0-.017.022-.062.067-.133a1.29 1.29 0 0 0 .08-.121c.01-.009.04-.045.094-.107a106.068 106.068 0 0 1 .522-.59c.215-.232.367-.401.456-.508.098-.099.236-.273.415-.523.188-.25.335-.477.442-.683.107-.205.228-.468.362-.79.134-.321.25-.66.348-1.018-1.402-.794-2.51-1.777-3.322-2.946C12.402 25.025 12 23.77 12 22.43c0-1.553.536-2.986 1.607-4.299 1.072-1.321 2.527-2.361 4.366-3.12 1.84-.768 3.848-1.152 6.027-1.152 2.179 0 4.188.384 6.027 1.152 1.84.759 3.294 1.799 4.366 3.12z" fill="#1F78D1" fill-rule="nonzero"/></g></svg>
{"iconCount":180,"spriteSize":82176,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]}
\ No newline at end of file
{"iconCount":181,"spriteSize":81482,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 38 38"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#FFF"/><path fill="#1F78D1" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 38 38"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#FFF"/><path fill="#1F78D1" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></svg>
\ No newline at end of file
......@@ -15,7 +15,7 @@ import GroupLabelSubscription from './group_label_subscription';
import BuildArtifacts from './build_artifacts';
import CILintEditor from './ci_lint_editor';
import groupsSelect from './groups_select';
/* global Search */
import Search from './search';
/* global Admin */
import NamespaceSelect from './namespace_select';
import NewCommitForm from './new_commit_form';
......@@ -24,7 +24,7 @@ import projectAvatar from './project_avatar';
/* global MergeRequest */
import Compare from './compare';
import initCompareAutocomplete from './compare_autocomplete';
/* global ProjectFindFile */
import ProjectFindFile from './project_find_file';
import ProjectNew from './project_new';
import projectImport from './project_import';
import Labels from './labels';
......@@ -91,6 +91,7 @@ import DueDateSelectors from './due_date_select';
import Diff from './diff';
import ProjectLabelSubscription from './project_label_subscription';
import ProjectVariables from './project_variables';
import SearchAutocomplete from './search_autocomplete';
(function() {
var Dispatcher;
......@@ -683,7 +684,7 @@ import ProjectVariables from './project_variables';
Dispatcher.prototype.initSearch = function() {
// Only when search form is present
if ($('.search').length) {
return new gl.SearchAutocomplete();
return new SearchAutocomplete();
}
};
......
......@@ -19,12 +19,9 @@ export function addImageBadge(containerEl, { coordinate, badgeText, noteId }) {
}
export function addImageCommentBadge(containerEl, { coordinate, noteId }) {
const buttonEl = createImageBadge(noteId, coordinate, ['image-comment-badge', 'inverted']);
const iconEl = document.createElement('i');
iconEl.className = 'fa fa-comment-o';
iconEl.setAttribute('aria-label', 'comment');
const buttonEl = createImageBadge(noteId, coordinate, ['image-comment-badge']);
buttonEl.innerHTML = gl.utils.spriteIcon('image-comment-dark');
buttonEl.appendChild(iconEl);
containerEl.appendChild(buttonEl);
}
......
......@@ -7,7 +7,7 @@ document.addEventListener('DOMContentLoaded', () => {
const initialDataEl = document.getElementById('js-issuable-app-initial-data');
const props = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"'));
$('.issuable-edit').on('click', (e) => {
$('.js-issuable-edit').on('click', (e) => {
e.preventDefault();
eventHub.$emit('open.form');
......
......@@ -60,15 +60,10 @@ import './notifications_dropdown';
import './notifications_form';
import './pager';
import './preview_markdown';
import './project_find_file';
import './project_import';
import './projects_dropdown';
import './projects_list';
import './syntax_highlight';
import './render_gfm';
import './right_sidebar';
import './search';
import './search_autocomplete';
import initBreadcrumbs from './breadcrumb';
import './dispatcher';
......
......@@ -10,6 +10,7 @@ import './mixins/line_conflict_actions';
import './components/diff_file_editor';
import './components/inline_conflict_lines';
import './components/parallel_conflict_lines';
import syntaxHighlight from '../syntax_highlight';
$(() => {
const INTERACTIVE_RESOLVE_MODE = 'interactive';
......@@ -53,7 +54,7 @@ $(() => {
mergeConflictsStore.setLoadingState(false);
this.$nextTick(() => {
$('.js-syntax-highlight').syntaxHighlight();
syntaxHighlight($('.js-syntax-highlight'));
});
});
},
......
......@@ -17,6 +17,7 @@ import Diff from './diff';
import {
localTimeAgo,
} from './lib/utils/datetime_utility';
import syntaxHighlight from './syntax_highlight';
/* eslint-disable max-len */
// MergeRequestTabs
......@@ -298,7 +299,7 @@ import {
}
localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight();
syntaxHighlight($('#diffs .js-syntax-highlight'));
if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) {
this.expandViewContainer();
......
......@@ -15,7 +15,7 @@
import issuableStateMixin from '../mixins/issuable_state';
export default {
name: 'issueCommentForm',
name: 'commentForm',
data() {
return {
note: '',
......
......@@ -2,7 +2,7 @@
import noteEditedText from './note_edited_text.vue';
import noteAwardsList from './note_awards_list.vue';
import noteAttachment from './note_attachment.vue';
import issueNoteForm from './issue_note_form.vue';
import noteForm from './note_form.vue';
import TaskList from '../../task_list';
import autosave from '../mixins/autosave';
......@@ -29,7 +29,7 @@
noteEditedText,
noteAwardsList,
noteAttachment,
issueNoteForm,
noteForm,
},
computed: {
noteBody() {
......@@ -87,7 +87,7 @@
<div
v-html="note.note_html"
class="note-text md"></div>
<issue-note-form
<note-form
v-if="isEditing"
ref="noteForm"
@handleFormUpdate="handleFormUpdate"
......
......@@ -2,12 +2,12 @@
import { mapActions, mapGetters } from 'vuex';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import issueNote from './issue_note.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteableNote from './noteable_note.vue';
import noteHeader from './note_header.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteEditedText from './note_edited_text.vue';
import issueNoteForm from './issue_note_form.vue';
import noteForm from './note_form.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave';
......@@ -25,12 +25,12 @@
};
},
components: {
issueNote,
noteableNote,
userAvatarLink,
noteHeader,
noteSignedOutWidget,
noteEditedText,
issueNoteForm,
noteForm,
placeholderNote,
placeholderSystemNote,
},
......@@ -86,7 +86,7 @@
return placeholderNote;
}
return issueNote;
return noteableNote;
},
componentData(note) {
return note.isPlaceholderNote ? note.notes[0] : note;
......@@ -209,7 +209,7 @@
type="button"
class="js-vue-discussion-reply btn btn-text-field"
title="Add a reply">Reply...</button>
<issue-note-form
<note-form
v-if="isReplying"
save-button-title="Comment"
:discussion="note"
......
......@@ -5,7 +5,7 @@
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue';
import noteActions from './note_actions.vue';
import issueNoteBody from './issue_note_body.vue';
import noteBody from './note_body.vue';
import eventHub from '../event_hub';
export default {
......@@ -26,7 +26,7 @@
userAvatarLink,
noteHeader,
noteActions,
issueNoteBody,
noteBody,
},
computed: {
...mapGetters([
......@@ -123,9 +123,7 @@
// we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content
this.note.note = noteText;
if (this.$refs.noteBody) {
this.$refs.noteBody.$refs.noteForm.note = noteText; // TODO: This could be better
}
this.$refs.noteBody.$refs.noteForm.note = noteText;
},
},
created() {
......@@ -174,7 +172,7 @@
@handleDelete="deleteHandler"
/>
</div>
<issue-note-body
<note-body
:note="note"
:can-edit="note.current_user.can_edit"
:is-editing="isEditing"
......
......@@ -4,16 +4,16 @@
import Flash from '../../flash';
import store from '../stores/';
import * as constants from '../constants';
import issueNote from './issue_note.vue';
import issueDiscussion from './issue_discussion.vue';
import noteableNote from './noteable_note.vue';
import noteableDiscussion from './noteable_discussion.vue';
import systemNote from '../../vue_shared/components/notes/system_note.vue';
import issueCommentForm from './issue_comment_form.vue';
import commentForm from './comment_form.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
name: 'issueNotesApp',
name: 'notesApp',
props: {
noteableData: {
type: Object,
......@@ -36,10 +36,10 @@
};
},
components: {
issueNote,
issueDiscussion,
noteableNote,
noteableDiscussion,
systemNote,
issueCommentForm,
commentForm,
loadingIcon,
placeholderNote,
placeholderSystemNote,
......@@ -69,10 +69,10 @@
}
return placeholderNote;
} else if (note.individual_note) {
return note.notes[0].system ? systemNote : issueNote;
return note.notes[0].system ? systemNote : noteableNote;
}
return issueDiscussion;
return noteableDiscussion;
},
getComponentData(note) {
return note.individual_note ? note.notes[0] : note;
......@@ -87,7 +87,7 @@
.then(() => this.checkLocationHash())
.catch(() => {
this.isLoading = false;
Flash('Something went wrong while fetching issue comments. Please try again.');
Flash('Something went wrong while fetching comments. Please try again.');
});
},
initPolling() {
......@@ -147,6 +147,6 @@
/>
</ul>
<issue-comment-form />
<comment-form />
</div>
</template>
import Vue from 'vue';
import issueNotesApp from './components/issue_notes_app.vue';
import notesApp from './components/notes_app.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#js-vue-notes',
components: {
issueNotesApp,
notesApp,
},
data() {
const notesDataset = document.getElementById('js-vue-notes').dataset;
......@@ -32,7 +32,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
};
},
render(createElement) {
return createElement('issue-notes-app', {
return createElement('notes-app', {
props: {
noteableData: this.noteableData,
notesData: this.notesData,
......
<script>
export default {
export default {
props: {
helpPagePath: {
type: String,
......@@ -10,28 +10,33 @@ export default {
required: true,
},
},
};
};
</script>
<template>
<div class="row empty-state js-empty-state">
<div class="col-xs-12">
<div class="svg-content">
<img :src="emptyStateSvgPath"/>
<div class="svg-content svg-250">
<img :src="emptyStateSvgPath" />
</div>
</div>
<div class="col-xs-12 text-center">
<div class="col-xs-12">
<div class="text-content">
<h4>Build with confidence</h4>
<h4 class="text-center">
{{ s__("Pipelines|Build with confidence") }}
</h4>
<p>
Continous Integration can help catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver code to your product environment.
{{ s__("Pipelines|Continous Integration can help catch bugs by running your tests automatically, while Continuous Deployment can help you deliver code to your product environment.") }}
</p>
<a :href="helpPagePath" class="btn btn-info">
Get started with Pipelines
<div class="text-center">
<a
:href="helpPagePath"
class="btn btn-info"
>
{{ s__("Pipelines|Get started with Pipelines") }}
</a>
</div>
</div>
</div>
</div>
</template>
......@@ -59,8 +59,26 @@
},
computed: {
status() {
return this.job && this.job.status ? this.job.status : {};
},
tooltipText() {
return `${this.job.name} - ${this.job.status.label}`;
const textBuilder = [];
if (this.job.name) {
textBuilder.push(this.job.name);
}
if (this.job.name && this.status.label) {
textBuilder.push('-');
}
if (this.status.label) {
textBuilder.push(`${this.job.status.label}`);
}
return textBuilder.join(' ');
},
/**
......@@ -78,8 +96,8 @@
<div class="ci-job-component">
<a
v-tooltip
v-if="job.status.has_details"
:href="job.status.details_path"
v-if="status.has_details"
:href="status.details_path"
:title="tooltipText"
:class="cssClassJobName"
data-container="body"
......@@ -95,6 +113,7 @@
<div
v-else
v-tooltip
class="js-job-component-tooltip"
:title="tooltipText"
:class="cssClassJobName"
data-container="body"
......@@ -108,18 +127,18 @@
<action-component
v-if="hasAction && !isDropdown"
:tooltip-text="job.status.action.title"
:link="job.status.action.path"
:action-icon="job.status.action.icon"
:action-method="job.status.action.method"
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
:action-method="status.action.method"
/>
<dropdown-action-component
v-if="hasAction && isDropdown"
:tooltip-text="job.status.action.title"
:link="job.status.action.path"
:action-icon="job.status.action.icon"
:action-method="job.status.action.method"
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
:action-method="status.action.method"
/>
</div>
</template>
import Vue from 'vue';
import PipelinesStore from './stores/pipelines_store';
import pipelinesComponent from './components/pipelines.vue';
import Translate from '../vue_shared/translate';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipelines-list-vue',
......
......@@ -2,11 +2,33 @@
import fuzzaldrinPlus from 'fuzzaldrin-plus';
(function() {
this.ProjectFindFile = (function() {
var highlighter;
// highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
const highlighter = function(element, text, matches) {
var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched;
lastIndex = 0;
highlightText = "";
matchedChars = [];
for (j = 0, len = matches.length; j < len; j += 1) {
matchIndex = matches[j];
unmatched = text.substring(lastIndex, matchIndex);
if (unmatched) {
if (matchedChars.length) {
element.append(matchedChars.join("").bold());
}
matchedChars = [];
element.append(document.createTextNode(unmatched));
}
matchedChars.push(text[matchIndex]);
lastIndex = matchIndex + 1;
}
if (matchedChars.length) {
element.append(matchedChars.join("").bold());
}
return element.append(document.createTextNode(text.substring(lastIndex)));
};
function ProjectFindFile(element1, options) {
export default class ProjectFindFile {
constructor(element1, options) {
this.element = element1;
this.options = options;
this.goToBlob = this.goToBlob.bind(this);
......@@ -23,7 +45,7 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
this.load(this.options.url);
}
ProjectFindFile.prototype.initEvent = function() {
initEvent() {
this.inputElement.off("keyup");
this.inputElement.on("keyup", (function(_this) {
return function(event) {
......@@ -38,18 +60,18 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
}
};
})(this));
};
}
ProjectFindFile.prototype.findFile = function() {
findFile() {
var result, searchText;
searchText = this.inputElement.val();
result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths;
return this.renderList(result, searchText);
// find file
};
}
// files pathes load
ProjectFindFile.prototype.load = function(url) {
load(url) {
return $.ajax({
url: url,
method: "get",
......@@ -63,10 +85,10 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
};
})(this)
});
};
}
// render result
ProjectFindFile.prototype.renderList = function(filePaths, searchText) {
renderList(filePaths, searchText) {
var blobItemUrl, filePath, html, i, j, len, matches, results;
this.element.find(".tree-table > tbody").empty();
results = [];
......@@ -79,39 +101,14 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
matches = fuzzaldrinPlus.match(filePath, searchText);
}
blobItemUrl = this.options.blobUrlTemplate + "/" + filePath;
html = this.makeHtml(filePath, matches, blobItemUrl);
html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl);
results.push(this.element.find(".tree-table > tbody").append(html));
}
return results;
};
// highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
highlighter = function(element, text, matches) {
var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched;
lastIndex = 0;
highlightText = "";
matchedChars = [];
for (j = 0, len = matches.length; j < len; j += 1) {
matchIndex = matches[j];
unmatched = text.substring(lastIndex, matchIndex);
if (unmatched) {
if (matchedChars.length) {
element.append(matchedChars.join("").bold());
}
matchedChars = [];
element.append(document.createTextNode(unmatched));
}
matchedChars.push(text[matchIndex]);
lastIndex = matchIndex + 1;
}
if (matchedChars.length) {
element.append(matchedChars.join("").bold());
}
return element.append(document.createTextNode(text.substring(lastIndex)));
};
// make tbody row html
ProjectFindFile.prototype.makeHtml = function(filePath, matches, blobItemUrl) {
static makeHtml(filePath, matches, blobItemUrl) {
var $tr;
$tr = $("<tr class='tree-item'><td class='tree-item-file-name link-container'><a><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'></span></a></td></tr>");
if (matches) {
......@@ -121,9 +118,9 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
$tr.find(".str-truncated").text(filePath);
}
return $tr;
};
}
ProjectFindFile.prototype.selectRow = function(type) {
selectRow(type) {
var next, rows, selectedRow;
rows = this.element.find(".files-slider tr.tree-item");
selectedRow = this.element.find(".files-slider tr.tree-item.selected");
......@@ -143,28 +140,25 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
}
return selectedRow.addClass("selected").focus();
}
};
}
ProjectFindFile.prototype.selectRowUp = function() {
selectRowUp() {
return this.selectRow("UP");
};
}
ProjectFindFile.prototype.selectRowDown = function() {
selectRowDown() {
return this.selectRow("DOWN");
};
}
ProjectFindFile.prototype.goToTree = function() {
goToTree() {
return location.href = this.options.treeUrl;
};
}
ProjectFindFile.prototype.goToBlob = function() {
goToBlob() {
var $link = this.element.find(".tree-item.selected .tree-item-file-name a");
if ($link.length) {
$link.get(0).click();
}
};
return ProjectFindFile;
})();
}).call(window);
}
}
import renderMath from './render_math';
import renderMermaid from './render_mermaid';
import syntaxHighlight from './syntax_highlight';
// Render Gitlab flavoured Markdown
//
// Delegates to syntax highlight and render math & mermaid diagrams.
//
$.fn.renderGFM = function renderGFM() {
this.find('.js-syntax-highlight').syntaxHighlight();
syntaxHighlight(this.find('.js-syntax-highlight'));
renderMath(this.find('.js-render-math'));
renderMermaid(this.find('.js-render-mermaid'));
return this;
......
<script>
/* global LineHighlighter */
import { mapGetters } from 'vuex';
import syntaxHighlight from '../../syntax_highlight';
export default {
computed: {
......@@ -13,7 +14,7 @@ export default {
},
methods: {
highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight();
syntaxHighlight($(this.$el).find('.file-content'));
},
},
mounted() {
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */
import Flash from './flash';
import Api from './api';
(function() {
this.Search = (function() {
function Search() {
var $groupDropdown, $projectDropdown;
$groupDropdown = $('.js-search-group-dropdown');
$projectDropdown = $('.js-search-project-dropdown');
export default class Search {
constructor() {
const $groupDropdown = $('.js-search-group-dropdown');
const $projectDropdown = $('.js-search-project-dropdown');
this.searchInput = '.js-search-input';
this.searchClear = '.js-search-clear';
this.groupId = $groupDropdown.data('group-id');
this.eventListeners();
$groupDropdown.glDropdown({
selectable: true,
filterable: true,
fieldName: 'group_id',
search: {
fields: ['full_name']
fields: ['full_name'],
},
data: function(term, callback) {
return Api.groups(term, {}, function(data) {
data(term, callback) {
return Api.groups(term, {}, (data) => {
data.unshift({
full_name: 'Any'
full_name: 'Any',
});
data.splice(1, 0, 'divider');
return callback(data);
});
},
id: function(obj) {
id(obj) {
return obj.id;
},
text: function(obj) {
text(obj) {
return obj.full_name;
},
toggleLabel: function(obj) {
return ($groupDropdown.data('default-label')) + " " + obj.full_name;
toggleLabel(obj) {
return `${($groupDropdown.data('default-label'))} ${obj.full_name}`;
},
clicked: (function(_this) {
return function() {
return _this.submitSearch();
};
})(this)
clicked: () => Search.submitSearch(),
});
$projectDropdown.glDropdown({
selectable: true,
filterable: true,
fieldName: 'project_id',
search: {
fields: ['name']
fields: ['name'],
},
data: (term, callback) => {
this.getProjectsData(term)
.then((data) => {
data.unshift({
name_with_namespace: 'Any'
name_with_namespace: 'Any',
});
data.splice(1, 0, 'divider');
......@@ -61,47 +60,46 @@ import Api from './api';
.then(data => callback(data))
.catch(() => new Flash('Error fetching projects'));
},
id: function(obj) {
id(obj) {
return obj.id;
},
text: function(obj) {
text(obj) {
return obj.name_with_namespace;
},
toggleLabel: function(obj) {
return ($projectDropdown.data('default-label')) + " " + obj.name_with_namespace;
toggleLabel(obj) {
return `${($projectDropdown.data('default-label'))} ${obj.name_with_namespace}`;
},
clicked: (function(_this) {
return function() {
return _this.submitSearch();
};
})(this)
clicked: () => Search.submitSearch(),
});
}
Search.prototype.eventListeners = function() {
$(document).off('keyup', '.js-search-input').on('keyup', '.js-search-input', this.searchKeyUp);
return $(document).off('click', '.js-search-clear').on('click', '.js-search-clear', this.clearSearchField);
};
eventListeners() {
$(document)
.off('keyup', this.searchInput)
.on('keyup', this.searchInput, this.searchKeyUp);
$(document)
.off('click', this.searchClear)
.on('click', this.searchClear, this.clearSearchField.bind(this));
}
Search.prototype.submitSearch = function() {
static submitSearch() {
return $('.js-search-form').submit();
};
}
Search.prototype.searchKeyUp = function() {
var $input;
$input = $(this);
searchKeyUp() {
const $input = $(this);
if ($input.val() === '') {
return $('.js-search-clear').addClass('hidden');
$('.js-search-clear').addClass('hidden');
} else {
return $('.js-search-clear').removeClass('hidden');
$('.js-search-clear').removeClass('hidden');
}
}
};
Search.prototype.clearSearchField = function() {
return $('.js-search-input').val('').trigger('keyup').focus();
};
clearSearchField() {
return $(this.searchInput).val('').trigger('keyup').focus();
}
Search.prototype.getProjectsData = function(term) {
getProjectsData(term) {
return new Promise((resolve) => {
if (this.groupId) {
Api.groupProjects(this.groupId, term, resolve);
......@@ -111,8 +109,5 @@ import Api from './api';
}, resolve);
}
});
};
return Search;
})();
}).call(window);
}
}
......@@ -8,17 +8,55 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
* When the user clicks `x` button it cleans the input and closes the dropdown.
*/
((global) => {
const KEYCODE = {
const KEYCODE = {
ESCAPE: 27,
BACKSPACE: 8,
ENTER: 13,
UP: 38,
DOWN: 40,
};
function setSearchOptions() {
var $projectOptionsDataEl = $('.js-search-project-options');
var $groupOptionsDataEl = $('.js-search-group-options');
var $dashboardOptionsDataEl = $('.js-search-dashboard-options');
if ($projectOptionsDataEl.length) {
gl.projectOptions = gl.projectOptions || {};
var projectPath = $projectOptionsDataEl.data('project-path');
gl.projectOptions[projectPath] = {
name: $projectOptionsDataEl.data('name'),
issuesPath: $projectOptionsDataEl.data('issues-path'),
issuesDisabled: $projectOptionsDataEl.data('issues-disabled'),
mrPath: $projectOptionsDataEl.data('mr-path'),
};
}
if ($groupOptionsDataEl.length) {
gl.groupOptions = gl.groupOptions || {};
class SearchAutocomplete {
var groupPath = $groupOptionsDataEl.data('group-path');
gl.groupOptions[groupPath] = {
name: $groupOptionsDataEl.data('name'),
issuesPath: $groupOptionsDataEl.data('issues-path'),
mrPath: $groupOptionsDataEl.data('mr-path'),
};
}
if ($dashboardOptionsDataEl.length) {
gl.dashboardOptions = {
issuesPath: $dashboardOptionsDataEl.data('issues-path'),
mrPath: $dashboardOptionsDataEl.data('mr-path'),
};
}
}
export default class SearchAutocomplete {
constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
setSearchOptions();
this.bindEventContext();
this.wrap = wrap || $('.search');
this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
......@@ -411,45 +449,4 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
return this.searchInput.val('').focus();
}
}
}
global.SearchAutocomplete = SearchAutocomplete;
$(function() {
var $projectOptionsDataEl = $('.js-search-project-options');
var $groupOptionsDataEl = $('.js-search-group-options');
var $dashboardOptionsDataEl = $('.js-search-dashboard-options');
if ($projectOptionsDataEl.length) {
gl.projectOptions = gl.projectOptions || {};
var projectPath = $projectOptionsDataEl.data('project-path');
gl.projectOptions[projectPath] = {
name: $projectOptionsDataEl.data('name'),
issuesPath: $projectOptionsDataEl.data('issues-path'),
issuesDisabled: $projectOptionsDataEl.data('issues-disabled'),
mrPath: $projectOptionsDataEl.data('mr-path'),
};
}
if ($groupOptionsDataEl.length) {
gl.groupOptions = gl.groupOptions || {};
var groupPath = $groupOptionsDataEl.data('group-path');
gl.groupOptions[groupPath] = {
name: $groupOptionsDataEl.data('name'),
issuesPath: $groupOptionsDataEl.data('issues-path'),
mrPath: $groupOptionsDataEl.data('mr-path'),
};
}
if ($dashboardOptionsDataEl.length) {
gl.dashboardOptions = {
issuesPath: $dashboardOptionsDataEl.data('issues-path'),
mrPath: $dashboardOptionsDataEl.data('mr-path'),
};
}
});
})(window.gl || (window.gl = {}));
}
......@@ -11,7 +11,7 @@ export default class ShortcutsIssuable extends ShortcutsNavigation {
super();
this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form');
this.editBtn = document.querySelector('.issuable-edit');
this.editBtn = document.querySelector('.js-issuable-edit');
Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee'));
Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone'));
......
import Flash from '../../../flash';
import AssigneeTitle from './assignee_title';
import Assignees from './assignees';
import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub';
export default {
name: 'SidebarAssignees',
data() {
return {
mediator: new Mediator(),
store: new Store(),
loading: false,
field: '',
};
},
props: {
mediator: {
type: Object,
required: true,
},
field: {
type: String,
required: true,
},
signedIn: {
type: Boolean,
required: false,
default: false,
},
},
components: {
'assignee-title': AssigneeTitle,
assignees: Assignees,
......@@ -61,10 +71,6 @@ export default {
eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees);
eventHub.$off('sidebar.saveAssignees', this.saveAssignees);
},
beforeMount() {
this.field = this.$el.dataset.field;
this.signedIn = typeof this.$el.dataset.signedIn !== 'undefined';
},
template: `
<div>
<assignee-title
......
<script>
import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import participants from './participants.vue';
export default {
data() {
return {
mediator: new Mediator(),
store: new Store(),
};
},
props: {
mediator: {
type: Object,
required: true,
},
},
components: {
participants,
},
......@@ -21,6 +25,7 @@ export default {
<participants
:loading="store.isFetching.participants"
:participants="store.participants"
:number-of-less-participants="7" />
:number-of-less-participants="7"
/>
</div>
</template>
<script>
import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub';
import Flash from '../../../flash';
import { __ } from '../../../locale';
......@@ -9,11 +8,15 @@ import subscriptions from './subscriptions.vue';
export default {
data() {
return {
mediator: new Mediator(),
store: new Store(),
};
},
props: {
mediator: {
type: Object,
required: true,
},
},
components: {
subscriptions,
},
......
......@@ -10,6 +10,27 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate);
function mountAssigneesComponent(mediator) {
const el = document.getElementById('js-vue-sidebar-assignees');
if (!el) return;
// eslint-disable-next-line no-new
new Vue({
el,
components: {
SidebarAssignees,
},
render: createElement => createElement('sidebar-assignees', {
props: {
mediator,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
},
}),
});
}
function mountConfidentialComponent(mediator) {
const el = document.getElementById('js-confidential-entry-point');
......@@ -49,9 +70,10 @@ function mountLockComponent(mediator) {
}).$mount(el);
}
function mountParticipantsComponent() {
function mountParticipantsComponent(mediator) {
const el = document.querySelector('.js-sidebar-participants-entry-point');
// eslint-disable-next-line no-new
if (!el) return;
// eslint-disable-next-line no-new
......@@ -60,11 +82,15 @@ function mountParticipantsComponent() {
components: {
sidebarParticipants,
},
render: createElement => createElement('sidebar-participants', {}),
render: createElement => createElement('sidebar-participants', {
props: {
mediator,
},
}),
});
}
function mountSubscriptionsComponent() {
function mountSubscriptionsComponent(mediator) {
const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
if (!el) return;
......@@ -75,22 +101,35 @@ function mountSubscriptionsComponent() {
components: {
sidebarSubscriptions,
},
render: createElement => createElement('sidebar-subscriptions', {}),
render: createElement => createElement('sidebar-subscriptions', {
props: {
mediator,
},
}),
});
}
function mount(mediator) {
const sidebarAssigneesEl = document.getElementById('js-vue-sidebar-assignees');
// Only create the sidebarAssignees vue app if it is found in the DOM
// We currently do not use sidebarAssignees for the MR page
if (sidebarAssigneesEl) {
new Vue(SidebarAssignees).$mount(sidebarAssigneesEl);
}
function mountTimeTrackingComponent() {
const el = document.getElementById('issuable-time-tracker');
if (!el) return;
// eslint-disable-next-line no-new
new Vue({
el,
components: {
SidebarTimeTracking,
},
render: createElement => createElement('sidebar-time-tracking', {}),
});
}
export function mountSidebar(mediator) {
mountAssigneesComponent(mediator);
mountConfidentialComponent(mediator);
mountLockComponent(mediator);
mountParticipantsComponent();
mountSubscriptionsComponent();
mountParticipantsComponent(mediator);
mountSubscriptionsComponent(mediator);
new SidebarMoveIssue(
mediator,
......@@ -98,7 +137,9 @@ function mount(mediator) {
$('.js-move-issue-confirmation-button'),
).init();
new Vue(SidebarTimeTracking).$mount('#issuable-time-tracker');
mountTimeTrackingComponent();
}
export default mount;
export function getSidebarOptions() {
return JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
}
import Mediator from './sidebar_mediator';
import mountSidebar from './mount_sidebar';
import { mountSidebar, getSidebarOptions } from './mount_sidebar';
function domContentLoaded() {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
const mediator = new Mediator(sidebarOptions);
const mediator = new Mediator(getSidebarOptions());
mediator.fetch();
mountSidebar(mediator);
......
......@@ -8,7 +8,6 @@ export default class SidebarMediator {
if (!SidebarMediator.singleton) {
this.initSingleton(options);
}
return SidebarMediator.singleton;
}
......
export default class SidebarStore {
constructor(store) {
constructor(options) {
if (!SidebarStore.singleton) {
const { currentUser, rootPath, editable } = store;
this.initSingleton(options);
}
return SidebarStore.singleton;
}
initSingleton(options) {
const { currentUser, rootPath, editable } = options;
this.currentUser = currentUser;
this.rootPath = rootPath;
this.editable = editable;
......@@ -25,9 +32,6 @@ export default class SidebarStore {
SidebarStore.singleton = this;
}
return SidebarStore.singleton;
}
setAssigneeData(data) {
this.isFetching.assignees = false;
if (data.assignees) {
......
......@@ -2,6 +2,7 @@
import FilesCommentButton from './files_comment_button';
import imageDiffHelper from './image_diff/helpers/index';
import syntaxHighlight from './syntax_highlight';
const WRAPPER = '<div class="diff-content"></div>';
const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
......@@ -64,7 +65,7 @@ export default class SingleFileDiff {
_this.loadingContent.hide();
if (data.html) {
_this.content = $(data.html);
_this.content.syntaxHighlight();
syntaxHighlight(_this.content);
} else {
_this.hasError = true;
_this.content = $(ERROR_HTML);
......
......@@ -10,17 +10,15 @@
// <div class="js-syntax-highlight"></div>
//
$.fn.syntaxHighlight = function() {
var $children;
if ($(this).hasClass('js-syntax-highlight')) {
export default function syntaxHighlight(el) {
if ($(el).hasClass('js-syntax-highlight')) {
// Given the element itself, apply highlighting
return $(this).addClass(gon.user_color_scheme);
return $(el).addClass(gon.user_color_scheme);
} else {
// Given a parent element, recurse to any of its applicable children
$children = $(this).find('.js-syntax-highlight');
const $children = $(el).find('.js-syntax-highlight');
if ($children.length) {
return $children.syntaxHighlight();
return syntaxHighlight($children);
}
}
};
}
......@@ -4,8 +4,8 @@
padding: 1px 5px;
font-size: 12px;
color: $blue-500;
width: 23px;
height: 23px;
width: 24px;
height: 24px;
border: 1px solid $blue-500;
&:hover,
......
......@@ -143,20 +143,48 @@
}
}
@mixin dropdown-item-hover {
background-color: $dropdown-item-hover-bg;
color: $gl-text-color;
outline: 0;
// make sure the text color is not overriden
&.text-danger {
color: $brand-danger;
}
.avatar {
border-color: $white-light;
}
}
@mixin dropdown-link {
background: transparent;
border: 0;
border-radius: 0;
box-shadow: none;
display: block;
font-weight: $gl-font-weight-normal;
position: relative;
padding: 5px 8px;
padding: 8px 16px;
color: $gl-text-color;
line-height: initial;
border-radius: 2px;
white-space: nowrap;
line-height: normal;
white-space: normal;
overflow: hidden;
text-align: left;
width: 100%;
// make sure the text color is not overriden
&.text-danger {
color: $brand-danger;
}
&:hover,
&:active,
&:focus,
&.is-focused {
background-color: $dropdown-link-hover-bg;
@include dropdown-item-hover;
text-decoration: none;
.badge {
......@@ -166,6 +194,13 @@
&.dropdown-menu-user-link {
line-height: 16px;
padding-top: 10px;
padding-bottom: 7px;
white-space: nowrap;
.dropdown-menu-user-username {
display: block;
}
}
.icon-play {
......@@ -187,8 +222,8 @@
z-index: 300;
min-width: 240px;
max-width: 500px;
margin-top: 2px;
margin-bottom: 2px;
margin-top: $dropdown-vertical-offset;
margin-bottom: 24px;
font-size: 14px;
font-weight: $gl-font-weight-normal;
padding: 8px 0;
......@@ -197,6 +232,10 @@
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
&.dropdown-open-top {
margin-bottom: $dropdown-vertical-offset;
}
&.dropdown-open-left {
right: 0;
left: auto;
......@@ -227,16 +266,27 @@
}
li {
display: block;
text-align: left;
list-style: none;
padding: 0 10px;
padding: 0 1px;
a,
button,
.menu-item {
@include dropdown-link;
}
}
.divider {
height: 1px;
margin: 6px 10px;
margin: 6px 0;
padding: 0;
background-color: $dropdown-divider-color;
&:hover {
background-color: $dropdown-divider-color;
}
}
.separator {
......@@ -247,10 +297,6 @@
background-color: $dropdown-divider-color;
}
a {
@include dropdown-link;
}
.dropdown-menu-empty-item a {
&:hover,
&:focus {
......@@ -262,7 +308,7 @@
color: $gl-text-color-secondary;
font-size: 13px;
line-height: 22px;
padding: 0 16px;
padding: 8px 16px;
}
&.capitalize-header .dropdown-header {
......@@ -277,7 +323,7 @@
.separator + .dropdown-header,
.separator + .dropdown-bold-header {
padding-top: 2px;
padding-top: 10px;
}
.unclickable {
......@@ -298,48 +344,28 @@
}
.dropdown-menu li {
padding: $gl-btn-padding;
cursor: pointer;
&.droplab-item-active button {
@include dropdown-item-hover;
}
> a,
> button {
display: flex;
margin: 0;
padding: 0;
border-radius: 0;
text-overflow: inherit;
background-color: inherit;
color: inherit;
border: inherit;
text-align: left;
&:hover,
&:focus {
background-color: inherit;
color: inherit;
}
&.btn .fa:not(:last-child) {
margin-left: 5px;
}
}
&:hover,
&:focus {
background-color: $dropdown-hover-color;
color: $white-light;
}
&.droplab-item-selected i {
visibility: visible;
}
&.divider {
margin: 0 8px;
padding: 0;
border-top: $gray-darkest;
}
.icon {
visibility: hidden;
}
......@@ -431,11 +457,6 @@
}
}
.dropdown-menu-user-link {
padding-top: 10px;
padding-bottom: 7px;
}
.dropdown-menu-user-full-name {
display: block;
font-weight: $gl-font-weight-normal;
......@@ -464,23 +485,22 @@
.dropdown-menu-align-right {
left: auto;
right: 0;
margin-top: -5px;
}
.dropdown-menu-selectable {
li {
a {
padding-left: 26px;
padding: 8px 40px;
position: relative;
&.is-indeterminate,
&.is-active {
font-weight: $gl-font-weight-bold;
color: $gl-text-color;
&::before {
position: absolute;
left: 6px;
top: 50%;
left: 16px;
top: 16px;
transform: translateY(-50%);
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
......@@ -488,6 +508,12 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
&.dropdown-menu-user-link {
&::before {
top: 50%;
}
}
}
&.is-indeterminate::before {
......@@ -496,9 +522,7 @@
&.is-active::before {
content: "\f00c";
position: absolute;
top: 50%;
transform: translateY(-50%);
}
}
}
}
......@@ -735,136 +759,6 @@
}
}
@mixin dropdown-item-hover {
background-color: $dropdown-item-hover-bg;
color: $gl-text-color;
}
// TODO: change global style and remove mixin
@mixin new-style-dropdown($selector: '') {
#{$selector}.dropdown-menu,
#{$selector}.dropdown-menu-nav {
margin-bottom: 24px;
&.dropdown-open-top {
margin-bottom: $dropdown-vertical-offset;
}
li {
display: block;
padding: 0 1px;
&:hover {
background-color: transparent;
}
&.divider {
margin: 6px 0;
&:hover {
background-color: $dropdown-divider-color;
}
}
&.dropdown-header {
padding: 8px 16px;
}
&.droplab-item-active button {
@include dropdown-item-hover;
}
a,
button,
.menu-item {
margin-bottom: 0;
border-radius: 0;
box-shadow: none;
padding: 8px 16px;
text-align: left;
white-space: normal;
width: 100%;
font-weight: $gl-font-weight-normal;
line-height: normal;
&.dropdown-menu-user-link {
white-space: nowrap;
.dropdown-menu-user-username {
display: block;
}
}
// make sure the text color is not overriden
&.text-danger {
color: $brand-danger;
}
&.is-focused,
&:hover,
&:active,
&:focus {
@include dropdown-item-hover;
background-color: $dropdown-item-hover-bg;
color: $gl-text-color;
// make sure the text color is not overriden
&.text-danger {
color: $brand-danger;
}
}
&.is-active {
font-weight: inherit;
&::before {
top: 16px;
}
&.dropdown-menu-user-link::before {
top: 50%;
transform: translateY(-50%);
}
}
}
&.dropdown-menu-empty-item a {
&:hover,
&:focus {
background-color: transparent;
}
}
}
&.dropdown-menu-selectable {
li {
a {
padding: 8px 40px;
&.is-indeterminate::before,
&.is-active::before {
left: 16px;
}
}
}
}
}
#{$selector}.dropdown-menu-align-right {
margin-top: 2px;
}
.open {
#{$selector}.dropdown-menu,
#{$selector}.dropdown-menu-nav {
@media (max-width: $screen-xs-max) {
max-width: 100%;
}
}
}
}
@media (max-width: $screen-xs-max) {
.navbar-gitlab {
li.header-projects,
......@@ -891,9 +785,6 @@
}
}
@include new-style-dropdown('.breadcrumbs-list .dropdown ');
@include new-style-dropdown('.js-namespace-select + ');
header.header-content .dropdown-menu.projects-dropdown-menu {
padding: 0;
}
......
......@@ -50,8 +50,6 @@
}
.filtered-search-wrapper {
@include new-style-dropdown;
display: -webkit-flex;
display: flex;
......@@ -165,16 +163,6 @@
}
}
.droplab-dropdown li.filtered-search-token {
padding: 0;
&:hover,
&:focus {
background-color: inherit;
color: inherit;
}
}
.filtered-search-term {
.name {
background-color: inherit;
......@@ -336,21 +324,12 @@
.filtered-search-history-dropdown-content {
max-height: none;
}
.filtered-search-history-dropdown-item,
.filtered-search-history-clear-button {
@include dropdown-link;
overflow: hidden;
width: 100%;
margin: 0.5em 0;
background-color: transparent;
border: 0;
text-align: left;
.filtered-search-history-dropdown-item,
.filtered-search-history-clear-button {
white-space: nowrap;
text-overflow: ellipsis;
}
}
.filtered-search-history-dropdown-token {
......@@ -402,24 +381,9 @@
}
}
%filter-dropdown-item-btn-hover {
text-decoration: none;
outline: 0;
.avatar {
border-color: $white-light;
}
}
.droplab-dropdown .dropdown-menu .filter-dropdown-item {
.btn {
border: 0;
width: 100%;
text-align: left;
padding: 8px 16px;
text-overflow: ellipsis;
overflow: hidden;
border-radius: 0;
.fa {
width: 15px;
......@@ -434,11 +398,6 @@
height: 17px;
top: 0;
}
&:hover,
&:focus {
@extend %filter-dropdown-item-btn-hover;
}
}
.dropdown-light-content {
......@@ -459,17 +418,9 @@
word-break: break-all;
}
}
&.droplab-item-active .btn {
@extend %filter-dropdown-item-btn-hover;
}
}
.filter-dropdown-loading {
padding: 8px 16px;
text-align: center;
}
.issues-details-filters {
@include new-style-dropdown;
}
......@@ -3,8 +3,6 @@
}
.navbar-gitlab {
@include new-style-dropdown;
&.navbar-gitlab {
padding: 0 16px;
z-index: 1000;
......
......@@ -19,6 +19,13 @@
max-width: 425px;
width: 100%;
}
&.svg-250 {
img,
svg {
width: 250px;
}
}
}
@mixin svg-size($size) {
......
......@@ -132,8 +132,6 @@ ul.content-list {
}
.controls {
@include new-style-dropdown;
float: right;
> .control-text {
......
......@@ -86,8 +86,6 @@
}
.nav-controls {
@include new-style-dropdown;
display: inline-block;
float: right;
text-align: right;
......
......@@ -50,6 +50,11 @@
&:not(.disabled) {
cursor: pointer;
}
svg {
width: $gl-padding;
height: $gl-padding;
}
}
}
......@@ -139,10 +144,6 @@
}
}
.issuable-sidebar {
@include new-style-dropdown;
}
.pikaday-container {
.pika-single {
margin-top: 2px;
......
......@@ -343,8 +343,6 @@ a > code {
@extend .ref-name;
}
@include new-style-dropdown('.git-revision-dropdown');
/**
* Apply Markdown typography
*
......
......@@ -721,7 +721,7 @@ $issuable-warning-icon-margin: 4px;
Image Commenting cursor
*/
$image-comment-cursor-left-offset: 12;
$image-comment-cursor-top-offset: 30;
$image-comment-cursor-top-offset: 12;
/*
Popup
......
......@@ -323,8 +323,6 @@
}
.build-dropdown {
@include new-style-dropdown;
margin: $gl-padding 0;
padding: 0;
......
......@@ -13,8 +13,6 @@
max-width: 100%;
}
@include new-style-dropdown('.clusters-dropdown ');
.clusters-container {
.nav-bar-right {
padding: $gl-padding-top $gl-padding;
......
#cycle-analytics {
@include new-style-dropdown;
max-width: 1000px;
margin: 24px auto 0;
position: relative;
......
......@@ -32,8 +32,6 @@
}
.detail-page-header-actions {
@include new-style-dropdown;
align-self: center;
flex-shrink: 0;
flex: 0 0 auto;
......
......@@ -581,8 +581,6 @@
}
.commit-stat-summary {
@include new-style-dropdown;
@media (min-width: $screen-sm-min) {
margin-left: -$gl-padding;
padding-left: $gl-padding;
......@@ -732,18 +730,18 @@
.frame.click-to-comment {
position: relative;
cursor: image-url('icon_image_comment.svg')
cursor: image-url('illustrations/image_comment_light_cursor.svg')
$image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
// Retina cursor
cursor: -webkit-image-set(image-url('icon_image_comment.svg') 1x, image-url('icon_image_comment@2x.svg') 2x)
cursor: -webkit-image-set(image-url('illustrations/image_comment_light_cursor.svg') 1x, image-url('illustrations/image_comment_light_cursor@2x.svg') 2x)
$image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
.comment-indicator {
position: absolute;
padding: 0;
width: (2px * $image-comment-cursor-left-offset);
height: (1px * $image-comment-cursor-top-offset);
height: (2px * $image-comment-cursor-top-offset);
// center the indicator to match the top left click region
margin-top: (-1px * $image-comment-cursor-top-offset) + 2;
margin-left: (-1px * $image-comment-cursor-left-offset) + 1;
......@@ -778,15 +776,20 @@
.frame .badge,
.frame .image-comment-badge {
// Center align badges on the frame
transform: translate3d(-50%, -50%, 0);
transform: translate(-50%, -50%);
}
.image-comment-badge {
@include btn-comment-icon;
position: absolute;
width: 24px;
height: 24px;
padding: 0;
background: none;
border: 0;
&.inverted {
border-color: $white-light;
> svg {
width: 100%;
height: 100%;
}
}
......
......@@ -204,8 +204,6 @@
.gitlab-ci-yml-selector,
.dockerfile-selector,
.template-type-selector {
@include new-style-dropdown;
display: inline-block;
vertical-align: top;
font-family: $regular_font;
......
......@@ -12,8 +12,6 @@
.environments-container {
.ci-table {
@include new-style-dropdown;
.deployment-column {
> span {
word-break: break-all;
......
......@@ -470,7 +470,8 @@
}
}
.milestone-title span {
.milestone-title span,
.collapse-truncated-title {
@include str-truncated(100%);
display: block;
margin: 0 4px;
......@@ -487,12 +488,6 @@
}
}
.dropdown-content {
a:hover {
color: inherit;
}
}
.dropdown-menu-toggle {
width: 100%;
padding-top: 6px;
......@@ -511,10 +506,6 @@
}
}
.sidebar-move-issue-dropdown {
@include new-style-dropdown;
}
.sidebar-move-issue-confirmation-button {
width: 100%;
......
......@@ -142,8 +142,6 @@ ul.related-merge-requests > li {
}
.issue-form {
@include new-style-dropdown;
.select2-container {
width: 250px !important;
}
......
......@@ -116,8 +116,6 @@
}
.manage-labels-list {
@include new-style-dropdown;
> li:not(.empty-message):not(.is-not-draggable) {
background-color: $white-light;
cursor: move;
......
......@@ -58,8 +58,6 @@
}
.member-form-control {
@include new-style-dropdown;
@media (max-width: $screen-xs-max) {
padding-bottom: 5px;
margin-left: 0;
......@@ -73,8 +71,6 @@
}
.member-search-form {
@include new-style-dropdown;
position: relative;
@media (min-width: $screen-sm-min) {
......
......@@ -485,8 +485,6 @@
}
.mr-source-target {
@include new-style-dropdown;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
......@@ -608,8 +606,6 @@
}
.mr-version-controls {
@include new-style-dropdown;
position: relative;
background: $gray-light;
color: $gl-text-color;
......@@ -727,7 +723,3 @@
font-size: 16px;
}
}
.merge-request-form {
@include new-style-dropdown;
}
......@@ -23,8 +23,6 @@
.new-note,
.note-edit-form {
.note-form-actions {
@include new-style-dropdown;
position: relative;
margin: $gl-padding 0 0;
}
......
......@@ -490,8 +490,6 @@ ul.notes {
}
.note-actions {
@include new-style-dropdown;
align-self: flex-start;
flex-shrink: 0;
display: inline-flex;
......
......@@ -14,7 +14,3 @@
font-size: 18px;
}
}
.notification-form {
@include new-style-dropdown;
}
......@@ -286,8 +286,6 @@
// Pipeline visualization
.pipeline-actions {
@include new-style-dropdown;
border-bottom: 0;
}
......@@ -703,9 +701,6 @@ button.mini-pipeline-graph-dropdown-toggle {
}
}
@include new-style-dropdown('.big-pipeline-graph-dropdown-menu');
@include new-style-dropdown('.mini-pipeline-graph-dropdown-menu');
// dropdown content for big and mini pipeline
.big-pipeline-graph-dropdown-menu,
.mini-pipeline-graph-dropdown-menu {
......@@ -804,7 +799,6 @@ button.mini-pipeline-graph-dropdown-toggle {
font-weight: normal;
line-height: $line-height-base;
white-space: nowrap;
border-radius: 3px;
.ci-job-name-component {
align-items: center;
......
......@@ -323,8 +323,6 @@
}
.project-repo-buttons {
@include new-style-dropdown;
.project-action-button .dropdown-menu {
max-height: 250px;
overflow-y: auto;
......@@ -898,8 +896,6 @@ pre.light-well {
.new-protected-branch,
.new-protected-tag {
@include new-style-dropdown;
label {
margin-top: 6px;
font-weight: $gl-font-weight-normal;
......@@ -919,8 +915,6 @@ pre.light-well {
.protected-branches-list,
.protected-tags-list {
@include new-style-dropdown;
margin-bottom: 30px;
.settings-message {
......
......@@ -116,11 +116,6 @@ input[type="checkbox"]:hover {
opacity: 0;
display: block;
left: -5px;
padding: 0;
ul {
padding: 10px 0;
}
}
.dropdown-content {
......@@ -185,8 +180,6 @@ input[type="checkbox"]:hover {
}
.search-holder {
@include new-style-dropdown;
@media (min-width: $screen-sm-min) {
display: -webkit-flex;
display: flex;
......
......@@ -265,7 +265,3 @@
font-weight: $gl-font-weight-bold;
}
}
.todos-filters {
@include new-style-dropdown;
}
.tree-holder {
@include new-style-dropdown;
.nav-block {
margin: 10px 0;
......
class Admin::GroupsController < Admin::ApplicationController
include MembersPresentation
before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update]
def index
......@@ -10,8 +12,10 @@ class Admin::GroupsController < Admin::ApplicationController
def show
@group = Group.with_statistics.joins(:route).group('routes.path').find_by_full_path(params[:id])
@members = @group.members.order("access_level DESC").page(params[:members_page])
@requesters = AccessRequestsFinder.new(@group).execute(current_user)
@members = present_members(
@group.members.order("access_level DESC").page(params[:members_page]))
@requesters = present_members(
AccessRequestsFinder.new(@group).execute(current_user))
@projects = @group.projects.with_statistics.page(params[:projects_page])
end
......
class Admin::ProjectsController < Admin::ApplicationController
include MembersPresentation
before_action :project, only: [:show, :transfer, :repository_check]
before_action :group, only: [:show, :transfer]
......@@ -19,11 +21,14 @@ class Admin::ProjectsController < Admin::ApplicationController
def show
if @group
@group_members = @group.members.order("access_level DESC").page(params[:group_members_page])
@group_members = present_members(
@group.members.order("access_level DESC").page(params[:group_members_page]))
end
@project_members = @project.members.page(params[:project_members_page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
@project_members = present_members(
@project.members.page(params[:project_members_page]))
@requesters = present_members(
AccessRequestsFinder.new(@project).execute(current_user))
end
def transfer
......
module MembersPresentation
extend ActiveSupport::Concern
def present_members(members)
Gitlab::View::Presenter::Factory.new(
members,
current_user: current_user,
presenter_class: MembersPresenter
).fabricate!
end
end
class Groups::GroupMembersController < Groups::ApplicationController
include MembershipActions
include MembersPresentation
include SortingHelper
# Authorize
......@@ -14,15 +15,17 @@ class Groups::GroupMembersController < Groups::ApplicationController
@members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort(@sort)
@members = @members.page(params[:page]).per(50)
@members.includes(:user)
@members = present_members(@members.includes(:user))
@requesters = AccessRequestsFinder.new(@group).execute(current_user)
@requesters = present_members(
AccessRequestsFinder.new(@group).execute(current_user))
@group_member = @group.group_members.new
end
def update
@group_member = @group.members_and_requesters.find(params[:id])
.present(current_user: current_user)
return render_403 unless can?(current_user, :update_group_member, @group_member)
......
......@@ -8,11 +8,8 @@ class Projects::ClustersController < Projects::ApplicationController
STATUS_POLLING_INTERVAL = 10_000
def index
@scope = params[:scope] || 'all'
@clusters = ClustersFinder.new(project, current_user, @scope).execute.page(params[:page])
@active_count = ClustersFinder.new(project, current_user, :active).execute.count
@inactive_count = ClustersFinder.new(project, current_user, :inactive).execute.count
@all_count = @active_count + @inactive_count
clusters = ClustersFinder.new(project, current_user, :all).execute
@clusters = clusters.page(params[:page]).per(20)
end
def new
......
class Projects::ProjectMembersController < Projects::ApplicationController
include MembershipActions
include MembersPresentation
include SortingHelper
# Authorize
......@@ -20,13 +21,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
@project_members = @project_members.sort(@sort).page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
@project_members = present_members(@project_members.sort(@sort).page(params[:page]))
@requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user))
@project_member = @project.project_members.new
end
def update
@project_member = @project.members_and_requesters.find(params[:id])
.present(current_user: current_user)
return render_403 unless can?(current_user, :update_project_member, @project_member)
......
......@@ -118,18 +118,22 @@ module BlobHelper
icon("#{file_type_icon_class('file', mode, name)} fw")
end
def blob_raw_path
def blob_raw_url(only_path: false)
if @build && @entry
raw_project_job_artifacts_path(@project, @build, path: @entry.path)
raw_project_job_artifacts_url(@project, @build, path: @entry.path, only_path: only_path)
elsif @snippet
if @snippet.project_id
raw_project_snippet_path(@project, @snippet)
raw_project_snippet_url(@project, @snippet, only_path: only_path)
else
raw_snippet_path(@snippet)
raw_snippet_url(@snippet, only_path: only_path)
end
elsif @blob
project_raw_path(@project, @id)
project_raw_url(@project, @id, only_path: only_path)
end
end
def blob_raw_path
blob_raw_url(only_path: true)
end
# SVGs can contain malicious JavaScript; only include whitelisted
......
......@@ -104,15 +104,23 @@ module DiffHelper
].join(' ').html_safe
end
def diff_file_blob_raw_path(diff_file)
project_raw_path(@project, tree_join(diff_file.content_sha, diff_file.file_path))
def diff_file_blob_raw_url(diff_file, only_path: false)
project_raw_url(@project, tree_join(diff_file.content_sha, diff_file.file_path), only_path: only_path)
end
def diff_file_old_blob_raw_path(diff_file)
def diff_file_old_blob_raw_url(diff_file, only_path: false)
sha = diff_file.old_content_sha
return unless sha
project_raw_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path))
project_raw_url(@project, tree_join(diff_file.old_content_sha, diff_file.old_path), only_path: only_path)
end
def diff_file_blob_raw_path(diff_file)
diff_file_blob_raw_url(diff_file, only_path: true)
end
def diff_file_old_blob_raw_path(diff_file)
diff_file_old_blob_raw_url(diff_file, only_path: true)
end
def diff_file_html_data(project, diff_file_path, diff_commit_id)
......
module LabelsHelper
include ActionView::Helpers::TagHelper
def show_label_issuables_link?(label, issuables_type, current_user: nil, project: nil)
return true if label.is_a?(GroupLabel)
return true unless project
project.feature_available?(issuables_type, current_user)
end
# Link to a Label
#
# label - Label object to link to
......
module MembersHelper
# Returns a `<action>_<source>_member` association, e.g.:
# - admin_project_member, update_project_member, destroy_project_member
# - admin_group_member, update_group_member, destroy_group_member
def action_member_permission(action, member)
"#{action}_#{member.type.underscore}".to_sym
end
def remove_member_message(member, user: nil)
user = current_user if defined?(current_user)
......
......@@ -85,8 +85,7 @@ module CacheMarkdownField
def cached_html_up_to_date?(markdown_field)
html_field = cached_markdown_fields.html_field(markdown_field)
cached = cached_html_for(markdown_field).present? && __send__(markdown_field).present? # rubocop:disable GitlabSecurity/PublicSend
return false unless cached
return false if cached_html_for(markdown_field).nil? && !__send__(markdown_field).nil? # rubocop:disable GitlabSecurity/PublicSend
markdown_changed = attribute_changed?(markdown_field) || false
html_changed = attribute_changed?(html_field) || false
......
......@@ -12,7 +12,7 @@ class Issue < ActiveRecord::Base
include ThrottledTouch
include IgnorableColumn
ignore_column :assignee_id
ignore_column :assignee_id, :branch_name
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
......
......@@ -4,6 +4,7 @@ class Member < ActiveRecord::Base
include Importable
include Expirable
include Gitlab::Access
include Presentable
attr_accessor :raw_invite_token
......
......@@ -8,6 +8,7 @@ class MergeRequest < ActiveRecord::Base
include ManualInverseAssociation
include EachBatch
include ThrottledTouch
include Gitlab::Utils::StrongMemoize
ignore_column :locked_at,
:ref_fetched
......@@ -53,6 +54,7 @@ class MergeRequest < ActiveRecord::Base
after_create :ensure_merge_request_diff, unless: :importing?
after_update :reload_diff_if_branch_changed
after_update :clear_memoized_shas
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
......@@ -387,14 +389,18 @@ class MergeRequest < ActiveRecord::Base
end
def source_branch_head
return unless source_project
source_project.repository.commit(source_branch_ref) if source_branch_ref
strong_memoize(:source_branch_head) do
if source_project && source_branch_ref
source_project.repository.commit(source_branch_ref)
end
end
end
def target_branch_head
strong_memoize(:target_branch_head) do
target_project.repository.commit(target_branch_ref)
end
end
def branch_merge_base_commit
start_sha = target_branch_sha
......@@ -525,6 +531,13 @@ class MergeRequest < ActiveRecord::Base
end
end
def clear_memoized_shas
@target_branch_sha = @source_branch_sha = nil
clear_memoization(:source_branch_head)
clear_memoization(:target_branch_head)
end
def reload_diff_if_branch_changed
if (source_branch_changed? || target_branch_changed?) &&
(source_branch_head && target_branch_head)
......
......@@ -104,19 +104,19 @@ class MergeRequestDiff < ActiveRecord::Base
def base_commit
return unless base_commit_sha
project.commit(base_commit_sha)
project.commit_by(oid: base_commit_sha)
end
def start_commit
return unless start_commit_sha
project.commit(start_commit_sha)
project.commit_by(oid: start_commit_sha)
end
def head_commit
return unless head_commit_sha
project.commit(head_commit_sha)
project.commit_by(oid: head_commit_sha)
end
def commit_shas
......
......@@ -401,6 +401,9 @@ class Note < ActiveRecord::Base
end
noteable_object&.touch
# We return the noteable object so we can re-use it in EE for ElasticSearch.
noteable_object
end
def banzai_render_context(field)
......
......@@ -659,7 +659,8 @@ class Project < ActiveRecord::Base
end
def import_started?
import? && import_status == 'started'
# import? does SQL work so only run it if it looks like there's an import running
import_status == 'started' && import?
end
def import_scheduled?
......@@ -1147,7 +1148,7 @@ class Project < ActiveRecord::Base
def change_head(branch)
if repository.branch_exists?(branch)
repository.before_change_head
repository.write_ref('HEAD', "refs/heads/#{branch}")
repository.write_ref('HEAD', "refs/heads/#{branch}", force: true)
repository.copy_gitattributes(branch)
repository.after_change_head
reload_default_branch
......
......@@ -19,6 +19,7 @@ class Repository
attr_accessor :full_path, :disk_path, :project, :is_wiki
delegate :ref_name_for_sha, to: :raw_repository
delegate :write_ref, to: :raw_repository
CreateTreeError = Class.new(StandardError)
......@@ -237,11 +238,10 @@ class Repository
# This will still fail if the file is corrupted (e.g. 0 bytes)
begin
write_ref(keep_around_ref_name(sha), sha)
rescue Rugged::ReferenceError => ex
Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}"
rescue Rugged::OSError => ex
raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/
write_ref(keep_around_ref_name(sha), sha, force: true)
rescue Gitlab::Git::Repository::GitError => ex
# Necessary because https://gitlab.com/gitlab-org/gitlab-ce/issues/20156
return true if ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/
Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}"
end
......@@ -251,10 +251,6 @@ class Repository
ref_exists?(keep_around_ref_name(sha))
end
def write_ref(ref_path, sha)
rugged.references.create(ref_path, sha, force: true)
end
def diverging_commit_counts(branch)
root_ref_hash = raw_repository.commit(root_ref).id
cache.fetch(:"diverging_commit_counts_#{branch.name}") do
......@@ -971,8 +967,7 @@ class Repository
tmp_remote_name = true
end
add_remote(remote_name, url)
set_remote_as_mirror(remote_name, refmap: refmap)
add_remote(remote_name, url, mirror_refmap: refmap)
fetch_remote(remote_name, forced: forced)
ensure
remove_remote(remote_name) if tmp_remote_name
......@@ -995,7 +990,7 @@ class Repository
end
def create_ref(ref, ref_path)
raw_repository.write_ref(ref_path, ref)
write_ref(ref_path, ref)
end
def ls_files(ref)
......
class GroupMemberPresenter < MemberPresenter
private
def admin_member_permission
:admin_group_member
end
def update_member_permission
:update_group_member
end
def destroy_member_permission
:destroy_group_member
end
end
class MemberPresenter < Gitlab::View::Presenter::Delegated
presents :member
def access_level_roles
member.class.access_level_roles
end
def can_resend_invite?
invite? &&
can?(current_user, admin_member_permission, source)
end
def can_update?
can?(current_user, update_member_permission, member)
end
def can_remove?
can?(current_user, destroy_member_permission, member)
end
def can_approve?
request? && can_update?
end
private
def admin_member_permission
raise NotImplementedError
end
def update_member_permission
raise NotImplementedError
end
def destroy_member_permission
raise NotImplementedError
end
end
class MembersPresenter < Gitlab::View::Presenter::Delegated
include Enumerable
presents :members
def to_ary
to_a
end
def each
members.each do |member|
yield member.present(current_user: current_user)
end
end
end
class ProjectMemberPresenter < MemberPresenter
private
def admin_member_permission
:admin_project_member
end
def update_member_permission
:update_project_member
end
def destroy_member_permission
:destroy_project_member
end
end
......@@ -38,11 +38,15 @@ module Ci
begin
# In case when 2 runners try to assign the same build, second runner will be declined
# with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
begin
build.runner_id = runner.id
build.run!
register_success(build)
return Result.new(build, true)
rescue Ci::Build::MissingDependenciesError
build.drop!(:missing_dependency_failure)
end
rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError
# We are looping to find another build that is not conflicting
# It also indicates that this build can be picked and passed to runner.
......@@ -54,9 +58,6 @@ module Ci
# we still have to return 409 in the end,
# to make sure that this is properly handled by runner.
valid = false
rescue Ci::Build::MissingDependenciesError
build.drop!(:missing_dependency_failure)
valid = false
end
end
......
......@@ -35,8 +35,17 @@ module Members
def can_update_access_requester?(access_requester, opts = {})
access_requester && (
opts[:force] ||
can?(current_user, action_member_permission(:update, access_requester), access_requester)
can?(current_user, update_member_permission(access_requester), access_requester)
)
end
def update_member_permission(member)
case member
when GroupMember
:update_group_member
when ProjectMember
:update_project_member
end
end
end
end
......@@ -36,7 +36,16 @@ module Members
end
def can_destroy_member?(member)
member && can?(current_user, action_member_permission(:destroy, member), member)
member && can?(current_user, destroy_member_permission(member), member)
end
def destroy_member_permission(member)
case member
when GroupMember
:destroy_group_member
when ProjectMember
:destroy_project_member
end
end
end
end
......@@ -83,12 +83,12 @@
You're all done!
- elsif current_user.todos.any?
.todos-all-done
.svg-content
.svg-content.svg-250
= image_tag 'illustrations/todos_all_done.svg'
- if todos_filter_empty?
%h4.text-center
= Gitlab.config.gitlab.no_todos_messages.sample
%p.text-center
%p
Are you looking for things to do? Take a look at
= succeed "," do
= link_to "the opened issues", issues_dashboard_path
......@@ -104,7 +104,7 @@
= image_tag 'illustrations/todos_empty.svg'
.todos-empty-content
%h4
Todos let you see what you should do next.
Todos let you see what you should do next
%p
When an issue or merge request is assigned to you, or when you
%strong
......
- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: @project.web_url, avatar_url: @project.avatar_url } if @project&.persisted?
- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted?
.projects-dropdown-container
.project-dropdown-sidebar
%ul
......
......@@ -3,15 +3,15 @@
Template
.template-selector-dropdowns-wrap
.template-type-selector.js-template-type-selector-wrap.hidden
= dropdown_tag("Choose type", options: { toggle_class: 'btn js-template-type-selector', title: "Choose a template type" } )
= dropdown_tag("Choose type", options: { toggle_class: 'js-template-type-selector', title: "Choose a template type" } )
.license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a license template", options: { toggle_class: 'btn js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
= dropdown_tag("Apply a license template", options: { toggle_class: 'js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
.gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
= dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
= dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'js-gitlab-ci-yml-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
.dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden
= dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } )
= dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'js-dockerfile-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } )
.template-selectors-undo-menu.hidden
%span.text-info Template applied
%button.btn.btn-sm.btn-info Undo
.file-content.image_file
= image_tag(blob_raw_path, alt: viewer.blob.name)
-# Uses the full URL rather than the path, to prevent it from getting prefixed with the asset host.
= image_tag(blob_raw_url, alt: viewer.blob.name)
......@@ -20,7 +20,7 @@
.col-sm-10.create-from
.dropdown
= hidden_field_tag :ref, default_ref
= button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide form-control js-branch-select git-revision-dropdown-toggle', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do
= button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide js-branch-select git-revision-dropdown-toggle', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do
.text-left.dropdown-toggle-text= default_ref
= icon('chevron-down')
= render 'shared/ref_dropdown', dropdown_class: 'wide'
......
.row.empty-state
.col-xs-12
.svg-content= image_tag 'illustrations/clusters_empty.svg'
.col-xs-12.text-center
.col-xs-12
.text-content
%h4= s_('ClusterIntegration|Integrate cluster automation')
%h4.text-center= s_('ClusterIntegration|Integrate cluster automation')
- link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
%p= s_('ClusterIntegration|Clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
%p
.text-center
= link_to s_('ClusterIntegration|Add cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= icon("angle-left")
.fade-right= icon("angle-right")
%ul.nav-links.scrolling-tabs
%li{ class: ('active' if @scope == 'active') }>
= link_to project_clusters_path(@project, scope: :active), class: "js-active-tab" do
= s_("ClusterIntegration|Active")
%span.badge= @active_count
%li{ class: ('active' if @scope == 'inactive') }>
= link_to project_clusters_path(@project, scope: :inactive), class: "js-inactive-tab" do
= s_("ClusterIntegration|Inactive")
%span.badge= @inactive_count
%li{ class: ('active' if @scope.nil? || @scope == 'all') }>
= link_to project_clusters_path(@project), class: "js-all-tab" do
= s_("ClusterIntegration|All")
%span.badge= @all_count
......@@ -2,8 +2,12 @@
- page_title "Clusters"
.clusters-container
- if !@clusters.empty?
= render "tabs"
- if @clusters.empty?
= render "empty_state"
- else
.top-area.adjust
.nav-text
= s_("ClusterIntegration|Clusters can be used to deploy applications and to provide Review Apps for this project")
.ci-table.js-clusters-list
.gl-responsive-table-row.table-row-header{ role: "row" }
.table-section.section-30{ role: "rowheader" }
......@@ -16,9 +20,3 @@
- @clusters.each do |cluster|
= render "cluster", cluster: cluster.present(current_user: current_user)
= paginate @clusters, theme: "gitlab"
- elsif @scope == 'all'
= render "empty_state"
- else
= render "tabs"
.prepend-top-20.text-center
= s_("ClusterIntegration|There are no clusters to show")
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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