Commit 75215e6d authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into list-multiple-clusters

* master: (154 commits)
  Don't disable the Rails mailer when seeding the test environment
  Documentation bug fixes: Added procedure to disable built-in Issues.
  Ensure `Namespace`'s is namespaced in `Gitlab::Kubernetes::Helm#initialize` and fix a transient failing spec due to that
  Add changelog entry
  Show only group name by default and put full namespace in tooltip
  Adds validation for Project#ci_config_path not to contain leading slash
  Gracefully handle case when repository's root ref does not exist
  Set EE variable
  issue note store
  allow caching options to be specified for counting services
  Update CHANGELOG.md for 10.2.3
  Add link to gitaly converation
  Fix pipeline config source specs and test it explicitly
  Set an artificial $HOME for gitaly in test
  Add a fixture file that uses seed-fu in the test env so that a borken seed-fu is detected
  Pin seed-fu to 2.3.6 since 2.3.7 is broken
  Do not set pipeline source after initialization
  updated diff spec
  fix for special charecter in file names
  We could simply count the commits
  ...
parents d7ebb823 c997c95d
...@@ -579,7 +579,7 @@ codequality: ...@@ -579,7 +579,7 @@ codequality:
script: script:
- cp .rubocop.yml .rubocop.yml.bak - cp .rubocop.yml .rubocop.yml.bak
- grep -v "rubocop-gitlab-security" .rubocop.yml.bak > .rubocop.yml - grep -v "rubocop-gitlab-security" .rubocop.yml.bak > .rubocop.yml
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 analyze -f json > raw_codeclimate.json - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json
- cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
- mv .rubocop.yml.bak .rubocop.yml - mv .rubocop.yml.bak .rubocop.yml
artifacts: artifacts:
......
...@@ -2,6 +2,25 @@ ...@@ -2,6 +2,25 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.2.3 (2017-11-30)
### Fixed (7 changes)
- Fix hashed storage for Import/Export uploads. !15482
- Ensure that rake gitlab:cleanup:repos task does not mess with hashed repositories. !15520
- Ensure that rake gitlab:cleanup:dirs task does not mess with hashed repositories. !15600
- Fix WIP system note not being created.
- Fix link text from group context.
- Fix defaults for MR states and merge statuses.
- Fix pulling and pushing using a personal access token with the sudo scope.
### Performance (3 changes)
- Drastically improve project search performance by no longer searching namespace name.
- Reuse authors when rendering event Atom feeds.
- Optimise StuckCiJobsWorker using cheap SQL query outside, and expensive inside.
## 10.2.2 (2017-11-23) ## 10.2.2 (2017-11-23)
### Fixed (5 changes) ### Fixed (5 changes)
......
...@@ -111,7 +111,7 @@ gem 'google-api-client', '~> 0.13.6' ...@@ -111,7 +111,7 @@ gem 'google-api-client', '~> 0.13.6'
gem 'unf', '~> 0.1.4' gem 'unf', '~> 0.1.4'
# Seed data # Seed data
gem 'seed-fu', '~> 2.3.5' gem 'seed-fu', '2.3.6' # Upgrade to > 2.3.7 once https://github.com/mbleigh/seed-fu/issues/123 is solved
# Markdown and HTML processing # Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0' gem 'html-pipeline', '~> 1.11.0'
...@@ -283,7 +283,7 @@ group :metrics do ...@@ -283,7 +283,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false gem 'influxdb', '~> 0.2', require: false
# Prometheus # Prometheus
gem 'prometheus-client-mmap', '~> 0.7.0.beta37' gem 'prometheus-client-mmap', '~> 0.7.0.beta39'
gem 'raindrops', '~> 0.18' gem 'raindrops', '~> 0.18'
end end
......
...@@ -625,7 +625,7 @@ GEM ...@@ -625,7 +625,7 @@ GEM
parser parser
unparser unparser
procto (0.0.3) procto (0.0.3)
prometheus-client-mmap (0.7.0.beta37) prometheus-client-mmap (0.7.0.beta39)
mmap2 (~> 2.2, >= 2.2.9) mmap2 (~> 2.2, >= 2.2.9)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
...@@ -1111,7 +1111,7 @@ DEPENDENCIES ...@@ -1111,7 +1111,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3) peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2) pg (~> 0.18.2)
premailer-rails (~> 1.9.7) premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta37) prometheus-client-mmap (~> 0.7.0.beta39)
pry-byebug (~> 3.4.1) pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4) pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1) rack-attack (~> 4.4.1)
...@@ -1153,7 +1153,7 @@ DEPENDENCIES ...@@ -1153,7 +1153,7 @@ DEPENDENCIES
sanitize (~> 2.0) sanitize (~> 2.0)
sass-rails (~> 5.0.6) sass-rails (~> 5.0.6)
scss_lint (~> 0.54.0) scss_lint (~> 0.54.0)
seed-fu (~> 2.3.5) seed-fu (= 2.3.6)
select2-rails (~> 3.5.9) select2-rails (~> 3.5.9)
selenium-webdriver (~> 3.5) selenium-webdriver (~> 3.5)
sentry-raven (~> 2.5.3) sentry-raven (~> 2.5.3)
......
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */ import Clipboard from 'clipboard';
import Clipboard from 'vendor/clipboard'; function showTooltip(target, title) {
const $target = $(target);
const originalTitle = $target.data('original-title');
var genericError, genericSuccess, showTooltip; if (!$target.data('hideTooltip')) {
$target
.attr('title', title)
.tooltip('fixTitle')
.tooltip('show')
.attr('title', originalTitle)
.tooltip('fixTitle');
}
}
genericSuccess = function(e) { function genericSuccess(e) {
showTooltip(e.trigger, 'Copied'); showTooltip(e.trigger, 'Copied');
// Clear the selection and blur the trigger so it loses its border // Clear the selection and blur the trigger so it loses its border
e.clearSelection(); e.clearSelection();
return $(e.trigger).blur(); $(e.trigger).blur();
}; }
// Safari doesn't support `execCommand`, so instead we inform the user to /**
// copy manually. * Safari > 10 doesn't support `execCommand`, so instead we inform the user to copy manually.
// * See http://clipboardjs.com/#browser-support
// See http://clipboardjs.com/#browser-support */
genericError = function(e) { function genericError(e) {
var key; let key;
if (/Mac/i.test(navigator.userAgent)) { if (/Mac/i.test(navigator.userAgent)) {
key = '⌘'; // Command key = '⌘'; // Command
} else { } else {
key = 'Ctrl'; key = 'Ctrl';
} }
return showTooltip(e.trigger, "Press " + key + "-C to copy"); showTooltip(e.trigger, `Press ${key}-C to copy`);
}; }
showTooltip = function(target, title) {
var $target = $(target);
var originalTitle = $target.data('original-title');
if (!$target.data('hideTooltip')) {
$target
.attr('title', 'Copied')
.tooltip('fixTitle')
.tooltip('show')
.attr('title', originalTitle)
.tooltip('fixTitle');
}
};
$(function() { export default function initCopyToClipboard() {
const clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]'); const clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
clipboard.on('success', genericSuccess); clipboard.on('success', genericSuccess);
clipboard.on('error', genericError); clipboard.on('error', genericError);
// This a workaround around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM. /**
// The Ruby `clipboard_button` helper sneaks a JSON hash with `text` and `gfm` keys into the `data-clipboard-text` * This a workaround around ClipboardJS limitations to allow the context-specific copy/pasting
// attribute that ClipboardJS reads from. * of plain text or GFM. The Ruby `clipboard_button` helper sneaks a JSON hash with `text` and
// When ClipboardJS creates a new `textarea` (directly inside `body`, with a `readonly` attribute`), sets its value * `gfm` keys into the `data-clipboard-text` attribute that ClipboardJS reads from.
// to the value of this data attribute, focusses on it, and finally programmatically issues the 'Copy' command, * When ClipboardJS creates a new `textarea` (directly inside `body`, with a `readonly`
// this code intercepts the copy command/event at the last minute to deconstruct this JSON hash and set the * attribute`), sets its value to the value of this data attribute, focusses on it, and finally
// `text/plain` and `text/x-gfm` copy data types to the intended values. * programmatically issues the 'Copy' command, this code intercepts the copy command/event at
$(document).on('copy', 'body > textarea[readonly]', function(e) { * the last minute to deconstruct this JSON hash and set the `text/plain` and `text/x-gfm` copy
* data types to the intended values.
*/
$(document).on('copy', 'body > textarea[readonly]', (e) => {
const clipboardData = e.originalEvent.clipboardData; const clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return; if (!clipboardData) return;
...@@ -71,4 +70,4 @@ $(function() { ...@@ -71,4 +70,4 @@ $(function() {
clipboardData.setData('text/plain', json.text); clipboardData.setData('text/plain', json.text);
clipboardData.setData('text/x-gfm', json.gfm); clipboardData.setData('text/x-gfm', json.gfm);
}); });
}); }
import './autosize'; import './autosize';
import './bind_in_out'; import './bind_in_out';
import initCopyAsGFM from './copy_as_gfm'; import initCopyAsGFM from './copy_as_gfm';
import initCopyToClipboard from './copy_to_clipboard';
import './details_behavior'; import './details_behavior';
import installGlEmojiElement from './gl_emoji'; import installGlEmojiElement from './gl_emoji';
import './quick_submit'; import './quick_submit';
...@@ -9,3 +10,4 @@ import './toggler_behavior'; ...@@ -9,3 +10,4 @@ import './toggler_behavior';
installGlEmojiElement(); installGlEmojiElement();
initCopyAsGFM(); initCopyAsGFM();
initCopyToClipboard();
...@@ -383,6 +383,7 @@ import ProjectVariables from './project_variables'; ...@@ -383,6 +383,7 @@ import ProjectVariables from './project_variables';
projectImport(); projectImport();
break; break;
case 'projects:pipelines:new': case 'projects:pipelines:new':
case 'projects:pipelines:create':
new NewBranchForm($('.js-new-pipeline-form')); new NewBranchForm($('.js-new-pipeline-form'));
break; break;
case 'projects:pipelines:builds': case 'projects:pipelines:builds':
...@@ -521,6 +522,13 @@ import ProjectVariables from './project_variables'; ...@@ -521,6 +522,13 @@ import ProjectVariables from './project_variables';
case 'projects:settings:ci_cd:show': case 'projects:settings:ci_cd:show':
// Initialize expandable settings panels // Initialize expandable settings panels
initSettingsPanels(); initSettingsPanels();
import(/* webpackChunkName: "ci-cd-settings" */ './projects/ci_cd_settings_bundle')
.then(ciCdSettings => ciCdSettings.default())
.catch((err) => {
Flash(s__('ProjectSettings|Problem setting up the CI/CD settings JavaScript'));
throw err;
});
case 'groups:settings:ci_cd:show': case 'groups:settings:ci_cd:show':
new ProjectVariables(); new ProjectVariables();
break; break;
......
...@@ -3,6 +3,7 @@ const DATA_DROPDOWN = 'data-dropdown'; ...@@ -3,6 +3,7 @@ const DATA_DROPDOWN = 'data-dropdown';
const SELECTED_CLASS = 'droplab-item-selected'; const SELECTED_CLASS = 'droplab-item-selected';
const ACTIVE_CLASS = 'droplab-item-active'; const ACTIVE_CLASS = 'droplab-item-active';
const IGNORE_CLASS = 'droplab-item-ignore'; const IGNORE_CLASS = 'droplab-item-ignore';
const IGNORE_HIDING_CLASS = 'droplab-item-ignore-hiding';
// Matches `{{anything}}` and `{{ everything }}`. // Matches `{{anything}}` and `{{ everything }}`.
const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g; const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g;
...@@ -13,4 +14,5 @@ export { ...@@ -13,4 +14,5 @@ export {
ACTIVE_CLASS, ACTIVE_CLASS,
TEMPLATE_REGEX, TEMPLATE_REGEX,
IGNORE_CLASS, IGNORE_CLASS,
IGNORE_HIDING_CLASS,
}; };
import utils from './utils'; import utils from './utils';
import { SELECTED_CLASS, IGNORE_CLASS } from './constants'; import { SELECTED_CLASS, IGNORE_CLASS, IGNORE_HIDING_CLASS } from './constants';
class DropDown { class DropDown {
constructor(list) { constructor(list, config = {}) {
this.currentIndex = 0; this.currentIndex = 0;
this.hidden = true; this.hidden = true;
this.list = typeof list === 'string' ? document.querySelector(list) : list; this.list = typeof list === 'string' ? document.querySelector(list) : list;
this.items = []; this.items = [];
this.eventWrapper = {}; this.eventWrapper = {};
if (config.addActiveClassToDropdownButton) {
this.dropdownToggle = this.list.parentNode.querySelector('.js-dropdown-toggle');
}
this.getItems(); this.getItems();
this.initTemplateString(); this.initTemplateString();
this.addEvents(); this.addEvents();
...@@ -42,7 +45,7 @@ class DropDown { ...@@ -42,7 +45,7 @@ class DropDown {
this.addSelectedClass(selected); this.addSelectedClass(selected);
e.preventDefault(); e.preventDefault();
this.hide(); if (!e.target.classList.contains(IGNORE_HIDING_CLASS)) this.hide();
const listEvent = new CustomEvent('click.dl', { const listEvent = new CustomEvent('click.dl', {
detail: { detail: {
...@@ -67,7 +70,20 @@ class DropDown { ...@@ -67,7 +70,20 @@ class DropDown {
addEvents() { addEvents() {
this.eventWrapper.clickEvent = this.clickEvent.bind(this); this.eventWrapper.clickEvent = this.clickEvent.bind(this);
this.eventWrapper.closeDropdown = this.closeDropdown.bind(this);
this.list.addEventListener('click', this.eventWrapper.clickEvent); this.list.addEventListener('click', this.eventWrapper.clickEvent);
this.list.addEventListener('keyup', this.eventWrapper.closeDropdown);
}
closeDropdown(event) {
// `ESC` key closes the dropdown.
if (event.keyCode === 27) {
event.preventDefault();
return this.toggle();
}
return true;
} }
setData(data) { setData(data) {
...@@ -110,6 +126,8 @@ class DropDown { ...@@ -110,6 +126,8 @@ class DropDown {
this.list.style.display = 'block'; this.list.style.display = 'block';
this.currentIndex = 0; this.currentIndex = 0;
this.hidden = false; this.hidden = false;
if (this.dropdownToggle) this.dropdownToggle.classList.add('active');
} }
hide() { hide() {
...@@ -117,6 +135,8 @@ class DropDown { ...@@ -117,6 +135,8 @@ class DropDown {
this.list.style.display = 'none'; this.list.style.display = 'none';
this.currentIndex = 0; this.currentIndex = 0;
this.hidden = true; this.hidden = true;
if (this.dropdownToggle) this.dropdownToggle.classList.remove('active');
} }
toggle() { toggle() {
...@@ -128,6 +148,7 @@ class DropDown { ...@@ -128,6 +148,7 @@ class DropDown {
destroy() { destroy() {
this.hide(); this.hide();
this.list.removeEventListener('click', this.eventWrapper.clickEvent); this.list.removeEventListener('click', this.eventWrapper.clickEvent);
this.list.removeEventListener('keyup', this.eventWrapper.closeDropdown);
} }
static setImagesSrc(template) { static setImagesSrc(template) {
......
...@@ -3,7 +3,7 @@ import DropDown from './drop_down'; ...@@ -3,7 +3,7 @@ import DropDown from './drop_down';
class Hook { class Hook {
constructor(trigger, list, plugins, config) { constructor(trigger, list, plugins, config) {
this.trigger = trigger; this.trigger = trigger;
this.list = new DropDown(list); this.list = new DropDown(list, config);
this.type = 'Hook'; this.type = 'Hook';
this.event = 'click'; this.event = 'click';
this.plugins = plugins || []; this.plugins = plugins || [];
......
...@@ -36,7 +36,10 @@ export default function dropzoneInput(form) { ...@@ -36,7 +36,10 @@ export default function dropzoneInput(form) {
$formDropzone.append(divHover); $formDropzone.append(divHover);
$formDropzone.find('.div-dropzone-hover').append(iconPaperclip); $formDropzone.find('.div-dropzone-hover').append(iconPaperclip);
if (!uploadsPath) return; if (!uploadsPath) {
$formDropzone.addClass('js-invalid-dropzone');
return;
}
const dropzone = $formDropzone.dropzone({ const dropzone = $formDropzone.dropzone({
url: uploadsPath, url: uploadsPath,
......
...@@ -41,7 +41,7 @@ const createFlashEl = (message, type, isInContentWrapper = false) => ` ...@@ -41,7 +41,7 @@ const createFlashEl = (message, type, isInContentWrapper = false) => `
`; `;
const removeFlashClickListener = (flashEl, fadeTransition) => { const removeFlashClickListener = (flashEl, fadeTransition) => {
flashEl.parentNode.addEventListener('click', () => hideFlash(flashEl, fadeTransition)); flashEl.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
}; };
/* /*
......
<script> <script>
import tooltip from '../../vue_shared/directives/tooltip';
import identicon from '../../vue_shared/components/identicon.vue'; import identicon from '../../vue_shared/components/identicon.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
...@@ -8,6 +9,9 @@ import itemStats from './item_stats.vue'; ...@@ -8,6 +9,9 @@ import itemStats from './item_stats.vue';
import itemActions from './item_actions.vue'; import itemActions from './item_actions.vue';
export default { export default {
directives: {
tooltip,
},
components: { components: {
identicon, identicon,
itemCaret, itemCaret,
...@@ -112,10 +116,16 @@ export default { ...@@ -112,10 +116,16 @@ export default {
</a> </a>
</div> </div>
<div <div
class="title"> class="title namespace-title">
<a <a
v-tooltip
:href="group.relativePath" :href="group.relativePath"
class="no-expand">{{group.fullName}}</a> :title="group.fullName"
class="no-expand"
data-placement="top"
>
{{group.name}}
</a>
<span <span
v-if="group.permission" v-if="group.permission"
class="access-type" class="access-type"
......
...@@ -16,6 +16,10 @@ export default { ...@@ -16,6 +16,10 @@ export default {
required: true, required: true,
type: String, type: String,
}, },
updateEndpoint: {
required: true,
type: String,
},
canUpdate: { canUpdate: {
required: true, required: true,
type: Boolean, type: Boolean,
...@@ -34,6 +38,11 @@ export default { ...@@ -34,6 +38,11 @@ export default {
required: false, required: false,
default: true, default: true,
}, },
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
issuableRef: { issuableRef: {
type: String, type: String,
required: true, required: true,
...@@ -240,6 +249,7 @@ export default { ...@@ -240,6 +249,7 @@ export default {
:project-namespace="projectNamespace" :project-namespace="projectNamespace"
:show-delete-button="showDeleteButton" :show-delete-button="showDeleteButton"
:can-attach-file="canAttachFile" :can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
/> />
<div v-else> <div v-else>
<title-component <title-component
...@@ -256,6 +266,8 @@ export default { ...@@ -256,6 +266,8 @@ export default {
:description-text="state.descriptionText" :description-text="state.descriptionText"
:updated-at="state.updatedAt" :updated-at="state.updatedAt"
:task-status="state.taskStatus" :task-status="state.taskStatus"
:issuable-type="issuableType"
:update-url="updateEndpoint"
/> />
<edited-component <edited-component
v-if="hasUpdated" v-if="hasUpdated"
......
...@@ -22,6 +22,16 @@ ...@@ -22,6 +22,16 @@
required: false, required: false,
default: '', default: '',
}, },
issuableType: {
type: String,
required: false,
default: 'issue',
},
updateUrl: {
type: String,
required: false,
default: null,
},
}, },
data() { data() {
return { return {
...@@ -48,7 +58,7 @@ ...@@ -48,7 +58,7 @@
if (this.canUpdate) { if (this.canUpdate) {
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new TaskList({ new TaskList({
dataType: 'issue', dataType: this.issuableType,
fieldName: 'description', fieldName: 'description',
selector: '.detail-page-description', selector: '.detail-page-description',
}); });
...@@ -95,7 +105,9 @@ ...@@ -95,7 +105,9 @@
<textarea <textarea
class="hidden js-task-list-field" class="hidden js-task-list-field"
v-if="descriptionText" v-if="descriptionText"
v-model="descriptionText"> v-model="descriptionText"
:data-update-url="updateUrl"
>
</textarea> </textarea>
</div> </div>
</template> </template>
...@@ -22,6 +22,11 @@ ...@@ -22,6 +22,11 @@
required: false, required: false,
default: true, default: true,
}, },
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
}, },
components: { components: {
markdownField, markdownField,
...@@ -42,7 +47,9 @@ ...@@ -42,7 +47,9 @@
<markdown-field <markdown-field
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
:can-attach-file="canAttachFile"> :can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
>
<textarea <textarea
id="issue-description" id="issue-description"
class="note-textarea js-gfm-input js-autosize markdown-area" class="note-textarea js-gfm-input js-autosize markdown-area"
......
...@@ -46,6 +46,11 @@ ...@@ -46,6 +46,11 @@
required: false, required: false,
default: true, default: true,
}, },
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
}, },
components: { components: {
lockedWarning, lockedWarning,
...@@ -89,7 +94,9 @@ ...@@ -89,7 +94,9 @@
:form-state="formState" :form-state="formState"
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
:can-attach-file="canAttachFile" /> :can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
/>
<edit-actions <edit-actions
:form-state="formState" :form-state="formState"
:can-destroy="canDestroy" :can-destroy="canDestroy"
......
...@@ -79,7 +79,7 @@ ...@@ -79,7 +79,7 @@
v-tooltip v-tooltip
v-if="showInlineEditButton && canUpdate" v-if="showInlineEditButton && canUpdate"
type="button" type="button"
class="btn-blank btn-edit note-action-button" class="btn btn-default btn-edit btn-svg"
v-html="pencilIcon" v-html="pencilIcon"
title="Edit title and description" title="Edit title and description"
data-placement="bottom" data-placement="bottom"
......
...@@ -190,7 +190,7 @@ export const insertText = (target, text) => { ...@@ -190,7 +190,7 @@ export const insertText = (target, text) => {
target.selectionStart = target.selectionEnd = selectionStart + insertedText.length; target.selectionStart = target.selectionEnd = selectionStart + insertedText.length;
// Trigger autosave // Trigger autosave
$(target).trigger('input'); target.dispatchEvent(new Event('input'));
// Trigger autosize // Trigger autosize
const event = document.createEvent('Event'); const event = document.createEvent('Event');
......
...@@ -150,3 +150,17 @@ export function timeIntervalInWords(intervalInSeconds) { ...@@ -150,3 +150,17 @@ export function timeIntervalInWords(intervalInSeconds) {
} }
return text; return text;
} }
export function dateInWords(date, abbreviated = false) {
if (!date) return date;
const month = date.getMonth();
const year = date.getFullYear();
const monthNames = [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')];
const monthNamesAbbr = [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')];
const monthName = abbreviated ? monthNamesAbbr[month] : monthNames[month];
return `${monthName} ${date.getDate()}, ${year}`;
}
...@@ -55,3 +55,12 @@ export const slugify = str => str.trim().toLowerCase(); ...@@ -55,3 +55,12 @@ export const slugify = str => str.trim().toLowerCase();
*/ */
export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`; export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - 3))}...`;
/**
* Capitalizes first character
*
* @param {String} text
* @return {String}
*/
export function capitalizeFirstCharacter(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`;
}
...@@ -44,7 +44,6 @@ import './commits'; ...@@ -44,7 +44,6 @@ import './commits';
import './compare'; import './compare';
import './compare_autocomplete'; import './compare_autocomplete';
import './confirm_danger_modal'; import './confirm_danger_modal';
import './copy_to_clipboard';
import Flash, { removeFlashClickListener } from './flash'; import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown'; import './gl_dropdown';
import './gl_field_error'; import './gl_field_error';
...@@ -301,6 +300,8 @@ $(function () { ...@@ -301,6 +300,8 @@ $(function () {
const flashContainer = document.querySelector('.flash-container'); const flashContainer = document.querySelector('.flash-container');
if (flashContainer && flashContainer.children.length) { if (flashContainer && flashContainer.children.length) {
removeFlashClickListener(flashContainer.children[0]); flashContainer.querySelectorAll('.flash-alert, .flash-notice, .flash-success').forEach((flashEl) => {
removeFlashClickListener(flashEl);
});
} }
}); });
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
noteType: constants.COMMENT, noteType: constants.COMMENT,
// Can't use mapGetters, // Can't use mapGetters,
// this needs to be in the data object because it belongs to the state // this needs to be in the data object because it belongs to the state
issueState: this.$store.getters.getIssueData.state, issueState: this.$store.getters.getNoteableData.state,
isSubmitting: false, isSubmitting: false,
isSubmitButtonDisabled: true, isSubmitButtonDisabled: true,
}; };
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
...mapGetters([ ...mapGetters([
'getCurrentUserLastNote', 'getCurrentUserLastNote',
'getUserData', 'getUserData',
'getIssueData', 'getNoteableData',
'getNotesData', 'getNotesData',
]), ]),
isLoggedIn() { isLoggedIn() {
...@@ -59,7 +59,7 @@ ...@@ -59,7 +59,7 @@
return this.issueState === constants.OPENED || this.issueState === constants.REOPENED; return this.issueState === constants.OPENED || this.issueState === constants.REOPENED;
}, },
canCreateNote() { canCreateNote() {
return this.getIssueData.current_user.can_create_note; return this.getNoteableData.current_user.can_create_note;
}, },
issueActionButtonTitle() { issueActionButtonTitle() {
if (this.note.length) { if (this.note.length) {
...@@ -85,16 +85,16 @@ ...@@ -85,16 +85,16 @@
return this.getNotesData.quickActionsDocsPath; return this.getNotesData.quickActionsDocsPath;
}, },
markdownPreviewPath() { markdownPreviewPath() {
return this.getIssueData.preview_note_path; return this.getNoteableData.preview_note_path;
}, },
author() { author() {
return this.getUserData; return this.getUserData;
}, },
canUpdateIssue() { canUpdateIssue() {
return this.getIssueData.current_user.can_update; return this.getNoteableData.current_user.can_update;
}, },
endpoint() { endpoint() {
return this.getIssueData.create_note_path; return this.getNoteableData.create_note_path;
}, },
}, },
methods: { methods: {
...@@ -119,7 +119,7 @@ ...@@ -119,7 +119,7 @@
data: { data: {
note: { note: {
noteable_type: constants.NOTEABLE_TYPE, noteable_type: constants.NOTEABLE_TYPE,
noteable_id: this.getIssueData.id, noteable_id: this.getNoteableData.id,
note: this.note, note: this.note,
}, },
}, },
...@@ -207,7 +207,7 @@ ...@@ -207,7 +207,7 @@
}, },
initAutoSave() { initAutoSave() {
if (this.isLoggedIn) { if (this.isLoggedIn) {
this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id], 'issue'); this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getNoteableData.id], 'issue');
} }
}, },
initTaskList() { initTaskList() {
...@@ -266,9 +266,9 @@ ...@@ -266,9 +266,9 @@
<div class="error-alert"></div> <div class="error-alert"></div>
<issue-warning <issue-warning
v-if="hasWarning(getIssueData)" v-if="hasWarning(getNoteableData)"
:is-locked="isLocked(getIssueData)" :is-locked="isLocked(getNoteableData)"
:is-confidential="isConfidential(getIssueData)" :is-confidential="isConfidential(getNoteableData)"
/> />
<markdown-field <markdown-field
......
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
], ],
computed: { computed: {
...mapGetters([ ...mapGetters([
'getIssueData', 'getNoteableData',
]), ]),
discussion() { discussion() {
return this.note.notes[0]; return this.note.notes[0];
...@@ -50,10 +50,10 @@ ...@@ -50,10 +50,10 @@
return this.discussion.author; return this.discussion.author;
}, },
canReply() { canReply() {
return this.getIssueData.current_user.can_create_note; return this.getNoteableData.current_user.can_create_note;
}, },
newNotePath() { newNotePath() {
return this.getIssueData.create_note_path; return this.getNoteableData.create_note_path;
}, },
lastUpdatedBy() { lastUpdatedBy() {
const { notes } = this.note; const { notes } = this.note;
......
...@@ -46,8 +46,8 @@ ...@@ -46,8 +46,8 @@
computed: { computed: {
...mapGetters([ ...mapGetters([
'getDiscussionLastNote', 'getDiscussionLastNote',
'getIssueData', 'getNoteableData',
'getIssueDataByProp', 'getNoteableDataByProp',
'getNotesDataByProp', 'getNotesDataByProp',
'getUserDataByProp', 'getUserDataByProp',
]), ]),
...@@ -55,7 +55,7 @@ ...@@ -55,7 +55,7 @@
return `#note_${this.noteId}`; return `#note_${this.noteId}`;
}, },
markdownPreviewPath() { markdownPreviewPath() {
return this.getIssueDataByProp('preview_note_path'); return this.getNoteableDataByProp('preview_note_path');
}, },
markdownDocsPath() { markdownDocsPath() {
return this.getNotesDataByProp('markdownDocsPath'); return this.getNotesDataByProp('markdownDocsPath');
...@@ -129,9 +129,9 @@ ...@@ -129,9 +129,9 @@
class="edit-note common-note-form js-quick-submit gfm-form"> class="edit-note common-note-form js-quick-submit gfm-form">
<issue-warning <issue-warning
v-if="hasWarning(getIssueData)" v-if="hasWarning(getNoteableData)"
:is-locked="isLocked(getIssueData)" :is-locked="isLocked(getNoteableData)"
:is-confidential="isConfidential(getIssueData)" :is-confidential="isConfidential(getNoteableData)"
/> />
<markdown-field <markdown-field
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
export default { export default {
name: 'issueNotesApp', name: 'issueNotesApp',
props: { props: {
issueData: { noteableData: {
type: Object, type: Object,
required: true, required: true,
}, },
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
actionToggleAward: 'toggleAward', actionToggleAward: 'toggleAward',
scrollToNoteIfNeeded: 'scrollToNoteIfNeeded', scrollToNoteIfNeeded: 'scrollToNoteIfNeeded',
setNotesData: 'setNotesData', setNotesData: 'setNotesData',
setIssueData: 'setIssueData', setNoteableData: 'setNoteableData',
setUserData: 'setUserData', setUserData: 'setUserData',
setLastFetchedAt: 'setLastFetchedAt', setLastFetchedAt: 'setLastFetchedAt',
setTargetNoteHash: 'setTargetNoteHash', setTargetNoteHash: 'setTargetNoteHash',
...@@ -106,7 +106,7 @@ ...@@ -106,7 +106,7 @@
}, },
created() { created() {
this.setNotesData(this.notesData); this.setNotesData(this.notesData);
this.setIssueData(this.issueData); this.setNoteableData(this.noteableData);
this.setUserData(this.userData); this.setUserData(this.userData);
}, },
mounted() { mounted() {
......
...@@ -10,7 +10,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -10,7 +10,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
const notesDataset = document.getElementById('js-vue-notes').dataset; const notesDataset = document.getElementById('js-vue-notes').dataset;
return { return {
issueData: JSON.parse(notesDataset.issueData), noteableData: JSON.parse(notesDataset.noteableData),
currentUserData: JSON.parse(notesDataset.currentUserData), currentUserData: JSON.parse(notesDataset.currentUserData),
notesData: { notesData: {
lastFetchedAt: notesDataset.lastFetchedAt, lastFetchedAt: notesDataset.lastFetchedAt,
...@@ -26,7 +26,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -26,7 +26,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
render(createElement) { render(createElement) {
return createElement('issue-notes-app', { return createElement('issue-notes-app', {
props: { props: {
issueData: this.issueData, noteableData: this.noteableData,
notesData: this.notesData, notesData: this.notesData,
userData: this.currentUserData, userData: this.currentUserData,
}, },
......
...@@ -4,7 +4,7 @@ import Poll from '../../lib/utils/poll'; ...@@ -4,7 +4,7 @@ import Poll from '../../lib/utils/poll';
import * as types from './mutation_types'; import * as types from './mutation_types';
import * as utils from './utils'; import * as utils from './utils';
import * as constants from '../constants'; import * as constants from '../constants';
import service from '../services/issue_notes_service'; import service from '../services/notes_service';
import loadAwardsHandler from '../../awards_handler'; import loadAwardsHandler from '../../awards_handler';
import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
...@@ -12,7 +12,7 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; ...@@ -12,7 +12,7 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let eTagPoll; let eTagPoll;
export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data); export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
export const setIssueData = ({ commit }, data) => commit(types.SET_ISSUE_DATA, data); export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data); export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data); export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data); export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data);
......
...@@ -6,8 +6,8 @@ export const targetNoteHash = state => state.targetNoteHash; ...@@ -6,8 +6,8 @@ export const targetNoteHash = state => state.targetNoteHash;
export const getNotesData = state => state.notesData; export const getNotesData = state => state.notesData;
export const getNotesDataByProp = state => prop => state.notesData[prop]; export const getNotesDataByProp = state => prop => state.notesData[prop];
export const getIssueData = state => state.issueData; export const getNoteableData = state => state.noteableData;
export const getIssueDataByProp = state => prop => state.issueData[prop]; export const getNoteableDataByProp = state => prop => state.noteableData[prop];
export const getUserData = state => state.userData || {}; export const getUserData = state => state.userData || {};
export const getUserDataByProp = state => prop => state.userData && state.userData[prop]; export const getUserDataByProp = state => prop => state.userData && state.userData[prop];
......
...@@ -15,7 +15,7 @@ export default new Vuex.Store({ ...@@ -15,7 +15,7 @@ export default new Vuex.Store({
// holds endpoints and permissions provided through haml // holds endpoints and permissions provided through haml
notesData: {}, notesData: {},
userData: {}, userData: {},
issueData: {}, noteableData: {},
}, },
actions, actions,
getters, getters,
......
...@@ -3,7 +3,7 @@ export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION'; ...@@ -3,7 +3,7 @@ export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION';
export const DELETE_NOTE = 'DELETE_NOTE'; export const DELETE_NOTE = 'DELETE_NOTE';
export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES'; export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES';
export const SET_NOTES_DATA = 'SET_NOTES_DATA'; export const SET_NOTES_DATA = 'SET_NOTES_DATA';
export const SET_ISSUE_DATA = 'SET_ISSUE_DATA'; export const SET_NOTEABLE_DATA = 'SET_NOTEABLE_DATA';
export const SET_USER_DATA = 'SET_USER_DATA'; export const SET_USER_DATA = 'SET_USER_DATA';
export const SET_INITIAL_NOTES = 'SET_INITIAL_NOTES'; export const SET_INITIAL_NOTES = 'SET_INITIAL_NOTES';
export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT'; export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT';
......
...@@ -66,8 +66,8 @@ export default { ...@@ -66,8 +66,8 @@ export default {
Object.assign(state, { notesData: data }); Object.assign(state, { notesData: data });
}, },
[types.SET_ISSUE_DATA](state, data) { [types.SET_NOTEABLE_DATA](state, data) {
Object.assign(state, { issueData: data }); Object.assign(state, { noteableData: data });
}, },
[types.SET_USER_DATA](state, data) { [types.SET_USER_DATA](state, data) {
......
...@@ -17,13 +17,14 @@ export default class Project { ...@@ -17,13 +17,14 @@ export default class Project {
$('a', $cloneOptions).on('click', (e) => { $('a', $cloneOptions).on('click', (e) => {
const $this = $(e.currentTarget); const $this = $(e.currentTarget);
const url = $this.attr('href'); const url = $this.attr('href');
const activeText = $this.find('.dropdown-menu-inner-title').text();
e.preventDefault(); e.preventDefault();
$('.is-active', $cloneOptions).not($this).removeClass('is-active'); $('.is-active', $cloneOptions).not($this).removeClass('is-active');
$this.toggleClass('is-active'); $this.toggleClass('is-active');
$projectCloneField.val(url); $projectCloneField.val(url);
$cloneBtnText.text($this.text()); $cloneBtnText.text(activeText);
return $('.clone').text(url); return $('.clone').text(url);
}); });
......
function updateAutoDevopsRadios(radioWrappers) {
radioWrappers.forEach((radioWrapper) => {
const radio = radioWrapper.querySelector('.js-auto-devops-enable-radio');
const runPipelineCheckboxWrapper = radioWrapper.querySelector('.js-run-auto-devops-pipeline-checkbox-wrapper');
const runPipelineCheckbox = radioWrapper.querySelector('.js-run-auto-devops-pipeline-checkbox');
if (runPipelineCheckbox) {
runPipelineCheckbox.checked = radio.checked;
runPipelineCheckboxWrapper.classList.toggle('hide', !radio.checked);
}
});
}
export default function initCiCdSettings() {
const radioWrappers = document.querySelectorAll('.js-auto-devops-enable-radio-wrapper');
radioWrappers.forEach(radioWrapper =>
radioWrapper.addEventListener('change', () => updateAutoDevopsRadios(radioWrappers)),
);
}
...@@ -48,6 +48,27 @@ export default { ...@@ -48,6 +48,27 @@ export default {
} }
return this.projectName; return this.projectName;
}, },
/**
* Smartly truncates project namespace by doing two things;
* 1. Only include Group names in path by removing project name
* 2. Only include first and last group names in the path
* when namespace has more than 2 groups present
*
* First part (removal of project name from namespace) can be
* done from backend but doing so involves migration of
* existing project namespaces which is not wise thing to do.
*/
truncatedNamespace() {
const namespaceArr = this.namespace.split(' / ');
namespaceArr.splice(-1, 1);
let namespace = namespaceArr.join(' / ');
if (namespaceArr.length > 2) {
namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`;
}
return namespace;
},
}, },
}; };
</script> </script>
...@@ -87,9 +108,7 @@ export default { ...@@ -87,9 +108,7 @@ export default {
<div <div
class="project-namespace" class="project-namespace"
:title="namespace" :title="namespace"
> >{{truncatedNamespace}}</div>
{{namespace}}
</div>
</div> </div>
</a> </a>
</li> </li>
......
<script>
import icon from '../../../vue_shared/components/icon.vue';
import listItem from './list_item.vue';
import listCollapsed from './list_collapsed.vue';
export default {
components: {
icon,
listItem,
listCollapsed,
},
props: {
title: {
type: String,
required: true,
},
fileList: {
type: Array,
required: true,
},
collapsed: {
type: Boolean,
required: true,
},
},
methods: {
toggleCollapsed() {
this.$emit('toggleCollapsed');
},
},
};
</script>
<template>
<div class="multi-file-commit-panel-section">
<header
class="multi-file-commit-panel-header"
:class="{
'is-collapsed': collapsed,
}"
>
<icon
name="list-bulleted"
:size="18"
css-classes="append-right-default"
/>
<template v-if="!collapsed">
{{ title }}
<button
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
@click="toggleCollapsed"
>
<i
aria-hidden="true"
class="fa fa-angle-double-right"
>
</i>
</button>
</template>
</header>
<div class="multi-file-commit-list">
<list-collapsed
v-if="collapsed"
/>
<template v-else>
<ul
v-if="fileList.length"
class="list-unstyled append-bottom-0"
>
<li
v-for="file in fileList"
:key="file.key"
>
<list-item
:file="file"
/>
</li>
</ul>
<div
v-else
class="help-block prepend-top-0"
>
No changes
</div>
</template>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import icon from '../../../vue_shared/components/icon.vue';
export default {
components: {
icon,
},
computed: {
...mapGetters([
'addedFiles',
'modifiedFiles',
]),
},
};
</script>
<template>
<div
class="multi-file-commit-list-collapsed text-center"
>
<icon
name="file-addition"
:size="18"
css-classes="multi-file-addition append-bottom-10"
/>
{{ addedFiles.length }}
<icon
name="file-modified"
:size="18"
css-classes="multi-file-modified prepend-top-10 append-bottom-10"
/>
{{ modifiedFiles.length }}
</div>
</template>
<script>
import icon from '../../../vue_shared/components/icon.vue';
export default {
components: {
icon,
},
props: {
file: {
type: Object,
required: true,
},
},
computed: {
iconName() {
return this.file.tempFile ? 'file-addition' : 'file-modified';
},
iconClass() {
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
},
},
};
</script>
<template>
<div class="multi-file-commit-list-item">
<icon
:name="iconName"
:size="16"
:css-classes="iconClass"
/>
<span class="multi-file-commit-list-path">
{{ file.path }}
</span>
</div>
</template>
...@@ -40,20 +40,24 @@ export default { ...@@ -40,20 +40,24 @@ export default {
</script> </script>
<template> <template>
<div class="repository-view"> <div
<div class="tree-content-holder" :class="{'tree-content-holder-mini' : isCollapsed}"> class="multi-file"
<repo-sidebar/> :class="{
<div 'is-collapsed': isCollapsed
v-if="isCollapsed" }"
class="panel-right" >
> <repo-sidebar/>
<repo-tabs/> <div
<component v-if="isCollapsed"
:is="currentBlobView" class="multi-file-edit-pane"
/> >
<repo-file-buttons/> <repo-tabs />
</div> <component
class="multi-file-edit-pane-content"
:is="currentBlobView"
/>
<repo-file-buttons />
</div> </div>
<repo-commit-section v-if="changedFiles.length" /> <repo-commit-section />
</div> </div>
</template> </template>
<script> <script>
import { mapGetters, mapState, mapActions } from 'vuex'; import { mapGetters, mapState, mapActions } from 'vuex';
import tooltip from '../../vue_shared/directives/tooltip';
import icon from '../../vue_shared/components/icon.vue';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue'; import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import { n__ } from '../../locale'; import commitFilesList from './commit_sidebar/list.vue';
export default { export default {
components: { components: {
PopupDialog, PopupDialog,
icon,
commitFilesList,
},
directives: {
tooltip,
}, },
data() { data() {
return { return {
...@@ -13,6 +20,7 @@ export default { ...@@ -13,6 +20,7 @@ export default {
submitCommitsLoading: false, submitCommitsLoading: false,
startNewMR: false, startNewMR: false,
commitMessage: '', commitMessage: '',
collapsed: true,
}; };
}, },
computed: { computed: {
...@@ -23,10 +31,10 @@ export default { ...@@ -23,10 +31,10 @@ export default {
'changedFiles', 'changedFiles',
]), ]),
commitButtonDisabled() { commitButtonDisabled() {
return !this.commitMessage || this.submitCommitsLoading; return this.commitMessage === '' || this.submitCommitsLoading || !this.changedFiles.length;
}, },
commitButtonText() { commitMessageCount() {
return n__('Commit %d file', 'Commit %d files', this.changedFiles.length); return this.commitMessage.length;
}, },
}, },
methods: { methods: {
...@@ -77,12 +85,20 @@ export default { ...@@ -77,12 +85,20 @@ export default {
this.submitCommitsLoading = false; this.submitCommitsLoading = false;
}); });
}, },
toggleCollapsed() {
this.collapsed = !this.collapsed;
},
}, },
}; };
</script> </script>
<template> <template>
<div id="commit-area"> <div
class="multi-file-commit-panel"
:class="{
'is-collapsed': collapsed,
}"
>
<popup-dialog <popup-dialog
v-if="showNewBranchDialog" v-if="showNewBranchDialog"
:primary-button-label="__('Create new branch')" :primary-button-label="__('Create new branch')"
...@@ -92,78 +108,71 @@ export default { ...@@ -92,78 +108,71 @@ export default {
@toggle="showNewBranchDialog = false" @toggle="showNewBranchDialog = false"
@submit="makeCommit(true)" @submit="makeCommit(true)"
/> />
<button
v-if="collapsed"
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn is-collapsed prepend-top-10 append-bottom-10"
@click="toggleCollapsed"
>
<i
aria-hidden="true"
class="fa fa-angle-double-left"
>
</i>
</button>
<commit-files-list
title="Staged"
:file-list="changedFiles"
:collapsed="collapsed"
@toggleCollapsed="toggleCollapsed"
/>
<form <form
class="form-horizontal" class="form-horizontal multi-file-commit-form"
@submit.prevent="tryCommit()"> @submit.prevent="tryCommit"
<fieldset> v-if="!collapsed"
<div class="form-group"> >
<label class="col-md-4 control-label staged-files"> <div class="multi-file-commit-fieldset">
Staged files ({{changedFiles.length}}) <textarea
</label> class="form-control multi-file-commit-message"
<div class="col-md-6"> name="commit-message"
<ul class="list-unstyled changed-files"> v-model="commitMessage"
<li placeholder="Commit message"
v-for="(file, index) in changedFiles" >
:key="index"> </textarea>
<span class="help-block"> </div>
{{ file.path }} <div class="multi-file-commit-fieldset">
</span> <label
</li> v-tooltip
</ul> title="Create a new merge request with these changes"
</div> data-container="body"
</div> data-placement="top"
<div class="form-group"> >
<label <input
class="col-md-4 control-label" type="checkbox"
for="commit-message"> v-model="startNewMR"
Commit message />
</label> Merge Request
<div class="col-md-6"> </label>
<textarea <button
id="commit-message" type="submit"
class="form-control" :disabled="commitButtonDisabled"
name="commit-message" class="btn btn-default btn-sm append-right-10 prepend-left-10"
v-model="commitMessage"> >
</textarea> <i
</div> v-if="submitCommitsLoading"
</div> class="js-commit-loading-icon fa fa-spinner fa-spin"
<div class="form-group target-branch"> aria-hidden="true"
<label aria-label="loading"
class="col-md-4 control-label" >
for="target-branch"> </i>
Target branch Commit
</label> </button>
<div class="col-md-6"> <div
<span class="help-block"> class="multi-file-commit-message-count"
{{currentBranch}} >
</span> {{ commitMessageCount }}
</div>
</div>
<div class="col-md-offset-4 col-md-6">
<button
type="submit"
:disabled="commitButtonDisabled"
class="btn btn-success">
<i
v-if="submitCommitsLoading"
class="js-commit-loading-icon fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="loading">
</i>
<span class="commit-summary">
{{ commitButtonText }}
</span>
</button>
</div>
<div class="col-md-offset-4 col-md-6">
<div class="checkbox">
<label>
<input type="checkbox" v-model="startNewMR">
<span>Start a <strong>new merge request</strong> with these changes</span>
</label>
</div>
</div> </div>
</fieldset> </div>
</form> </form>
</div> </div>
</template> </template>
...@@ -3,19 +3,18 @@ ...@@ -3,19 +3,18 @@
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import flash from '../../flash'; import flash from '../../flash';
import monacoLoader from '../monaco_loader'; import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
export default { export default {
destroyed() { beforeDestroy() {
if (this.monacoInstance) { this.editor.dispose();
this.monacoInstance.destroy();
}
}, },
mounted() { mounted() {
if (this.monaco) { if (this.editor && monaco) {
this.initMonaco(); this.initMonaco();
} else { } else {
monacoLoader(['vs/editor/editor.main'], () => { monacoLoader(['vs/editor/editor.main'], () => {
this.monaco = monaco; this.editor = Editor.create(monaco);
this.initMonaco(); this.initMonaco();
}); });
...@@ -29,47 +28,25 @@ export default { ...@@ -29,47 +28,25 @@ export default {
initMonaco() { initMonaco() {
if (this.shouldHideEditor) return; if (this.shouldHideEditor) return;
if (this.monacoInstance) { this.editor.clearEditor();
this.monacoInstance.setModel(null);
}
this.getRawFileData(this.activeFile) this.getRawFileData(this.activeFile)
.then(() => { .then(() => {
if (!this.monacoInstance) { this.editor.createInstance(this.$refs.editor);
this.monacoInstance = this.monaco.editor.create(this.$el, {
model: null,
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
});
this.languages = this.monaco.languages.getLanguages();
this.addMonacoEvents();
}
this.setupEditor();
}) })
.then(() => this.setupEditor())
.catch(() => flash('Error setting up monaco. Please try again.')); .catch(() => flash('Error setting up monaco. Please try again.'));
}, },
setupEditor() { setupEditor() {
if (!this.activeFile) return; if (!this.activeFile) return;
const content = this.activeFile.content !== '' ? this.activeFile.content : this.activeFile.raw;
const foundLang = this.languages.find(lang => const model = this.editor.createModel(this.activeFile);
lang.extensions && lang.extensions.indexOf(this.activeFileExtension) === 0,
);
const newModel = this.monaco.editor.createModel(
content, foundLang ? foundLang.id : 'plaintext',
);
this.monacoInstance.setModel(newModel); this.editor.attachModel(model);
}, model.onChange((m) => {
addMonacoEvents() {
this.monacoInstance.onKeyUp(() => {
this.changeFileContent({ this.changeFileContent({
file: this.activeFile, file: this.activeFile,
content: this.monacoInstance.getValue(), content: m.getValue(),
}); });
}); });
}, },
...@@ -99,9 +76,14 @@ export default { ...@@ -99,9 +76,14 @@ export default {
class="blob-viewer-container blob-editor-container" class="blob-viewer-container blob-editor-container"
> >
<div <div
v-if="shouldHideEditor" v-show="shouldHideEditor"
v-html="activeFile.html" v-html="activeFile.html"
> >
</div> </div>
<div
v-show="!shouldHideEditor"
ref="editor"
>
</div>
</div> </div>
</template> </template>
...@@ -55,7 +55,7 @@ ...@@ -55,7 +55,7 @@
class="file" class="file"
@click.prevent="clickedTreeRow(file)"> @click.prevent="clickedTreeRow(file)">
<td <td
class="multi-file-table-col-name" class="multi-file-table-name"
:colspan="submoduleColSpan" :colspan="submoduleColSpan"
> >
<i <i
...@@ -85,12 +85,11 @@ ...@@ -85,12 +85,11 @@
</td> </td>
<template v-if="!isCollapsed && !isSubmodule"> <template v-if="!isCollapsed && !isSubmodule">
<td class="hidden-sm hidden-xs"> <td class="multi-file-table-col-commit-message hidden-sm hidden-xs">
<a <a
v-if="file.lastCommit.message" v-if="file.lastCommit.message"
@click.stop @click.stop
:href="file.lastCommit.url" :href="file.lastCommit.url"
class="commit-message"
> >
{{ file.lastCommit.message }} {{ file.lastCommit.message }}
</a> </a>
......
...@@ -22,12 +22,12 @@ export default { ...@@ -22,12 +22,12 @@ export default {
<template> <template>
<div <div
v-if="showButtons" v-if="showButtons"
class="repo-file-buttons" class="multi-file-editor-btn-group"
> >
<a <a
:href="activeFile.rawPath" :href="activeFile.rawPath"
target="_blank" target="_blank"
class="btn btn-default raw" class="btn btn-default btn-sm raw"
rel="noopener noreferrer"> rel="noopener noreferrer">
{{ rawDownloadButtonLabel }} {{ rawDownloadButtonLabel }}
</a> </a>
...@@ -38,17 +38,17 @@ export default { ...@@ -38,17 +38,17 @@ export default {
aria-label="File actions"> aria-label="File actions">
<a <a
:href="activeFile.blamePath" :href="activeFile.blamePath"
class="btn btn-default blame"> class="btn btn-default btn-sm blame">
Blame Blame
</a> </a>
<a <a
:href="activeFile.commitsPath" :href="activeFile.commitsPath"
class="btn btn-default history"> class="btn btn-default btn-sm history">
History History
</a> </a>
<a <a
:href="activeFile.permalink" :href="activeFile.permalink"
class="btn btn-default permalink"> class="btn btn-default btn-sm permalink">
Permalink Permalink
</a> </a>
</div> </div>
......
...@@ -32,10 +32,12 @@ export default { ...@@ -32,10 +32,12 @@ export default {
</script> </script>
<template> <template>
<div class="blob-viewer-container"> <div>
<div <div
v-if="!activeFile.renderError" v-if="!activeFile.renderError"
v-html="activeFile.html"> v-html="activeFile.html"
class="multi-file-preview-holder"
>
</div> </div>
<div <div
v-else-if="activeFile.tempFile" v-else-if="activeFile.tempFile"
......
...@@ -44,20 +44,16 @@ export default { ...@@ -44,20 +44,16 @@ export default {
</script> </script>
<template> <template>
<div id="sidebar" :class="{'sidebar-mini' : isCollapsed}"> <div class="ide-file-list">
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th <th
v-if="isCollapsed" v-if="isCollapsed"
class="repo-file-options title"
> >
<strong class="clgray">
{{ projectName }}
</strong>
</th> </th>
<template v-else> <template v-else>
<th class="name multi-file-table-col-name"> <th class="name multi-file-table-name">
Name Name
</th> </th>
<th class="hidden-sm hidden-xs last-commit"> <th class="hidden-sm hidden-xs last-commit">
...@@ -79,7 +75,7 @@ export default { ...@@ -79,7 +75,7 @@ export default {
:key="n" :key="n"
/> />
<repo-file <repo-file
v-for="(file, index) in treeList" v-for="file in treeList"
:key="file.key" :key="file.key"
:file="file" :file="file"
/> />
......
...@@ -36,27 +36,32 @@ export default { ...@@ -36,27 +36,32 @@ export default {
<template> <template>
<li <li
:class="{ active : tab.active }"
@click="setFileActive(tab)" @click="setFileActive(tab)"
> >
<button <button
type="button" type="button"
class="close-btn" class="multi-file-tab-close"
@click.stop.prevent="closeFile({ file: tab })" @click.stop.prevent="closeFile({ file: tab })"
:aria-label="closeLabel"> :aria-label="closeLabel"
:class="{
'modified': tab.changed,
}"
:disabled="tab.changed"
>
<i <i
class="fa" class="fa"
:class="changedClass" :class="changedClass"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
</button> </button>
<a <div
href="#" class="multi-file-tab"
class="repo-tab" :class="{active : tab.active }"
:title="tab.url" :title="tab.url"
@click.prevent.stop="setFileActive(tab)"> >
{{tab.name}} {{ tab.name }}
</a> </div>
</li> </li>
</template> </template>
...@@ -16,14 +16,12 @@ ...@@ -16,14 +16,12 @@
<template> <template>
<ul <ul
id="tabs" class="multi-file-tabs list-unstyled append-bottom-0"
class="list-unstyled"
> >
<repo-tab <repo-tab
v-for="tab in openFiles" v-for="tab in openFiles"
:key="tab.id" :key="tab.id"
:tab="tab" :tab="tab"
/> />
<li class="tabs-divider" />
</ul> </ul>
</template> </template>
export default class Disposable {
constructor() {
this.disposers = new Set();
}
add(...disposers) {
disposers.forEach(disposer => this.disposers.add(disposer));
}
dispose() {
this.disposers.forEach(disposer => disposer.dispose());
this.disposers.clear();
}
}
/* global monaco */
import Disposable from './disposable';
export default class Model {
constructor(monaco, file) {
this.monaco = monaco;
this.disposable = new Disposable();
this.file = file;
this.content = file.content !== '' ? file.content : file.raw;
this.disposable.add(
this.originalModel = this.monaco.editor.createModel(
this.file.raw,
undefined,
new this.monaco.Uri(null, null, `original/${this.file.path}`),
),
this.model = this.monaco.editor.createModel(
this.content,
undefined,
new this.monaco.Uri(null, null, this.file.path),
),
);
this.events = new Map();
}
get url() {
return this.model.uri.toString();
}
get path() {
return this.file.path;
}
getModel() {
return this.model;
}
getOriginalModel() {
return this.originalModel;
}
onChange(cb) {
this.events.set(
this.path,
this.disposable.add(
this.model.onDidChangeContent(e => cb(this.model, e)),
),
);
}
dispose() {
this.disposable.dispose();
this.events.clear();
}
}
import Disposable from './disposable';
import Model from './model';
export default class ModelManager {
constructor(monaco) {
this.monaco = monaco;
this.disposable = new Disposable();
this.models = new Map();
}
hasCachedModel(path) {
return this.models.has(path);
}
addModel(file) {
if (this.hasCachedModel(file.path)) {
return this.models.get(file.path);
}
const model = new Model(this.monaco, file);
this.models.set(model.path, model);
this.disposable.add(model);
return model;
}
dispose() {
// dispose of all the models
this.disposable.dispose();
this.models.clear();
}
}
export default class DecorationsController {
constructor(editor) {
this.editor = editor;
this.decorations = new Map();
this.editorDecorations = new Map();
}
getAllDecorationsForModel(model) {
if (!this.decorations.has(model.url)) return [];
const modelDecorations = this.decorations.get(model.url);
const decorations = [];
modelDecorations.forEach(val => decorations.push(...val));
return decorations;
}
addDecorations(model, decorationsKey, decorations) {
const decorationMap = this.decorations.get(model.url) || new Map();
decorationMap.set(decorationsKey, decorations);
this.decorations.set(model.url, decorationMap);
this.decorate(model);
}
decorate(model) {
const decorations = this.getAllDecorationsForModel(model);
const oldDecorations = this.editorDecorations.get(model.url) || [];
this.editorDecorations.set(
model.url,
this.editor.instance.deltaDecorations(oldDecorations, decorations),
);
}
dispose() {
this.decorations.clear();
this.editorDecorations.clear();
}
}
/* global monaco */
import { throttle } from 'underscore';
import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable';
export const getDiffChangeType = (change) => {
if (change.modified) {
return 'modified';
} else if (change.added) {
return 'added';
} else if (change.removed) {
return 'removed';
}
return '';
};
export const getDecorator = change => ({
range: new monaco.Range(
change.lineNumber,
1,
change.endLineNumber,
1,
),
options: {
isWholeLine: true,
linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
},
});
export default class DirtyDiffController {
constructor(modelManager, decorationsController) {
this.disposable = new Disposable();
this.editorSimpleWorker = null;
this.modelManager = modelManager;
this.decorationsController = decorationsController;
this.dirtyDiffWorker = new DirtyDiffWorker();
this.throttledComputeDiff = throttle(this.computeDiff, 250);
this.decorate = this.decorate.bind(this);
this.dirtyDiffWorker.addEventListener('message', this.decorate);
}
attachModel(model) {
model.onChange(() => this.throttledComputeDiff(model));
}
computeDiff(model) {
this.dirtyDiffWorker.postMessage({
path: model.path,
originalContent: model.getOriginalModel().getValue(),
newContent: model.getModel().getValue(),
});
}
reDecorate(model) {
this.decorationsController.decorate(model);
}
decorate({ data }) {
const decorations = data.changes.map(change => getDecorator(change));
this.decorationsController.addDecorations(data.path, 'dirtyDiff', decorations);
}
dispose() {
this.disposable.dispose();
this.dirtyDiffWorker.removeEventListener('message', this.decorate);
this.dirtyDiffWorker.terminate();
}
}
import { diffLines } from 'diff';
// eslint-disable-next-line import/prefer-default-export
export const computeDiff = (originalContent, newContent) => {
const changes = diffLines(originalContent, newContent);
let lineNumber = 1;
return changes.reduce((acc, change) => {
const findOnLine = acc.find(c => c.lineNumber === lineNumber);
if (findOnLine) {
Object.assign(findOnLine, change, {
modified: true,
endLineNumber: (lineNumber + change.count) - 1,
});
} else if ('added' in change || 'removed' in change) {
acc.push(Object.assign({}, change, {
lineNumber,
modified: undefined,
endLineNumber: (lineNumber + change.count) - 1,
}));
}
if (!change.removed) {
lineNumber += change.count;
}
return acc;
}, []);
};
import { computeDiff } from './diff';
self.addEventListener('message', (e) => {
const data = e.data;
self.postMessage({
path: data.path,
changes: computeDiff(data.originalContent, data.newContent),
});
});
import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller';
import Disposable from './common/disposable';
import ModelManager from './common/model_manager';
import editorOptions from './editor_options';
export default class Editor {
static create(monaco) {
this.editorInstance = new Editor(monaco);
return this.editorInstance;
}
constructor(monaco) {
this.monaco = monaco;
this.currentModel = null;
this.instance = null;
this.dirtyDiffController = null;
this.disposable = new Disposable();
this.disposable.add(
this.modelManager = new ModelManager(this.monaco),
this.decorationsController = new DecorationsController(this),
);
}
createInstance(domElement) {
if (!this.instance) {
this.disposable.add(
this.instance = this.monaco.editor.create(domElement, {
model: null,
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
}),
this.dirtyDiffController = new DirtyDiffController(
this.modelManager, this.decorationsController,
),
);
}
}
createModel(file) {
return this.modelManager.addModel(file);
}
attachModel(model) {
this.instance.setModel(model.getModel());
this.dirtyDiffController.attachModel(model);
this.currentModel = model;
this.instance.updateOptions(editorOptions.reduce((acc, obj) => {
Object.keys(obj).forEach((key) => {
Object.assign(acc, {
[key]: obj[key](model),
});
});
return acc;
}, {}));
this.dirtyDiffController.reDecorate(model);
}
clearEditor() {
if (this.instance) {
this.instance.setModel(null);
}
}
dispose() {
this.disposable.dispose();
// dispose main monaco instance
if (this.instance) {
this.instance = null;
}
}
}
...@@ -16,6 +16,10 @@ export default { ...@@ -16,6 +16,10 @@ export default {
return Promise.resolve(file.content); return Promise.resolve(file.content);
} }
if (file.raw) {
return Promise.resolve(file.raw);
}
return Vue.http.get(file.rawPath, { params: { format: 'json' } }) return Vue.http.get(file.rawPath, { params: { format: 'json' } })
.then(res => res.text()); .then(res => res.text());
}, },
......
...@@ -34,3 +34,7 @@ export const canEditFile = (state) => { ...@@ -34,3 +34,7 @@ export const canEditFile = (state) => {
openedFiles.length && openedFiles.length &&
(currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary); (currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
}; };
export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
export const modifiedFiles = state => changedFiles(state).filter(f => !f.tempFile);
...@@ -12,6 +12,9 @@ ...@@ -12,6 +12,9 @@
/> />
*/ */
// only allow classes in images.scss e.g. s12
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
export default { export default {
props: { props: {
name: { name: {
...@@ -22,7 +25,10 @@ ...@@ -22,7 +25,10 @@
size: { size: {
type: Number, type: Number,
required: false, required: false,
default: 0, default: 16,
validator(value) {
return validSizes.includes(value);
},
}, },
cssClasses: { cssClasses: {
...@@ -42,10 +48,11 @@ ...@@ -42,10 +48,11 @@
}, },
}; };
</script> </script>
<template> <template>
<svg <svg
:class="[iconSizeClass, cssClasses]"> :class="[iconSizeClass, cssClasses]">
<use <use
v-bind="{'xlink:href':spriteHref}"/> v-bind="{'xlink:href':spriteHref}"/>
</svg> </svg>
</template> </template>
...@@ -30,6 +30,11 @@ ...@@ -30,6 +30,11 @@
required: false, required: false,
default: true, default: true,
}, },
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
},
}, },
data() { data() {
return { return {
...@@ -97,7 +102,7 @@ ...@@ -97,7 +102,7 @@
/* /*
GLForm class handles all the toolbar buttons GLForm class handles all the toolbar buttons
*/ */
return new GLForm($(this.$refs['gl-form']), true); return new GLForm($(this.$refs['gl-form']), this.enableAutocomplete);
}, },
beforeDestroy() { beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('gl-form'); const glForm = $(this.$refs['gl-form']).data('gl-form');
......
<script>
import Pikaday from 'pikaday';
import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix';
export default {
name: 'datePicker',
props: {
label: {
type: String,
required: false,
default: 'Date picker',
},
selectedDate: {
type: Date,
required: false,
},
minDate: {
type: Date,
required: false,
},
maxDate: {
type: Date,
required: false,
},
},
methods: {
selected(dateText) {
this.$emit('newDateSelected', this.calendar.toString(dateText));
},
toggled() {
this.$emit('hidePicker');
},
},
mounted() {
this.calendar = new Pikaday({
field: this.$el.querySelector('.dropdown-menu-toggle'),
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
container: this.$el,
defaultDate: this.selectedDate,
setDefaultDate: !!this.selectedDate,
minDate: this.minDate,
maxDate: this.maxDate,
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect: this.selected.bind(this),
onClose: this.toggled.bind(this),
});
this.$el.append(this.calendar.el);
this.calendar.show();
},
beforeDestroy() {
this.calendar.destroy();
},
};
</script>
<template>
<div class="pikaday-container">
<div class="dropdown open">
<button
type="button"
class="dropdown-menu-toggle"
data-toggle="dropdown"
@click="toggled"
>
<span class="dropdown-toggle-text">
{{label}}
</span>
<i
class="fa fa-chevron-down"
aria-hidden="true"
>
</i>
</button>
</div>
</div>
</template>
<script>
export default {
name: 'collapsedCalendarIcon',
props: {
containerClass: {
type: String,
required: false,
default: '',
},
text: {
type: String,
required: false,
default: '',
},
showIcon: {
type: Boolean,
required: false,
default: true,
},
},
methods: {
click() {
this.$emit('click');
},
},
};
</script>
<template>
<div
:class="containerClass"
@click="click"
>
<i
v-if="showIcon"
class="fa fa-calendar"
aria-hidden="true"
>
</i>
<slot>
<span>
{{ text }}
</span>
</slot>
</div>
</template>
<script>
import { dateInWords } from '../../../lib/utils/datetime_utility';
import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
export default {
name: 'sidebarCollapsedGroupedDatePicker',
props: {
collapsed: {
type: Boolean,
required: false,
default: true,
},
showToggleSidebar: {
type: Boolean,
required: false,
default: false,
},
minDate: {
type: Date,
required: false,
},
maxDate: {
type: Date,
required: false,
},
disableClickableIcons: {
type: Boolean,
required: false,
default: false,
},
},
components: {
toggleSidebar,
collapsedCalendarIcon,
},
computed: {
hasMinAndMaxDates() {
return this.minDate && this.maxDate;
},
hasNoMinAndMaxDates() {
return !this.minDate && !this.maxDate;
},
showMinDateBlock() {
return this.minDate || this.hasNoMinAndMaxDates;
},
showFromText() {
return !this.maxDate && this.minDate;
},
iconClass() {
const disabledClass = this.disableClickableIcons ? 'disabled' : '';
return `block sidebar-collapsed-icon calendar-icon ${disabledClass}`;
},
},
methods: {
toggleSidebar() {
this.$emit('toggleCollapse');
},
dateText(dateType = 'min') {
const date = this[`${dateType}Date`];
const dateWords = dateInWords(date, true);
const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords;
return date ? parsedDateWords : 'None';
},
},
};
</script>
<template>
<div class="block sidebar-grouped-item">
<div
v-if="showToggleSidebar"
class="issuable-sidebar-header"
>
<toggle-sidebar
:collapsed="collapsed"
@toggle="toggleSidebar"
/>
</div>
<collapsed-calendar-icon
v-if="showMinDateBlock"
:container-class="iconClass"
@click="toggleSidebar"
>
<span class="sidebar-collapsed-value">
<span v-if="showFromText">From</span>
<span>{{ dateText('min') }}</span>
</span>
</collapsed-calendar-icon>
<div
v-if="hasMinAndMaxDates"
class="text-center sidebar-collapsed-divider"
>
-
</div>
<collapsed-calendar-icon
v-if="maxDate"
:container-class="iconClass"
:show-icon="!minDate"
@click="toggleSidebar"
>
<span class="sidebar-collapsed-value">
<span v-if="!minDate">Until</span>
<span>{{ dateText('max') }}</span>
</span>
</collapsed-calendar-icon>
</div>
</template>
<script>
import datePicker from '../pikaday.vue';
import loadingIcon from '../loading_icon.vue';
import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
import { dateInWords } from '../../../lib/utils/datetime_utility';
export default {
name: 'sidebarDatePicker',
props: {
collapsed: {
type: Boolean,
required: false,
default: true,
},
showToggleSidebar: {
type: Boolean,
required: false,
default: false,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
editable: {
type: Boolean,
required: false,
default: false,
},
label: {
type: String,
required: false,
default: 'Date picker',
},
selectedDate: {
type: Date,
required: false,
},
minDate: {
type: Date,
required: false,
},
maxDate: {
type: Date,
required: false,
},
},
data() {
return {
editing: false,
};
},
components: {
datePicker,
toggleSidebar,
loadingIcon,
collapsedCalendarIcon,
},
computed: {
selectedAndEditable() {
return this.selectedDate && this.editable;
},
selectedDateWords() {
return dateInWords(this.selectedDate, true);
},
collapsedText() {
return this.selectedDateWords ? this.selectedDateWords : 'None';
},
},
methods: {
stopEditing() {
this.editing = false;
},
toggleDatePicker() {
this.editing = !this.editing;
},
newDateSelected(date = null) {
this.date = date;
this.editing = false;
this.$emit('saveDate', date);
},
toggleSidebar() {
this.$emit('toggleCollapse');
},
},
};
</script>
<template>
<div class="block">
<div class="issuable-sidebar-header">
<toggle-sidebar
:collapsed="collapsed"
@toggle="toggleSidebar"
/>
</div>
<collapsed-calendar-icon
class="sidebar-collapsed-icon"
:text="collapsedText"
/>
<div class="title">
{{ label }}
<loading-icon
v-if="isLoading"
:inline="true"
/>
<div class="pull-right">
<button
v-if="editable && !editing"
type="button"
class="btn-blank btn-link btn-primary-hover-link btn-sidebar-action"
@click="toggleDatePicker"
>
Edit
</button>
<toggle-sidebar
v-if="showToggleSidebar"
:collapsed="collapsed"
@toggle="toggleSidebar"
/>
</div>
</div>
<div class="value">
<date-picker
v-if="editing"
:selected-date="selectedDate"
:min-date="minDate"
:max-date="maxDate"
:label="label"
@newDateSelected="newDateSelected"
@hidePicker="stopEditing"
/>
<span
v-else
class="value-content"
>
<template v-if="selectedDate">
<strong>{{ selectedDateWords }}</strong>
<span
v-if="selectedAndEditable"
class="no-value"
>
-
<button
type="button"
class="btn-blank btn-link btn-secondary-hover-link"
@click="newDateSelected(null)"
>
remove
</button>
</span>
</template>
<span
v-else
class="no-value"
>
None
</span>
</span>
</div>
</div>
</template>
<script>
export default {
name: 'toggleSidebar',
props: {
collapsed: {
type: Boolean,
required: true,
},
},
methods: {
toggle() {
this.$emit('toggle');
},
},
};
</script>
<template>
<button
type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action"
@click="toggle"
>
<i
aria-label="toggle collapse"
class="fa"
:class="{ 'fa-angle-double-right': !collapsed, 'fa-angle-double-left': collapsed }"
></i>
</button>
</template>
...@@ -71,7 +71,7 @@ export default class ZenMode { ...@@ -71,7 +71,7 @@ export default class ZenMode {
this.active_textarea = this.active_backdrop.find('textarea'); this.active_textarea = this.active_backdrop.find('textarea');
// Prevent a user-resized textarea from persisting to fullscreen // Prevent a user-resized textarea from persisting to fullscreen
this.active_textarea.removeAttr('style'); this.active_textarea.removeAttr('style');
return this.active_textarea.focus(); this.active_textarea.focus();
} }
exit() { exit() {
...@@ -81,7 +81,11 @@ export default class ZenMode { ...@@ -81,7 +81,11 @@ export default class ZenMode {
this.scrollTo(this.active_textarea); this.scrollTo(this.active_textarea);
this.active_textarea = null; this.active_textarea = null;
this.active_backdrop = null; this.active_backdrop = null;
return Dropzone.forElement('.div-dropzone').enable();
const $dropzone = $('.div-dropzone');
if ($dropzone && !$dropzone.hasClass('js-invalid-dropzone')) {
Dropzone.forElement('.div-dropzone').enable();
}
} }
} }
......
...@@ -125,7 +125,7 @@ ...@@ -125,7 +125,7 @@
@include transition(border-color); @include transition(border-color);
} }
.note-action-button .link-highlight, .note-action-button,
.toolbar-btn, .toolbar-btn,
.dropdown-toggle-caret { .dropdown-toggle-caret {
@include transition(color); @include transition(color);
......
...@@ -88,17 +88,6 @@ ...@@ -88,17 +88,6 @@
border-color: $border-dark; border-color: $border-dark;
color: $color; color: $color;
} }
svg {
path {
fill: $color;
}
use {
stroke: $color;
}
}
} }
@mixin btn-green { @mixin btn-green {
...@@ -142,6 +131,13 @@ ...@@ -142,6 +131,13 @@
} }
} }
@mixin btn-svg {
height: $gl-padding;
width: $gl-padding;
top: 0;
vertical-align: text-top;
}
.btn { .btn {
@include btn-default; @include btn-default;
@include btn-white; @include btn-white;
...@@ -408,6 +404,7 @@ ...@@ -408,6 +404,7 @@
padding: 0; padding: 0;
background: transparent; background: transparent;
border: 0; border: 0;
border-radius: 0;
&:hover, &:hover,
&:active, &:active,
...@@ -417,3 +414,29 @@ ...@@ -417,3 +414,29 @@
box-shadow: none; box-shadow: none;
} }
} }
.btn-link.btn-secondary-hover-link {
color: $gl-text-color-secondary;
&:hover,
&:active,
&:focus {
color: $gl-link-color;
text-decoration: none;
}
}
.btn-link.btn-primary-hover-link {
color: inherit;
&:hover,
&:active,
&:focus {
color: $gl-link-color;
text-decoration: none;
}
}
.btn-svg svg {
@include btn-svg;
}
...@@ -2,14 +2,43 @@ ...@@ -2,14 +2,43 @@
.cgray { color: $common-gray; } .cgray { color: $common-gray; }
.clgray { color: $common-gray-light; } .clgray { color: $common-gray-light; }
.cred { color: $common-red; } .cred { color: $common-red; }
svg.cred { fill: $common-red; }
.cgreen { color: $common-green; } .cgreen { color: $common-green; }
svg.cgreen { fill: $common-green; }
.cdark { color: $common-gray-dark; } .cdark { color: $common-gray-dark; }
.text-plain,
.text-plain:hover {
color: $gl-text-color;
}
.text-secondary { .text-secondary {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
} }
.text-primary,
.text-primary:hover {
color: $brand-primary;
}
.text-success,
.text-success:hover {
color: $brand-success;
}
.text-danger,
.text-danger:hover {
color: $brand-danger;
}
.text-warning,
.text-warning:hover {
color: $brand-warning;
}
.text-info,
.text-info:hover {
color: $brand-info;
}
.underlined-link { text-decoration: underline; } .underlined-link { text-decoration: underline; }
.hint { font-style: italic; color: $hint-color; } .hint { font-style: italic; color: $hint-color; }
.light { color: $common-gray; } .light { color: $common-gray; }
......
...@@ -1002,6 +1002,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu { ...@@ -1002,6 +1002,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
max-width: 250px; max-width: 250px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap;
} }
&:hover { &:hover {
......
...@@ -34,8 +34,15 @@ ...@@ -34,8 +34,15 @@
} }
} }
.flash-success {
@extend .alert;
@extend .alert-success;
margin: 0;
}
.flash-notice, .flash-notice,
.flash-alert { .flash-alert,
.flash-success {
border-radius: $border-radius-default; border-radius: $border-radius-default;
.container-fluid, .container-fluid,
...@@ -48,7 +55,8 @@ ...@@ -48,7 +55,8 @@
margin-bottom: 0; margin-bottom: 0;
.flash-notice, .flash-notice,
.flash-alert { .flash-alert,
.flash-success {
border-radius: 0; border-radius: 0;
} }
} }
......
.ci-status-icon-success, .ci-status-icon-success,
.ci-status-icon-passed { .ci-status-icon-passed {
color: $green-500; svg {
fill: $green-500;
}
} }
.ci-status-icon-failed { .ci-status-icon-failed {
color: $gl-danger; svg {
fill: $gl-danger;
}
} }
.ci-status-icon-pending, .ci-status-icon-pending,
.ci-status-icon-failed_with_warnings, .ci-status-icon-failed_with_warnings,
.ci-status-icon-success_with_warnings { .ci-status-icon-success_with_warnings {
color: $orange-500; svg {
fill: $orange-500;
}
} }
.ci-status-icon-running { .ci-status-icon-running {
color: $blue-400; svg {
fill: $blue-400;
}
} }
.ci-status-icon-canceled, .ci-status-icon-canceled,
.ci-status-icon-disabled, .ci-status-icon-disabled,
.ci-status-icon-not-found { .ci-status-icon-not-found {
color: $gl-text-color; svg {
fill: $gl-text-color;
}
} }
.ci-status-icon-created, .ci-status-icon-created,
.ci-status-icon-skipped { .ci-status-icon-skipped {
color: $gray-darkest; svg {
fill: $gray-darkest;
}
} }
.ci-status-icon-manual { .ci-status-icon-manual {
color: $gl-text-color; svg {
fill: $gl-text-color;
}
} }
.icon-link { .icon-link {
......
...@@ -449,6 +449,12 @@ ul.indent-list { ...@@ -449,6 +449,12 @@ ul.indent-list {
} }
} }
.namespace-title {
.tooltip-inner {
max-width: 350px;
}
}
ul.group-list-tree { ul.group-list-tree {
li.group-row { li.group-row {
&.has-description { &.has-description {
......
...@@ -134,19 +134,22 @@ ...@@ -134,19 +134,22 @@
} }
.select2-search { .select2-search {
padding: 15px 15px 5px; padding: $grid-size;
.select2-drop-auto-width & { .select2-drop-auto-width & {
padding: 15px 15px 5px; padding: $grid-size;
} }
input { input {
padding: 2px 25px 2px 5px; padding: $grid-size;
background: $white-light image-url('select2.png'); background: $white-light image-url('select2.png');
background-clip: content-box;
background-origin: content-box;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right 0 bottom 6px; background-position: right 0 bottom 0 !important;
border: 1px solid $input-border; border: 1px solid $input-border;
border-radius: $border-radius-default; border-radius: $border-radius-default;
line-height: 16px;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
&:focus { &:focus {
...@@ -156,11 +159,16 @@ ...@@ -156,11 +159,16 @@
&.select2-active { &.select2-active {
background-color: $white-light; background-color: $white-light;
background-image: image-url('select2-spinner.gif') !important; background-image: image-url('select2-spinner.gif') !important;
background-origin: content-box;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right 5px center !important; background-position: right 6px center !important;
background-size: 16px 16px !important; background-size: 16px 16px !important;
} }
} }
+ .select2-results {
padding-top: 0;
}
} }
.select2-results { .select2-results {
......
...@@ -43,11 +43,13 @@ ...@@ -43,11 +43,13 @@
} }
.sidebar-collapsed-icon { .sidebar-collapsed-icon {
cursor: pointer;
.btn { .btn {
background-color: $gray-light; background-color: $gray-light;
} }
&:not(.disabled) {
cursor: pointer;
}
} }
} }
...@@ -55,6 +57,10 @@ ...@@ -55,6 +57,10 @@
padding-right: 0; padding-right: 0;
z-index: 300; z-index: 300;
.btn-sidebar-action {
display: inline-flex;
}
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper { &:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width; padding-right: $gutter_collapsed_width;
...@@ -136,3 +142,18 @@ ...@@ -136,3 +142,18 @@
.issuable-sidebar { .issuable-sidebar {
@include new-style-dropdown; @include new-style-dropdown;
} }
.pikaday-container {
.pika-single {
margin-top: 2px;
width: 250px;
}
.dropdown-menu-toggle {
line-height: 20px;
}
}
.sidebar-collapsed-icon .sidebar-collapsed-value {
font-size: 12px;
}
...@@ -195,33 +195,6 @@ summary { ...@@ -195,33 +195,6 @@ summary {
} }
} }
// Typography =================================================================
.text-primary,
.text-primary:hover {
color: $brand-primary;
}
.text-success,
.text-success:hover {
color: $brand-success;
}
.text-danger,
.text-danger:hover {
color: $brand-danger;
}
.text-warning,
.text-warning:hover {
color: $brand-warning;
}
.text-info,
.text-info:hover {
color: $brand-info;
}
// Prevent datetimes on tooltips to break into two lines // Prevent datetimes on tooltips to break into two lines
.local-timeago { .local-timeago {
white-space: nowrap; white-space: nowrap;
......
...@@ -70,14 +70,13 @@ ...@@ -70,14 +70,13 @@
.title { .title {
padding: 0; padding: 0;
margin-bottom: 16px; margin-bottom: $gl-padding;
border-bottom: 0; border-bottom: 0;
} }
.btn-edit { .btn-edit {
margin-left: auto; margin-left: auto;
// Set height to match title height height: $gl-padding * 2;
height: 2em;
} }
// Border around images in issue and MR descriptions. // Border around images in issue and MR descriptions.
...@@ -276,10 +275,15 @@ ...@@ -276,10 +275,15 @@
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
} }
.no-value { .no-value,
.btn-secondary-hover-link {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
} }
.btn-secondary-hover-link:hover {
color: $gl-link-color;
}
.sidebar-collapsed-icon { .sidebar-collapsed-icon {
display: none; display: none;
} }
...@@ -287,6 +291,8 @@ ...@@ -287,6 +291,8 @@
.gutter-toggle { .gutter-toggle {
margin-top: 7px; margin-top: 7px;
border-left: 1px solid $border-gray-normal; border-left: 1px solid $border-gray-normal;
padding-left: 0;
text-align: center;
} }
.title .gutter-toggle { .title .gutter-toggle {
...@@ -359,7 +365,7 @@ ...@@ -359,7 +365,7 @@
fill: $issuable-sidebar-color; fill: $issuable-sidebar-color;
} }
&:hover, &:hover:not(.disabled),
&:hover .todo-undone { &:hover .todo-undone {
color: $gl-text-color; color: $gl-text-color;
...@@ -900,3 +906,21 @@ ...@@ -900,3 +906,21 @@
margin: 0 3px; margin: 0 3px;
} }
} }
.right-sidebar-collapsed {
.sidebar-grouped-item {
.sidebar-collapsed-icon {
margin-bottom: 0;
}
.sidebar-collapsed-divider {
line-height: 5px;
font-size: 12px;
color: $theme-gray-700;
+ .sidebar-collapsed-icon {
padding-top: 0;
}
}
}
}
...@@ -203,7 +203,24 @@ ul.related-merge-requests > li { ...@@ -203,7 +203,24 @@ ul.related-merge-requests > li {
} }
.create-mr-dropdown-wrap { .create-mr-dropdown-wrap {
@include new-style-dropdown; .branch-message,
.ref-message {
display: none;
}
.ref::selection {
color: $placeholder-text-color;
}
.dropdown {
.dropdown-menu-toggle {
min-width: 285px;
}
.dropdown-select {
width: 285px;
}
}
.btn-group:not(.hide) { .btn-group:not(.hide) {
display: flex; display: flex;
...@@ -214,15 +231,16 @@ ul.related-merge-requests > li { ...@@ -214,15 +231,16 @@ ul.related-merge-requests > li {
flex-shrink: 0; flex-shrink: 0;
} }
.dropdown-menu { .create-merge-request-dropdown-menu {
width: 300px; width: 300px;
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
transform: translateY(0); transform: translateY(0);
display: none; display: none;
margin-top: 4px;
} }
.dropdown-toggle { .create-merge-request-dropdown-toggle {
.fa-caret-down { .fa-caret-down {
pointer-events: none; pointer-events: none;
color: inherit; color: inherit;
...@@ -230,18 +248,50 @@ ul.related-merge-requests > li { ...@@ -230,18 +248,50 @@ ul.related-merge-requests > li {
} }
} }
.droplab-item-ignore {
pointer-events: auto;
}
.create-item {
cursor: pointer;
margin: 0 1px;
&:hover,
&:focus {
background-color: $dropdown-item-hover-bg;
color: $gl-text-color;
}
}
li.divider {
margin: 8px 10px;
}
li:not(.divider) { li:not(.divider) {
padding: 8px 9px;
&:last-child {
padding-bottom: 8px;
}
&.droplab-item-selected { &.droplab-item-selected {
.icon-container { .icon-container {
i { i {
visibility: visible; visibility: visible;
} }
} }
.description {
display: block;
}
}
&.droplab-item-ignore {
padding-top: 8px;
} }
.icon-container { .icon-container {
float: left; float: left;
padding-left: 6px;
i { i {
visibility: hidden; visibility: hidden;
...@@ -249,13 +299,12 @@ ul.related-merge-requests > li { ...@@ -249,13 +299,12 @@ ul.related-merge-requests > li {
} }
.description { .description {
padding-left: 30px; padding-left: 22px;
font-size: 13px; }
strong { input,
display: block; span {
font-weight: $gl-font-weight-bold; margin: 4px 0 0;
}
} }
} }
} }
......
...@@ -252,6 +252,10 @@ ...@@ -252,6 +252,10 @@
background: $white-light; background: $white-light;
} }
.login-page-broadcast {
margin-top: 50px;
}
.navless-container { .navless-container {
padding: 65px 15px; // height of footer + bottom padding of email confirmation link padding: 65px 15px; // height of footer + bottom padding of email confirmation link
......
...@@ -543,10 +543,7 @@ ul.notes { ...@@ -543,10 +543,7 @@ ul.notes {
} }
svg { svg {
height: 16px; @include btn-svg;
width: 16px;
top: 0;
vertical-align: text-top;
} }
.award-control-icon-positive, .award-control-icon-positive,
...@@ -780,12 +777,6 @@ ul.notes { ...@@ -780,12 +777,6 @@ ul.notes {
} }
} }
svg {
fill: currentColor;
height: 16px;
width: 16px;
}
.loading { .loading {
margin: 0; margin: 0;
height: auto; height: auto;
......
...@@ -204,14 +204,7 @@ ...@@ -204,14 +204,7 @@
} }
svg { svg {
fill: $layout-link-gray;
path {
fill: $layout-link-gray;
}
use {
stroke: $layout-link-gray;
}
} }
.fa-caret-down { .fa-caret-down {
...@@ -315,6 +308,18 @@ ...@@ -315,6 +308,18 @@
} }
} }
} }
.clone-dropdown-btn {
background-color: $white-light;
}
.clone-options-dropdown {
min-width: 240px;
.dropdown-menu-inner-content {
min-width: 320px;
}
}
} }
.project-repo-buttons { .project-repo-buttons {
...@@ -799,10 +804,6 @@ pre.light-well { ...@@ -799,10 +804,6 @@ pre.light-well {
font-size: $gl-font-size; font-size: $gl-font-size;
} }
a {
color: $gl-text-color;
}
.avatar-container, .avatar-container,
.controls { .controls {
flex: 0 0 auto; flex: 0 0 auto;
......
...@@ -35,270 +35,276 @@ ...@@ -35,270 +35,276 @@
} }
} }
.repository-view { .multi-file {
border: 1px solid $border-color; display: flex;
border-radius: $border-radius-default; height: calc(100vh - 145px);
color: $almost-black; border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
&.is-collapsed {
.ide-file-list {
max-width: 250px;
}
}
}
.code.white pre .hll { .ide-file-list {
background-color: $well-light-border !important; flex: 1;
overflow: scroll;
.file {
cursor: pointer;
} }
.tree-content-holder { a {
display: -webkit-flex; color: $gl-text-color;
display: flex;
min-height: 300px;
} }
.tree-content-holder-mini { th {
height: 100vh; position: sticky;
top: 0;
} }
}
.panel-right { .multi-file-table-name,
display: -webkit-flex; .multi-file-table-col-commit-message {
display: flex; white-space: nowrap;
-webkit-flex-direction: column; overflow: hidden;
flex-direction: column; text-overflow: ellipsis;
width: 80%; max-width: 0;
height: 100%; }
.monaco-editor.vs { .multi-file-table-name {
.current-line { width: 350px;
border: 0; }
background: $well-light-border;
}
.line-numbers { .multi-file-table-col-commit-message {
cursor: pointer; width: 50%;
}
&:hover { .multi-file-edit-pane {
text-decoration: underline; display: flex;
} flex-direction: column;
} flex: 1;
} border-left: 1px solid $white-dark;
overflow: hidden;
}
.blob-no-preview { .multi-file-tabs {
.vertical-center { display: flex;
justify-content: center; overflow: scroll;
width: 100%; background-color: $white-normal;
} box-shadow: inset 0 -1px $white-dark;
}
&.blob-editor-container { > li {
overflow: hidden; position: relative;
} }
}
.blob-viewer-container { .multi-file-tab {
-webkit-flex: 1; @include str-truncated(150px);
flex: 1; padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
overflow: auto; background-color: $gray-normal;
border-right: 1px solid $white-dark;
> div, border-bottom: 1px solid $white-dark;
.file-content:not(.wiki) { cursor: pointer;
display: flex;
} &.active {
background-color: $white-light;
> div, border-bottom-color: $white-light;
.file-content, }
.blob-viewer, }
.line-number,
.blob-content,
.code {
min-height: 100%;
width: 100%;
}
.line-numbers {
min-width: 44px;
}
.blob-content {
flex: 1;
overflow-x: auto;
}
}
#tabs { .multi-file-tab-close {
position: relative; position: absolute;
flex-shrink: 0; right: 8px;
display: flex; top: 50%;
width: 100%; padding: 0;
padding-left: 0; background: none;
margin-bottom: 0; border: 0;
white-space: nowrap; font-size: $gl-font-size;
overflow-y: hidden; color: $gray-darkest;
overflow-x: auto; transform: translateY(-50%);
li { &:not(.modified):hover,
position: relative; &:not(.modified):focus {
background: $gray-normal; color: $hint-color;
padding: #{$gl-padding / 2} $gl-padding; }
border-right: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
cursor: pointer;
&.active {
background: $white-light;
border-bottom: 0;
}
a {
@include str-truncated(100px);
color: $gl-text-color;
vertical-align: middle;
text-decoration: none;
margin-right: 12px;
&:focus {
outline: none;
}
}
.close-btn {
position: absolute;
right: 8px;
top: 50%;
padding: 0;
background: none;
border: 0;
font-size: $gl-font-size;
transform: translateY(-50%);
}
.close-icon:hover {
color: $hint-color;
}
.close-icon,
.unsaved-icon {
color: $gray-darkest;
}
.unsaved-icon {
color: $brand-success;
}
&.tabs-divider {
width: 100%;
background-color: $white-light;
border-right: 0;
border-top-right-radius: 2px;
}
}
}
.repo-file-buttons { &.modified {
background-color: $white-light; color: $indigo-700;
padding: 5px 10px; }
border-top: 1px solid $white-normal; }
}
#binary-viewer { .multi-file-edit-pane-content {
height: 80vh; flex: 1;
overflow: auto; height: 0;
margin: 0; }
.blob-viewer { .multi-file-editor-btn-group {
padding-top: 20px; padding: $grid-size;
padding-left: 20px; border-top: 1px solid $white-dark;
} }
.binary-unknown { // Not great, but this is to deal with our current output
text-align: center; .multi-file-preview-holder {
padding-top: 100px; height: 100%;
background: $gray-light; overflow: scroll;
height: 100%;
font-size: 17px; .blob-viewer {
height: 100%;
span {
display: block;
}
}
}
} }
#commit-area { .file-content.code {
background: $gray-light; display: flex;
padding: 20px;
.help-block { i {
padding-top: 7px; margin-left: -10px;
margin-top: 0;
} }
} }
#view-toggler { .line-numbers {
height: 41px; min-width: 50px;
position: relative;
display: block;
border-bottom: 1px solid $white-normal;
background: $white-light;
margin-top: -5px;
} }
#binary-viewer { .file-content,
img { .line-numbers,
max-width: 100%; .blob-content,
} .code {
min-height: 100%;
} }
}
#sidebar { .multi-file-commit-panel {
flex: 1; display: flex;
height: 100%; flex-direction: column;
height: 100%;
width: 290px;
padding: $gl-padding;
background-color: $gray-light;
border-left: 1px solid $white-dark;
&.is-collapsed {
width: 60px;
padding: 0;
}
}
&.sidebar-mini { .multi-file-commit-panel-section {
width: 20%; display: flex;
border-right: 1px solid $white-normal; flex-direction: column;
overflow: auto; flex: 1;
} }
.table { .multi-file-commit-panel-header {
margin-bottom: 0; display: flex;
} align-items: center;
padding: 0 0 12px;
margin-bottom: 12px;
border-bottom: 1px solid $white-dark;
tr { &.is-collapsed {
.repo-file-options { border-bottom: 1px solid $white-dark;
padding: 2px 16px;
width: 100%;
}
.title {
font-size: 10px;
text-transform: uppercase;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
}
.file-icon {
margin-right: 5px;
}
td {
white-space: nowrap;
}
}
.file { svg {
cursor: pointer; margin-left: auto;
margin-right: auto;
} }
}
}
a { .multi-file-commit-panel-collapse-btn {
@include str-truncated(250px); padding-top: 0;
color: $almost-black; padding-bottom: 0;
} margin-left: auto;
font-size: 20px;
&.is-collapsed {
margin-right: auto;
}
}
.multi-file-commit-list {
flex: 1;
overflow: scroll;
}
.multi-file-commit-list-item {
display: flex;
align-items: center;
}
.multi-file-addition {
fill: $green-500;
}
.multi-file-modified {
fill: $orange-500;
}
.multi-file-commit-list-collapsed {
display: flex;
flex-direction: column;
> svg {
margin-left: auto;
margin-right: auto;
} }
} }
.render-error { .multi-file-commit-list-path {
min-height: calc(100vh - 62px); @include str-truncated(100%);
}
.multi-file-commit-form {
padding-top: 12px;
border-top: 1px solid $white-dark;
}
.multi-file-commit-fieldset {
display: flex;
align-items: center;
padding-bottom: 12px;
p { .btn {
width: 100%; flex: 1;
} }
} }
.multi-file-table-col-name { .multi-file-commit-message.form-control {
width: 350px; height: 80px;
resize: none;
}
.dirty-diff {
// !important need to override monaco inline style
width: 4px !important;
left: 0 !important;
&-modified {
background-color: $blue-500;
}
&-added {
background-color: $green-600;
}
&-removed {
height: 0 !important;
width: 0 !important;
bottom: -2px;
border-style: solid;
border-width: 5px;
border-color: transparent transparent transparent $red-500;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100px;
height: 1px;
background-color: rgba($red-500, .5);
}
}
} }
...@@ -54,7 +54,7 @@ module IssuableActions ...@@ -54,7 +54,7 @@ module IssuableActions
end end
def destroy def destroy
issuable.destroy Issuable::DestroyService.new(issuable.project, current_user).execute(issuable)
TodoService.new.destroy_issuable(issuable, current_user) TodoService.new.destroy_issuable(issuable, current_user)
name = issuable.human_class_name name = issuable.human_class_name
......
...@@ -41,7 +41,7 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -41,7 +41,7 @@ class Projects::BranchesController < Projects::ApplicationController
branch_name = sanitize(strip_tags(params[:branch_name])) branch_name = sanitize(strip_tags(params[:branch_name]))
branch_name = Addressable::URI.unescape(branch_name) branch_name = Addressable::URI.unescape(branch_name)
redirect_to_autodeploy = project.empty_repo? && project.deployment_services.present? redirect_to_autodeploy = project.empty_repo? && project.deployment_platform.present?
result = CreateBranchService.new(project, current_user) result = CreateBranchService.new(project, current_user)
.execute(branch_name, ref) .execute(branch_name, ref)
......
...@@ -45,8 +45,7 @@ class Projects::CommitsController < Projects::ApplicationController ...@@ -45,8 +45,7 @@ class Projects::CommitsController < Projects::ApplicationController
private private
def set_commits def set_commits
render_404 unless request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present? render_404 unless @path.empty? || request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present?
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i @limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
search = params[:search] search = params[:search]
......
...@@ -158,7 +158,8 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -158,7 +158,8 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def create_merge_request def create_merge_request
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute create_params = params.slice(:branch_name, :ref).merge(issue_iid: issue.iid)
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, create_params).execute
if result[:status] == :success if result[:status] == :success
render json: MergeRequestCreateSerializer.new.represent(result[:merge_request]) render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])
......
...@@ -65,7 +65,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap ...@@ -65,7 +65,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
if params[:ref].present? if params[:ref].present?
@ref = params[:ref] @ref = params[:ref]
@commit = @repository.commit("refs/heads/#{@ref}") @commit = @repository.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref)
end end
render layout: false render layout: false
...@@ -76,7 +76,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap ...@@ -76,7 +76,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
if params[:ref].present? if params[:ref].present?
@ref = params[:ref] @ref = params[:ref]
@commit = @target_project.commit("refs/heads/#{@ref}") @commit = @target_project.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref)
end end
render layout: false render layout: false
......
...@@ -27,7 +27,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic ...@@ -27,7 +27,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
@merge_request.merge_request_diff @merge_request.merge_request_diff
end end
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff.order_id_desc @merge_request_diffs = @merge_request.merge_request_diffs.viewable.order_id_desc
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present? if params[:start_sha].present?
......
...@@ -6,11 +6,19 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController ...@@ -6,11 +6,19 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
end end
def update def update
if @project.update(update_params) Projects::UpdateService.new(project, current_user, update_params).tap do |service|
flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated." if service.execute
redirect_to project_settings_ci_cd_path(@project) flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
else
render 'show' if service.run_auto_devops_pipeline?
CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false)
flash[:success] = "A new Auto DevOps pipeline has been created, go to <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details".html_safe
end
redirect_to project_settings_ci_cd_path(@project)
else
render 'show'
end
end end
end end
...@@ -21,6 +29,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController ...@@ -21,6 +29,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
:runners_token, :builds_enabled, :build_allow_git_fetch, :runners_token, :builds_enabled, :build_allow_git_fetch,
:build_timeout_in_minutes, :build_coverage_regex, :public_builds, :build_timeout_in_minutes, :build_coverage_regex, :public_builds,
:auto_cancel_pending_pipelines, :ci_config_path, :auto_cancel_pending_pipelines, :ci_config_path,
:run_auto_devops_pipeline_implicit, :run_auto_devops_pipeline_explicit,
auto_devops_attributes: [:id, :domain, :enabled] auto_devops_attributes: [:id, :domain, :enabled]
) )
end end
......
...@@ -104,8 +104,7 @@ class NotesFinder ...@@ -104,8 +104,7 @@ class NotesFinder
query = @params[:search] query = @params[:search]
return notes unless query return notes unless query
pattern = "%#{query}%" notes.search(query)
notes.where(Note.arel_table[:note].matches(pattern))
end end
# Notes changed since last fetch # Notes changed since last fetch
......
class RunnerJobsFinder
attr_reader :runner, :params
def initialize(runner, params = {})
@runner = runner
@params = params
end
def execute
items = @runner.builds
items = by_status(items)
items
end
private
def by_status(items)
return items unless HasStatus::AVAILABLE_STATUSES.include?(params[:status])
items.where(status: params[:status])
end
end
...@@ -30,9 +30,9 @@ module ApplicationSettingsHelper ...@@ -30,9 +30,9 @@ module ApplicationSettingsHelper
def enabled_project_button(project, protocol) def enabled_project_button(project, protocol)
case protocol case protocol
when 'ssh' when 'ssh'
ssh_clone_button(project, 'bottom', append_link: false) ssh_clone_button(project, append_link: false)
else else
http_clone_button(project, 'bottom', append_link: false) http_clone_button(project, append_link: false)
end end
end end
...@@ -177,6 +177,9 @@ module ApplicationSettingsHelper ...@@ -177,6 +177,9 @@ module ApplicationSettingsHelper
:ed25519_key_restriction, :ed25519_key_restriction,
:email_author_in_body, :email_author_in_body,
:enabled_git_access_protocol, :enabled_git_access_protocol,
:gitaly_timeout_default,
:gitaly_timeout_medium,
:gitaly_timeout_fast,
:gravatar_enabled, :gravatar_enabled,
:hashed_storage_enabled, :hashed_storage_enabled,
:help_page_hide_commercial_content, :help_page_hide_commercial_content,
......
...@@ -8,9 +8,25 @@ module AutoDevopsHelper ...@@ -8,9 +8,25 @@ module AutoDevopsHelper
!project.ci_service !project.ci_service
end end
def show_run_auto_devops_pipeline_checkbox_for_instance_setting?(project)
return false if project.repository.gitlab_ci_yml
if project&.auto_devops&.enabled.present?
!project.auto_devops.enabled && current_application_settings.auto_devops_enabled?
else
current_application_settings.auto_devops_enabled?
end
end
def show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(project)
return false if project.repository.gitlab_ci_yml
!project.auto_devops_enabled?
end
def auto_devops_warning_message(project) def auto_devops_warning_message(project)
missing_domain = !project.auto_devops&.has_domain? missing_domain = !project.auto_devops&.has_domain?
missing_service = !project.kubernetes_service&.active? missing_service = !project.deployment_platform&.active?
if missing_service if missing_service
params = { params = {
......
...@@ -56,42 +56,36 @@ module ButtonHelper ...@@ -56,42 +56,36 @@ module ButtonHelper
end end
end end
def http_clone_button(project, placement = 'right', append_link: true) def http_clone_button(project, append_link: true)
klass = 'http-selector'
klass << ' has-tooltip' if current_user.try(:require_extra_setup_for_git_auth?)
protocol = gitlab_config.protocol.upcase protocol = gitlab_config.protocol.upcase
dropdown_description = http_dropdown_description(protocol)
append_url = project.http_url_to_repo if append_link
dropdown_item_with_description(protocol, dropdown_description, href: append_url)
end
def http_dropdown_description(protocol)
if current_user.try(:require_password_creation_for_git?)
_("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol }
else
_("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol }
end
end
tooltip_title = def ssh_clone_button(project, append_link: true)
if current_user.try(:require_password_creation_for_git?) dropdown_description = _("You won't be able to pull or push project code via SSH until you add an SSH key to your profile") if current_user.try(:require_ssh_key?)
_("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol } append_url = project.ssh_url_to_repo if append_link
else
_("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol }
end
content_tag (append_link ? :a : :span), protocol, dropdown_item_with_description('SSH', dropdown_description, href: append_url)
class: klass,
href: (project.http_url_to_repo if append_link),
data: {
html: true,
placement: placement,
container: 'body',
title: tooltip_title
}
end end
def ssh_clone_button(project, placement = 'right', append_link: true) def dropdown_item_with_description(title, description, href: nil)
klass = 'ssh-selector' button_content = content_tag(:strong, title, class: 'dropdown-menu-inner-title')
klass << ' has-tooltip' if current_user.try(:require_ssh_key?) button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description
content_tag (append_link ? :a : :span), 'SSH', content_tag (href ? :a : :span),
class: klass, button_content,
href: (project.ssh_url_to_repo if append_link), class: "#{title.downcase}-selector",
data: { href: (href if href)
html: true,
placement: placement,
container: 'body',
title: _('Add an SSH key to your profile to pull or push via SSH.')
}
end end
end end
...@@ -212,6 +212,7 @@ module IssuablesHelper ...@@ -212,6 +212,7 @@ module IssuablesHelper
def issuable_initial_data(issuable) def issuable_initial_data(issuable)
data = { data = {
endpoint: issuable_path(issuable), endpoint: issuable_path(issuable),
updateEndpoint: "#{issuable_path(issuable)}.json",
canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable), canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable),
canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable), canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable),
issuableRef: issuable.to_reference, issuableRef: issuable.to_reference,
......
...@@ -172,6 +172,27 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -172,6 +172,27 @@ class ApplicationSetting < ActiveRecord::Base
end end
end end
validates :gitaly_timeout_default,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :gitaly_timeout_medium,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :gitaly_timeout_medium,
numericality: { less_than_or_equal_to: :gitaly_timeout_default },
if: :gitaly_timeout_default
validates :gitaly_timeout_medium,
numericality: { greater_than_or_equal_to: :gitaly_timeout_fast },
if: :gitaly_timeout_fast
validates :gitaly_timeout_fast,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :gitaly_timeout_fast,
numericality: { less_than_or_equal_to: :gitaly_timeout_default },
if: :gitaly_timeout_default
SUPPORTED_KEY_TYPES.each do |type| SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type } validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end end
...@@ -308,7 +329,10 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -308,7 +329,10 @@ class ApplicationSetting < ActiveRecord::Base
two_factor_grace_period: 48, two_factor_grace_period: 48,
user_default_external: false, user_default_external: false,
polling_interval_multiplier: 1, polling_interval_multiplier: 1,
usage_ping_enabled: Settings.gitlab['usage_ping_enabled'] usage_ping_enabled: Settings.gitlab['usage_ping_enabled'],
gitaly_timeout_fast: 10,
gitaly_timeout_medium: 30,
gitaly_timeout_default: 55
} }
end end
......
...@@ -104,6 +104,7 @@ module Ci ...@@ -104,6 +104,7 @@ module Ci
end end
before_transition any => [:failed] do |build| before_transition any => [:failed] do |build|
next unless build.project
next if build.retries_max.zero? next if build.retries_max.zero?
if build.retries_count < build.retries_max if build.retries_count < build.retries_max
......
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.
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.
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