Commit bdc84d4f authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into ide-file-finder

parents 79b49816 c212908a
...@@ -75,7 +75,7 @@ stages: ...@@ -75,7 +75,7 @@ stages:
.use-mysql: &use-mysql .use-mysql: &use-mysql
services: services:
- mysql:latest - mysql:5.7
- redis:alpine - redis:alpine
.rails5-variables: &rails5-variables .rails5-variables: &rails5-variables
......
This diff is collapsed.
...@@ -126,7 +126,7 @@ Most issues will have labels for at least one of the following: ...@@ -126,7 +126,7 @@ Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, etc. - Type: ~"feature proposal", ~bug, ~customer, etc.
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc. - Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc. - Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.
- Priority: ~Deliverable, ~Stretch, ~"Next Patch Release" - Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release"
All labels, their meaning and priority are defined on the All labels, their meaning and priority are defined on the
[labels page][labels-page]. [labels page][labels-page].
...@@ -185,10 +185,10 @@ indicate if an issue needs backend work, frontend work, or both. ...@@ -185,10 +185,10 @@ indicate if an issue needs backend work, frontend work, or both.
Team labels are always capitalized so that they show up as the first label for Team labels are always capitalized so that they show up as the first label for
any issue. any issue.
### Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release") ### Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")
Priority labels help us clearly communicate expectations of the work for the Milestone labels help us clearly communicate expectations of the work for the
release. There are two levels of priority labels: release. There are three levels of Milestone labels:
- ~Deliverable: Issues that are expected to be delivered in the current - ~Deliverable: Issues that are expected to be delivered in the current
milestone. milestone.
...@@ -203,16 +203,46 @@ Each issue scheduled for the current milestone should be labeled ~Deliverable ...@@ -203,16 +203,46 @@ Each issue scheduled for the current milestone should be labeled ~Deliverable
or ~"Stretch". Any open issue for a previous milestone should be labeled or ~"Stretch". Any open issue for a previous milestone should be labeled
~"Next Patch Release", or otherwise rescheduled to a different milestone. ~"Next Patch Release", or otherwise rescheduled to a different milestone.
### Severity labels (~S1, ~S2, etc.) ### Bug Priority labels (~P1, ~P2, ~P3 & etc.)
Severity labels help us clearly communicate the impact of a ~bug on users. Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be. If there are multiple defects, the priority decides which defect has to be fixed immediately versus later.
This label documents the planned timeline & urgency which is used to measure against our actual SLA on delivering ~bug fixes.
| Label | Meaning | Example | | Label | Meaning | Estimate time to fix | Guidance |
|-------|------------------------------------------|---------| |-------|-----------------|------------------------------------------------------------------|----------|
| ~S1 | Feature broken, no workaround | Unable to create an issue | | ~P1 | Urgent Priority | The current release | |
| ~S2 | Feature broken, workaround unacceptable | Can push commits, but only via the command line | | ~P2 | High Priority | The next release | |
| ~S3 | Feature broken, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue | | ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
| ~S4 | Cosmetic issue | Label colors are incorrect / not being displayed | | ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
#### Specific Priority guidance
| Label | Availability / Performance |
|-------|--------------------------------------------------------------|
| ~P1 | |
| ~P2 | The issue is (almost) guaranteed to occur in the near future |
| ~P3 | The issue is likely to occur in the near future |
| ~P4 | The issue _may_ occur but it's not likely |
### Bug Severity labels (~S1, ~S2, ~S3 & etc.)
Severity labels help us clearly communicate the impact of a ~bug on users.
| Label | Meaning | Impact of the defect | Example |
|-------|-------------------|-------------------------------------------------------|---------|
| ~S1 | Blocker | Outage, broken feature with no workaround | Unable to create an issue. Data corruption/loss. Security breach. |
| ~S2 | Critical Severity | Broken Feature, workaround too complex & unacceptable | Can push commits, but only via the command line. |
| ~S3 | Major Severity | Broken Feature, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue. |
| ~S4 | Low Severity | Functionality inconvenience or cosmetic issue | Label colors are incorrect / not being displayed. |
#### Specific Severity guidance
| Label | Security Impact |
|-------|-------------------------------------------------------------------|
| ~S1 | >50% customers impacted (possible company extinction level event) |
| ~S2 | Multiple customers impacted (but not apocalyptic) |
| ~S3 | A single customer impacted |
| ~S4 | No customer impact, or expected impact within 30 days |
### Label for community contributors (~"Accepting Merge Requests") ### Label for community contributors (~"Accepting Merge Requests")
......
Copyright (c) 2011-2017 GitLab B.V. Copyright GitLab B.V.
With regard to the GitLab Software: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
all copies or substantial portions of the Software. \ No newline at end of file
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
For all third party components incorporated into the GitLab Software, those
components are licensed under the original license provided by the owner of the
applicable component.
\ No newline at end of file
...@@ -67,6 +67,12 @@ You can access a new installation with the login **`root`** and password **`5ive ...@@ -67,6 +67,12 @@ You can access a new installation with the login **`root`** and password **`5ive
GitLab is an open source project and we are very happy to accept community contributions. Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details. GitLab is an open source project and we are very happy to accept community contributions. Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details.
## Licensing
GitLab Community Edition (CE) is available freely under the MIT Expat license.
All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component.
## Install a development environment ## Install a development environment
To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit). To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit).
......
...@@ -113,6 +113,8 @@ class List { ...@@ -113,6 +113,8 @@ class List {
issue.id = data.id; issue.id = data.id;
issue.iid = data.iid; issue.iid = data.iid;
issue.project = data.project; issue.project = data.project;
issue.path = data.real_path;
issue.referencePath = data.reference_path;
if (this.issuesSize > 1) { if (this.issuesSize > 1) {
const moveBeforeId = this.issues[1].id; const moveBeforeId = this.issues[1].id;
......
...@@ -84,20 +84,21 @@ export default class CreateMergeRequestDropdown { ...@@ -84,20 +84,21 @@ export default class CreateMergeRequestDropdown {
if (data.can_create_branch) { if (data.can_create_branch) {
this.available(); this.available();
this.enable(); this.enable();
this.updateBranchName(data.suggested_branch_name);
if (!this.droplabInitialized) { if (!this.droplabInitialized) {
this.droplabInitialized = true; this.droplabInitialized = true;
this.initDroplab(); this.initDroplab();
this.bindEvents(); this.bindEvents();
} }
} else if (data.has_related_branch) { } else {
this.hide(); this.hide();
} }
}) })
.catch(() => { .catch(() => {
this.unavailable(); this.unavailable();
this.disable(); this.disable();
Flash('Failed to check if a new branch can be created.'); Flash(__('Failed to check related branches.'));
}); });
} }
...@@ -409,13 +410,16 @@ export default class CreateMergeRequestDropdown { ...@@ -409,13 +410,16 @@ export default class CreateMergeRequestDropdown {
this.unavailableButton.classList.remove('hide'); this.unavailableButton.classList.remove('hide');
} }
updateBranchName(suggestedBranchName) {
this.branchInput.value = suggestedBranchName;
this.updateCreatePaths('branch', suggestedBranchName);
}
updateInputState(target, ref, result) { updateInputState(target, ref, result) {
// target - 'branch' or 'ref' - which the input field we are searching a ref for. // target - 'branch' or 'ref' - which the input field we are searching a ref for.
// ref - string - what a user typed. // ref - string - what a user typed.
// result - string - what has been found on backend. // result - string - what has been found on backend.
const pathReplacement = `$1${ref}`;
// If a found branch equals exact the same text a user typed, // If a found branch equals exact the same text a user typed,
// that means a new branch cannot be created as it already exists. // that means a new branch cannot be created as it already exists.
if (ref === result) { if (ref === result) {
...@@ -426,18 +430,12 @@ export default class CreateMergeRequestDropdown { ...@@ -426,18 +430,12 @@ export default class CreateMergeRequestDropdown {
this.refIsValid = true; this.refIsValid = true;
this.refInput.dataset.value = ref; this.refInput.dataset.value = ref;
this.showAvailableMessage('ref'); this.showAvailableMessage('ref');
this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath, this.updateCreatePaths(target, ref);
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath,
pathReplacement);
} }
} else if (target === 'branch') { } else if (target === 'branch') {
this.branchIsValid = true; this.branchIsValid = true;
this.showAvailableMessage('branch'); this.showAvailableMessage('branch');
this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath, this.updateCreatePaths(target, ref);
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath,
pathReplacement);
} else { } else {
this.refIsValid = false; this.refIsValid = false;
this.refInput.dataset.value = ref; this.refInput.dataset.value = ref;
...@@ -457,4 +455,15 @@ export default class CreateMergeRequestDropdown { ...@@ -457,4 +455,15 @@ export default class CreateMergeRequestDropdown {
this.disableCreateAction(); this.disableCreateAction();
} }
} }
// target - 'branch' or 'ref'
// ref - string - the new value to use as branch or ref
updateCreatePaths(target, ref) {
const pathReplacement = `$1${ref}`;
this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath,
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath,
pathReplacement);
}
} }
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
import $ from 'jquery'; import $ from 'jquery';
import Pikaday from 'pikaday'; import Pikaday from 'pikaday';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
class DueDateSelect { class DueDateSelect {
...@@ -14,6 +16,7 @@ class DueDateSelect { ...@@ -14,6 +16,7 @@ class DueDateSelect {
this.$dropdownParent = $dropdownParent; this.$dropdownParent = $dropdownParent;
this.$datePicker = $dropdownParent.find('.js-due-date-calendar'); this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
this.$block = $block; this.$block = $block;
this.$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
this.$selectbox = $dropdown.closest('.selectbox'); this.$selectbox = $dropdown.closest('.selectbox');
this.$value = $block.find('.value'); this.$value = $block.find('.value');
this.$valueContent = $block.find('.value-content'); this.$valueContent = $block.find('.value-content');
...@@ -128,7 +131,8 @@ class DueDateSelect { ...@@ -128,7 +131,8 @@ class DueDateSelect {
submitSelectedDate(isDropdown) { submitSelectedDate(isDropdown) {
const selectedDateValue = this.datePayload[this.abilityName].due_date; const selectedDateValue = this.datePayload[this.abilityName].due_date;
const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; const hasDueDate = this.displayedDate !== 'No due date';
const displayedDateStyle = hasDueDate ? 'bold' : 'no-value';
this.$loading.removeClass('hidden').fadeIn(); this.$loading.removeClass('hidden').fadeIn();
...@@ -145,10 +149,13 @@ class DueDateSelect { ...@@ -145,10 +149,13 @@ class DueDateSelect {
return axios.put(this.issueUpdateURL, this.datePayload) return axios.put(this.issueUpdateURL, this.datePayload)
.then(() => { .then(() => {
const tooltipText = hasDueDate ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})` : __('Due date');
if (isDropdown) { if (isDropdown) {
this.$dropdown.trigger('loaded.gl.dropdown'); this.$dropdown.trigger('loaded.gl.dropdown');
this.$dropdown.dropdown('toggle'); this.$dropdown.dropdown('toggle');
} }
this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
return this.$loading.fadeOut(); return this.$loading.fadeOut();
}); });
} }
......
...@@ -32,7 +32,7 @@ export default class Model { ...@@ -32,7 +32,7 @@ export default class Model {
); );
} }
this.events = new Map(); this.events = new Set();
this.updateContent = this.updateContent.bind(this); this.updateContent = this.updateContent.bind(this);
this.updateNewContent = this.updateNewContent.bind(this); this.updateNewContent = this.updateNewContent.bind(this);
...@@ -76,10 +76,11 @@ export default class Model { ...@@ -76,10 +76,11 @@ export default class Model {
} }
onChange(cb) { onChange(cb) {
this.events.set( this.events.add(this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))));
this.path, }
this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))),
); onDispose(cb) {
this.events.add(cb);
} }
updateContent({ content, changed }) { updateContent({ content, changed }) {
...@@ -96,6 +97,11 @@ export default class Model { ...@@ -96,6 +97,11 @@ export default class Model {
dispose() { dispose() {
this.disposable.dispose(); this.disposable.dispose();
this.events.forEach(cb => {
if (typeof cb === 'function') cb();
});
this.events.clear(); this.events.clear();
eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose); eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose);
......
...@@ -38,6 +38,15 @@ export default class DecorationsController { ...@@ -38,6 +38,15 @@ export default class DecorationsController {
); );
} }
hasDecorations(model) {
return this.decorations.has(model.url);
}
removeDecorations(model) {
this.decorations.delete(model.url);
this.editorDecorations.delete(model.url);
}
dispose() { dispose() {
this.decorations.clear(); this.decorations.clear();
this.editorDecorations.clear(); this.editorDecorations.clear();
......
...@@ -3,7 +3,7 @@ import { throttle } from 'underscore'; ...@@ -3,7 +3,7 @@ import { throttle } from 'underscore';
import DirtyDiffWorker from './diff_worker'; import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable'; import Disposable from '../common/disposable';
export const getDiffChangeType = (change) => { export const getDiffChangeType = change => {
if (change.modified) { if (change.modified) {
return 'modified'; return 'modified';
} else if (change.added) { } else if (change.added) {
...@@ -16,12 +16,7 @@ export const getDiffChangeType = (change) => { ...@@ -16,12 +16,7 @@ export const getDiffChangeType = (change) => {
}; };
export const getDecorator = change => ({ export const getDecorator = change => ({
range: new monaco.Range( range: new monaco.Range(change.lineNumber, 1, change.endLineNumber, 1),
change.lineNumber,
1,
change.endLineNumber,
1,
),
options: { options: {
isWholeLine: true, isWholeLine: true,
linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`, linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
...@@ -31,6 +26,7 @@ export const getDecorator = change => ({ ...@@ -31,6 +26,7 @@ export const getDecorator = change => ({
export default class DirtyDiffController { export default class DirtyDiffController {
constructor(modelManager, decorationsController) { constructor(modelManager, decorationsController) {
this.disposable = new Disposable(); this.disposable = new Disposable();
this.models = new Map();
this.editorSimpleWorker = null; this.editorSimpleWorker = null;
this.modelManager = modelManager; this.modelManager = modelManager;
this.decorationsController = decorationsController; this.decorationsController = decorationsController;
...@@ -42,7 +38,15 @@ export default class DirtyDiffController { ...@@ -42,7 +38,15 @@ export default class DirtyDiffController {
} }
attachModel(model) { attachModel(model) {
if (this.models.has(model.url)) return;
model.onChange(() => this.throttledComputeDiff(model)); model.onChange(() => this.throttledComputeDiff(model));
model.onDispose(() => {
this.decorationsController.removeDecorations(model);
this.models.delete(model.url);
});
this.models.set(model.url, model);
} }
computeDiff(model) { computeDiff(model) {
...@@ -54,7 +58,11 @@ export default class DirtyDiffController { ...@@ -54,7 +58,11 @@ export default class DirtyDiffController {
} }
reDecorate(model) { reDecorate(model) {
this.decorationsController.decorate(model); if (this.decorationsController.hasDecorations(model)) {
this.decorationsController.decorate(model);
} else {
this.computeDiff(model);
}
} }
decorate({ data }) { decorate({ data }) {
...@@ -65,6 +73,7 @@ export default class DirtyDiffController { ...@@ -65,6 +73,7 @@ export default class DirtyDiffController {
dispose() { dispose() {
this.disposable.dispose(); this.disposable.dispose();
this.models.clear();
this.dirtyDiffWorker.removeEventListener('message', this.decorate); this.dirtyDiffWorker.removeEventListener('message', this.decorate);
this.dirtyDiffWorker.terminate(); this.dirtyDiffWorker.terminate();
......
...@@ -83,7 +83,7 @@ export default class LabelsSelect { ...@@ -83,7 +83,7 @@ export default class LabelsSelect {
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
axios.put(issueUpdateURL, data) axios.put(issueUpdateURL, data)
.then(({ data }) => { .then(({ data }) => {
var labelCount, template, labelTooltipTitle, labelTitles; var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels;
$loading.fadeOut(); $loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown'); $dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide(); $selectbox.hide();
...@@ -115,8 +115,7 @@ export default class LabelsSelect { ...@@ -115,8 +115,7 @@ export default class LabelsSelect {
labelTooltipTitle = labelTitles.join(', '); labelTooltipTitle = labelTitles.join(', ');
} }
else { else {
labelTooltipTitle = ''; labelTooltipTitle = __('Labels');
$sidebarLabelTooltip.tooltip('destroy');
} }
$sidebarLabelTooltip $sidebarLabelTooltip
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility'; import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store'; import ModalStore from './boards/stores/modal_store';
...@@ -25,7 +26,7 @@ export default class MilestoneSelect { ...@@ -25,7 +26,7 @@ export default class MilestoneSelect {
} }
$els.each((i, dropdown) => { $els.each((i, dropdown) => {
let collapsedSidebarLabelTemplate, milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault; let milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
const $dropdown = $(dropdown); const $dropdown = $(dropdown);
const projectId = $dropdown.data('projectId'); const projectId = $dropdown.data('projectId');
const milestonesUrl = $dropdown.data('milestones'); const milestonesUrl = $dropdown.data('milestones');
...@@ -52,7 +53,6 @@ export default class MilestoneSelect { ...@@ -52,7 +53,6 @@ export default class MilestoneSelect {
if (issueUpdateURL) { if (issueUpdateURL) {
milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'); milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>');
} }
return $dropdown.glDropdown({ return $dropdown.glDropdown({
showMenuAbove: showMenuAbove, showMenuAbove: showMenuAbove,
...@@ -214,10 +214,16 @@ export default class MilestoneSelect { ...@@ -214,10 +214,16 @@ export default class MilestoneSelect {
data.milestone.remaining = timeFor(data.milestone.due_date); data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title; data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone)); $value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)); return $sidebarCollapsedValue
.attr('data-original-title', `${data.milestone.name}<br />${data.milestone.remaining}`)
.find('span')
.text(data.milestone.title);
} else { } else {
$value.html(milestoneLinkNoneTemplate); $value.html(milestoneLinkNoneTemplate);
return $sidebarCollapsedValue.find('span').text('No'); return $sidebarCollapsedValue
.attr('data-original-title', __('Milestone'))
.find('span')
.text(__('None'));
} }
}) })
.catch(() => { .catch(() => {
......
...@@ -1427,7 +1427,7 @@ export default class Notes { ...@@ -1427,7 +1427,7 @@ export default class Notes {
const { discussion_html } = data; const { discussion_html } = data;
const lines = $(discussion_html).find('.line_holder'); const lines = $(discussion_html).find('.line_holder');
lines.addClass('fade-in'); lines.addClass('fade-in');
$container.find('tbody').prepend(lines); $container.find('.diff-content > table > tbody').prepend(lines);
const fileHolder = $container.find('.file-holder'); const fileHolder = $container.find('.file-holder');
$container.find('.line-holder-placeholder').remove(); $container.find('.line-holder-placeholder').remove();
syntaxHighlight(fileHolder); syntaxHighlight(fileHolder);
......
...@@ -19,7 +19,7 @@ function getSystemDate(systemUtcOffsetSeconds) { ...@@ -19,7 +19,7 @@ function getSystemDate(systemUtcOffsetSeconds) {
const date = new Date(); const date = new Date();
const localUtcOffsetMinutes = 0 - date.getTimezoneOffset(); const localUtcOffsetMinutes = 0 - date.getTimezoneOffset();
const systemUtcOffsetMinutes = systemUtcOffsetSeconds / 60; const systemUtcOffsetMinutes = systemUtcOffsetSeconds / 60;
date.setMinutes((date.getMinutes() - localUtcOffsetMinutes) + systemUtcOffsetMinutes); date.setMinutes(date.getMinutes() - localUtcOffsetMinutes + systemUtcOffsetMinutes);
return date; return date;
} }
...@@ -35,18 +35,36 @@ function formatTooltipText({ date, count }) { ...@@ -35,18 +35,36 @@ function formatTooltipText({ date, count }) {
return `${contribText}<br />${dateDayName} ${dateText}`; return `${contribText}<br />${dateDayName} ${dateText}`;
} }
const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]); const initColorKey = () =>
d3
.scaleLinear()
.range(['#acd5f2', '#254e77'])
.domain([0, 3]);
export default class ActivityCalendar { export default class ActivityCalendar {
constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0) { constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0, firstDayOfWeek = 0) {
this.calendarActivitiesPath = calendarActivitiesPath; this.calendarActivitiesPath = calendarActivitiesPath;
this.clickDay = this.clickDay.bind(this); this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = ''; this.currentSelectedDate = '';
this.daySpace = 1; this.daySpace = 1;
this.daySize = 15; this.daySize = 15;
this.daySizeWithSpace = this.daySize + (this.daySpace * 2); this.daySizeWithSpace = this.daySize + this.daySpace * 2;
this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; this.monthNames = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
this.months = []; this.months = [];
this.firstDayOfWeek = firstDayOfWeek;
// Loop through the timestamps to create a group of objects // Loop through the timestamps to create a group of objects
// The group of objects will be grouped based on the day of the week they are // The group of objects will be grouped based on the day of the week they are
...@@ -70,7 +88,7 @@ export default class ActivityCalendar { ...@@ -70,7 +88,7 @@ export default class ActivityCalendar {
// Create a new group array if this is the first day of the week // Create a new group array if this is the first day of the week
// or if is first object // or if is first object
if ((day === 0 && i !== 0) || i === 0) { if ((day === this.firstDayOfWeek && i !== 0) || i === 0) {
this.timestampsTmp.push([]); this.timestampsTmp.push([]);
group += 1; group += 1;
} }
...@@ -109,21 +127,30 @@ export default class ActivityCalendar { ...@@ -109,21 +127,30 @@ export default class ActivityCalendar {
} }
renderSvg(container, group) { renderSvg(container, group) {
const width = ((group + 1) * this.daySizeWithSpace) + this.getExtraWidthPadding(group); const width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
return d3.select(container) return d3
.select(container)
.append('svg') .append('svg')
.attr('width', width) .attr('width', width)
.attr('height', 167) .attr('height', 167)
.attr('class', 'contrib-calendar'); .attr('class', 'contrib-calendar');
}
dayYPos(day) {
return this.daySizeWithSpace * ((day + 7 - this.firstDayOfWeek) % 7);
} }
renderDays() { renderDays() {
this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g') this.svg
.selectAll('g')
.data(this.timestampsTmp)
.enter()
.append('g')
.attr('transform', (group, i) => { .attr('transform', (group, i) => {
_.each(group, (stamp, a) => { _.each(group, (stamp, a) => {
if (a === 0 && stamp.day === 0) { if (a === 0 && stamp.day === 0) {
const month = stamp.date.getMonth(); const month = stamp.date.getMonth();
const x = (this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace; const x = this.daySizeWithSpace * i + 1 + this.daySizeWithSpace;
const lastMonth = _.last(this.months); const lastMonth = _.last(this.months);
if ( if (
lastMonth == null || lastMonth == null ||
...@@ -133,86 +160,113 @@ export default class ActivityCalendar { ...@@ -133,86 +160,113 @@ export default class ActivityCalendar {
} }
} }
}); });
return `translate(${(this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace}, 18)`; return `translate(${this.daySizeWithSpace * i + 1 + this.daySizeWithSpace}, 18)`;
}) })
.selectAll('rect') .selectAll('rect')
.data(stamp => stamp) .data(stamp => stamp)
.enter() .enter()
.append('rect') .append('rect')
.attr('x', '0') .attr('x', '0')
.attr('y', stamp => this.daySizeWithSpace * stamp.day) .attr('y', stamp => this.dayYPos(stamp.day))
.attr('width', this.daySize) .attr('width', this.daySize)
.attr('height', this.daySize) .attr('height', this.daySize)
.attr('fill', stamp => ( .attr(
stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed' 'fill',
)) stamp => (stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'),
.attr('title', stamp => formatTooltipText(stamp)) )
.attr('class', 'user-contrib-cell js-tooltip') .attr('title', stamp => formatTooltipText(stamp))
.attr('data-container', 'body') .attr('class', 'user-contrib-cell js-tooltip')
.on('click', this.clickDay); .attr('data-container', 'body')
.on('click', this.clickDay);
} }
renderDayTitles() { renderDayTitles() {
const days = [ const days = [
{ {
text: 'M', text: 'M',
y: 29 + (this.daySizeWithSpace * 1), y: 29 + this.dayYPos(1),
}, { },
{
text: 'W', text: 'W',
y: 29 + (this.daySizeWithSpace * 3), y: 29 + this.dayYPos(2),
}, { },
{
text: 'F', text: 'F',
y: 29 + (this.daySizeWithSpace * 5), y: 29 + this.dayYPos(3),
}, },
]; ];
this.svg.append('g') this.svg
.append('g')
.selectAll('text') .selectAll('text')
.data(days) .data(days)
.enter() .enter()
.append('text') .append('text')
.attr('text-anchor', 'middle') .attr('text-anchor', 'middle')
.attr('x', 8) .attr('x', 8)
.attr('y', day => day.y) .attr('y', day => day.y)
.text(day => day.text) .text(day => day.text)
.attr('class', 'user-contrib-text'); .attr('class', 'user-contrib-text');
} }
renderMonths() { renderMonths() {
this.svg.append('g') this.svg
.append('g')
.attr('direction', 'ltr') .attr('direction', 'ltr')
.selectAll('text') .selectAll('text')
.data(this.months) .data(this.months)
.enter() .enter()
.append('text') .append('text')
.attr('x', date => date.x) .attr('x', date => date.x)
.attr('y', 10) .attr('y', 10)
.attr('class', 'user-contrib-text') .attr('class', 'user-contrib-text')
.text(date => this.monthNames[date.month]); .text(date => this.monthNames[date.month]);
} }
renderKey() { renderKey() {
const keyValues = ['no contributions', '1-9 contributions', '10-19 contributions', '20-29 contributions', '30+ contributions']; const keyValues = [
const keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; 'no contributions',
'1-9 contributions',
'10-19 contributions',
'20-29 contributions',
'30+ contributions',
];
const keyColors = [
'#ededed',
this.colorKey(0),
this.colorKey(1),
this.colorKey(2),
this.colorKey(3),
];
this.svg.append('g') this.svg
.attr('transform', `translate(18, ${(this.daySizeWithSpace * 8) + 16})`) .append('g')
.attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
.selectAll('rect') .selectAll('rect')
.data(keyColors) .data(keyColors)
.enter() .enter()
.append('rect') .append('rect')
.attr('width', this.daySize) .attr('width', this.daySize)
.attr('height', this.daySize) .attr('height', this.daySize)
.attr('x', (color, i) => this.daySizeWithSpace * i) .attr('x', (color, i) => this.daySizeWithSpace * i)
.attr('y', 0) .attr('y', 0)
.attr('fill', color => color) .attr('fill', color => color)
.attr('class', 'js-tooltip') .attr('class', 'js-tooltip')
.attr('title', (color, i) => keyValues[i]) .attr('title', (color, i) => keyValues[i])
.attr('data-container', 'body'); .attr('data-container', 'body');
} }
initColor() { initColor() {
const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; const colorRange = [
return d3.scaleThreshold().domain([0, 10, 20, 30]).range(colorRange); '#ededed',
this.colorKey(0),
this.colorKey(1),
this.colorKey(2),
this.colorKey(3),
];
return d3
.scaleThreshold()
.domain([0, 10, 20, 30])
.range(colorRange);
} }
clickDay(stamp) { clickDay(stamp) {
...@@ -227,14 +281,15 @@ export default class ActivityCalendar { ...@@ -227,14 +281,15 @@ export default class ActivityCalendar {
$('.user-calendar-activities').html(LOADING_HTML); $('.user-calendar-activities').html(LOADING_HTML);
axios.get(this.calendarActivitiesPath, { axios
params: { .get(this.calendarActivitiesPath, {
date, params: {
}, date,
responseType: 'text', },
}) responseType: 'text',
.then(({ data }) => $('.user-calendar-activities').html(data)) })
.catch(() => flash(__('An error occurred while retrieving calendar activity'))); .then(({ data }) => $('.user-calendar-activities').html(data))
.catch(() => flash(__('An error occurred while retrieving calendar activity')));
} else { } else {
this.currentSelectedDate = ''; this.currentSelectedDate = '';
$('.user-calendar-activities').html(''); $('.user-calendar-activities').html('');
......
...@@ -5,6 +5,7 @@ import _ from 'underscore'; ...@@ -5,6 +5,7 @@ import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import flash from './flash'; import flash from './flash';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { __ } from './locale';
function Sidebar(currentUser) { function Sidebar(currentUser) {
this.toggleTodo = this.toggleTodo.bind(this); this.toggleTodo = this.toggleTodo.bind(this);
...@@ -41,12 +42,14 @@ Sidebar.prototype.addEventListeners = function() { ...@@ -41,12 +42,14 @@ Sidebar.prototype.addEventListeners = function() {
}; };
Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon; var $allGutterToggleIcons, $this, isExpanded, tooltipLabel;
e.preventDefault(); e.preventDefault();
$this = $(this); $this = $(this);
$thisIcon = $this.find('i'); isExpanded = $this.find('i').hasClass('fa-angle-double-right');
tooltipLabel = isExpanded ? __('Expand sidebar') : __('Collapse sidebar');
$allGutterToggleIcons = $('.js-sidebar-toggle i'); $allGutterToggleIcons = $('.js-sidebar-toggle i');
if ($thisIcon.hasClass('fa-angle-double-right')) {
if (isExpanded) {
$allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left'); $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
$('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
$('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); $('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
...@@ -57,6 +60,9 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { ...@@ -57,6 +60,9 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
if (gl.lazyLoader) gl.lazyLoader.loadCheck(); if (gl.lazyLoader) gl.lazyLoader.loadCheck();
} }
$this.attr('data-original-title', tooltipLabel);
if (!triggered) { if (!triggered) {
Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed')); Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
} }
......
<script> <script>
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
name: 'Assignees', name: 'Assignees',
directives: {
tooltip,
},
props: { props: {
rootPath: { rootPath: {
type: String, type: String,
...@@ -14,6 +20,11 @@ export default { ...@@ -14,6 +20,11 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
issuableType: {
type: String,
require: true,
default: 'issue',
},
}, },
data() { data() {
return { return {
...@@ -62,6 +73,12 @@ export default { ...@@ -62,6 +73,12 @@ export default {
names.push(`+ ${this.users.length - maxRender} more`); names.push(`+ ${this.users.length - maxRender} more`);
} }
if (!this.users.length) {
const emptyTooltipLabel = this.issuableType === 'issue' ?
__('Assignee(s)') : __('Assignee');
names.push(emptyTooltipLabel);
}
return names.join(', '); return names.join(', ');
}, },
sidebarAvatarCounter() { sidebarAvatarCounter() {
...@@ -109,7 +126,8 @@ export default { ...@@ -109,7 +126,8 @@ export default {
<div> <div>
<div <div
class="sidebar-collapsed-icon sidebar-collapsed-user" class="sidebar-collapsed-icon sidebar-collapsed-user"
:class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }" :class="{ 'multiple-users': hasMoreThanOneAssignee }"
v-tooltip
data-container="body" data-container="body"
data-placement="left" data-placement="left"
:title="collapsedTooltipTitle" :title="collapsedTooltipTitle"
......
<script> <script>
import Flash from '../../../flash'; import Flash from '~/flash';
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
import AssigneeTitle from './assignee_title.vue'; import AssigneeTitle from './assignee_title.vue';
import Assignees from './assignees.vue'; import Assignees from './assignees.vue';
import Store from '../../stores/sidebar_store';
import eventHub from '../../event_hub';
export default { export default {
name: 'SidebarAssignees', name: 'SidebarAssignees',
...@@ -25,6 +25,11 @@ export default { ...@@ -25,6 +25,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
issuableType: {
type: String,
require: true,
default: 'issue',
},
}, },
data() { data() {
return { return {
...@@ -90,6 +95,7 @@ export default { ...@@ -90,6 +95,7 @@ export default {
:users="store.assignees" :users="store.assignees"
:editable="store.editable" :editable="store.editable"
@assign-self="assignSelf" @assign-self="assignSelf"
:issuable-type="issuableType"
/> />
</div> </div>
</template> </template>
<script> <script>
import Flash from '../../../flash'; import { __ } from '~/locale';
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue'; import editForm from './edit_form.vue';
import Icon from '../../../vue_shared/components/icon.vue';
import { __ } from '../../../locale';
import eventHub from '../../event_hub';
export default { export default {
components: { components: {
editForm, editForm,
Icon, Icon,
}, },
directives: {
tooltip,
},
props: { props: {
isConfidential: { isConfidential: {
required: true, required: true,
...@@ -33,6 +37,9 @@ export default { ...@@ -33,6 +37,9 @@ export default {
confidentialityIcon() { confidentialityIcon() {
return this.isConfidential ? 'eye-slash' : 'eye'; return this.isConfidential ? 'eye-slash' : 'eye';
}, },
tooltipLabel() {
return this.isConfidential ? __('Confidential') : __('Not confidential');
},
}, },
created() { created() {
eventHub.$on('closeConfidentialityForm', this.toggleForm); eventHub.$on('closeConfidentialityForm', this.toggleForm);
...@@ -65,6 +72,10 @@ export default { ...@@ -65,6 +72,10 @@ export default {
<div <div
class="sidebar-collapsed-icon" class="sidebar-collapsed-icon"
@click="toggleForm" @click="toggleForm"
v-tooltip
data-container="body"
data-placement="left"
:title="tooltipLabel"
> >
<icon <icon
:name="confidentialityIcon" :name="confidentialityIcon"
......
<script> <script>
import { __ } from '~/locale';
import Flash from '~/flash'; import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import issuableMixin from '~/vue_shared/mixins/issuable';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue'; import editForm from './edit_form.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
import Icon from '../../../vue_shared/components/icon.vue';
import eventHub from '../../event_hub';
export default { export default {
components: { components: {
editForm, editForm,
Icon, Icon,
}, },
directives: {
tooltip,
},
mixins: [issuableMixin], mixins: [issuableMixin],
props: { props: {
...@@ -44,6 +51,10 @@ export default { ...@@ -44,6 +51,10 @@ export default {
isLockDialogOpen() { isLockDialogOpen() {
return this.mediator.store.isLockDialogOpen; return this.mediator.store.isLockDialogOpen;
}, },
tooltipLabel() {
return this.isLocked ? __('Locked') : __('Unlocked');
},
}, },
created() { created() {
...@@ -85,6 +96,10 @@ export default { ...@@ -85,6 +96,10 @@ export default {
<div <div
class="sidebar-collapsed-icon" class="sidebar-collapsed-icon"
@click="toggleForm" @click="toggleForm"
v-tooltip
data-container="body"
data-placement="left"
:title="tooltipLabel"
> >
<icon <icon
:name="lockIcon" :name="lockIcon"
......
<script> <script>
import { __, n__, sprintf } from '../../../locale'; import { __, n__, sprintf } from '~/locale';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; import tooltip from '~/vue_shared/directives/tooltip';
import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
export default { export default {
directives: {
tooltip,
},
components: { components: {
loadingIcon, loadingIcon,
userAvatarImage, userAvatarImage,
...@@ -72,7 +76,13 @@ ...@@ -72,7 +76,13 @@
<template> <template>
<div> <div>
<div class="sidebar-collapsed-icon"> <div
class="sidebar-collapsed-icon"
v-tooltip
data-container="body"
data-placement="left"
:title="participantLabel"
>
<i <i
class="fa fa-users" class="fa fa-users"
aria-hidden="true" aria-hidden="true"
......
<script> <script>
import icon from '../../../vue_shared/components/icon.vue'; import { __, sprintf } from '~/locale';
import { abbreviateTime } from '../../../lib/utils/pretty_time'; import { abbreviateTime } from '~/lib/utils/pretty_time';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
name: 'TimeTrackingCollapsedState', name: 'TimeTrackingCollapsedState',
components: { components: {
icon, icon,
}, },
directives: {
tooltip,
},
props: { props: {
showComparisonState: { showComparisonState: {
type: Boolean, type: Boolean,
...@@ -79,6 +84,21 @@ ...@@ -79,6 +84,21 @@
return ''; return '';
}, },
timeTrackedTooltipText() {
let title;
if (this.showComparisonState) {
title = __('Time remaining');
} else if (this.showEstimateOnlyState) {
title = __('Estimated');
} else if (this.showSpentOnlyState) {
title = __('Time spent');
}
return sprintf('%{title}: %{text}', ({ title, text: this.text }));
},
tooltipText() {
return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText;
},
}, },
methods: { methods: {
abbreviateTime(timeStr) { abbreviateTime(timeStr) {
...@@ -89,7 +109,13 @@ ...@@ -89,7 +109,13 @@
</script> </script>
<template> <template>
<div class="sidebar-collapsed-icon"> <div
class="sidebar-collapsed-icon"
v-tooltip
data-container="body"
data-placement="left"
:title="tooltipText"
>
<icon name="timer" /> <icon name="timer" />
<div class="time-tracking-collapsed-summary"> <div class="time-tracking-collapsed-summary">
<div :class="divClass"> <div :class="divClass">
......
...@@ -27,6 +27,7 @@ function mountAssigneesComponent(mediator) { ...@@ -27,6 +27,7 @@ function mountAssigneesComponent(mediator) {
mediator, mediator,
field: el.dataset.field, field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'), signedIn: el.hasAttribute('data-signed-in'),
issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
}, },
}), }),
}); });
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { __ } from './locale';
import ModalStore from './boards/stores/modal_store'; import ModalStore from './boards/stores/modal_store';
// TODO: remove eventHub hack after code splitting refactor // TODO: remove eventHub hack after code splitting refactor
...@@ -182,7 +183,7 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -182,7 +183,7 @@ function UsersSelect(currentUser, els, options = {}) {
return axios.put(issueURL, data) return axios.put(issueURL, data)
.then(({ data }) => { .then(({ data }) => {
var user; var user, tooltipTitle;
$dropdown.trigger('loaded.gl.dropdown'); $dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut(); $loading.fadeOut();
if (data.assignee) { if (data.assignee) {
...@@ -191,15 +192,17 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -191,15 +192,17 @@ function UsersSelect(currentUser, els, options = {}) {
username: data.assignee.username, username: data.assignee.username,
avatar: data.assignee.avatar_url avatar: data.assignee.avatar_url
}; };
tooltipTitle = _.escape(user.name);
} else { } else {
user = { user = {
name: 'Unassigned', name: 'Unassigned',
username: '', username: '',
avatar: '' avatar: ''
}; };
tooltipTitle = __('Assignee');
} }
$value.html(assigneeTemplate(user)); $value.html(assigneeTemplate(user));
$collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle'); $collapsedSidebar.attr('title', tooltipTitle).tooltip('fixTitle');
return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
}); });
}; };
......
<script> <script>
export default { import { __ } from '~/locale';
name: 'ToggleSidebar', import tooltip from '~/vue_shared/directives/tooltip';
props: {
collapsed: { export default {
type: Boolean, name: 'ToggleSidebar',
required: true, directives: {
}, tooltip,
},
props: {
collapsed: {
type: Boolean,
required: true,
},
},
computed: {
tooltipLabel() {
return this.collapsed ? __('Expand sidebar') : __('Collapse sidebar');
}, },
methods: { },
toggle() { methods: {
this.$emit('toggle'); toggle() {
}, this.$emit('toggle');
}, },
}; },
};
</script> </script>
<template> <template>
...@@ -20,6 +31,10 @@ ...@@ -20,6 +31,10 @@
type="button" type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action" class="btn btn-blank gutter-toggle btn-sidebar-action"
@click="toggle" @click="toggle"
v-tooltip
data-container="body"
data-placement="left"
:title="tooltipLabel"
> >
<i <i
aria-label="toggle collapse" aria-label="toggle collapse"
......
...@@ -247,6 +247,7 @@ $btn-sm-side-margin: 7px; ...@@ -247,6 +247,7 @@ $btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px; $btn-xs-side-margin: 5px;
$issue-status-expired: $orange-500; $issue-status-expired: $orange-500;
$issuable-sidebar-color: $gl-text-color-secondary; $issuable-sidebar-color: $gl-text-color-secondary;
$sidebar-block-hover-color: #ebebeb;
$group-path-color: #999; $group-path-color: #999;
$namespace-kind-color: #aaa; $namespace-kind-color: #aaa;
$panel-heading-link-color: #777; $panel-heading-link-color: #777;
...@@ -373,6 +374,8 @@ $dropdown-hover-color: $blue-400; ...@@ -373,6 +374,8 @@ $dropdown-hover-color: $blue-400;
$link-active-background: rgba(0, 0, 0, 0.04); $link-active-background: rgba(0, 0, 0, 0.04);
$link-hover-background: rgba(0, 0, 0, 0.06); $link-hover-background: rgba(0, 0, 0, 0.06);
$inactive-badge-background: rgba(0, 0, 0, 0.08); $inactive-badge-background: rgba(0, 0, 0, 0.08);
$sidebar-toggle-height: 60px;
$sidebar-milestone-toggle-bottom-margin: 10px;
/* /*
* Buttons * Buttons
......
...@@ -187,7 +187,12 @@ ...@@ -187,7 +187,12 @@
padding-left: 10px; padding-left: 10px;
&:hover { &:hover {
color: $gray-darkest; color: $gl-text-color;
}
&:hover,
&:focus {
text-decoration: none;
} }
} }
...@@ -368,6 +373,14 @@ ...@@ -368,6 +373,14 @@
padding: 15px 0 0; padding: 15px 0 0;
border-bottom: 0; border-bottom: 0;
overflow: hidden; overflow: hidden;
&:hover {
background-color: $sidebar-block-hover-color;
}
&.issuable-sidebar-header {
padding-top: 0;
}
} }
.participants { .participants {
...@@ -380,8 +393,17 @@ ...@@ -380,8 +393,17 @@
.gutter-toggle { .gutter-toggle {
width: 100%; width: 100%;
height: $sidebar-toggle-height;
margin-left: 0; margin-left: 0;
padding-left: 25px; padding-left: 0;
border-bottom: 1px solid $border-gray-dark;
}
a.gutter-toggle {
display: flex;
justify-content: center;
flex-direction: column;
text-align: center;
} }
.sidebar-collapsed-icon { .sidebar-collapsed-icon {
...@@ -428,10 +450,10 @@ ...@@ -428,10 +450,10 @@
.btn-clipboard { .btn-clipboard {
border: 0; border: 0;
background: transparent;
color: $issuable-sidebar-color; color: $issuable-sidebar-color;
&:hover { &:hover {
background: transparent;
color: $gl-text-color; color: $gl-text-color;
} }
} }
......
...@@ -53,10 +53,6 @@ ...@@ -53,10 +53,6 @@
} }
.milestone-sidebar { .milestone-sidebar {
.gutter-toggle {
margin-bottom: 10px;
}
.milestone-progress { .milestone-progress {
.title { .title {
padding-top: 5px; padding-top: 5px;
...@@ -102,7 +98,17 @@ ...@@ -102,7 +98,17 @@
margin-right: 0; margin-right: 0;
} }
.right-sidebar-expanded & {
.gutter-toggle {
margin-bottom: $sidebar-milestone-toggle-bottom-margin;
}
}
.right-sidebar-collapsed & { .right-sidebar-collapsed & {
.milestone-progress {
padding-top: 0;
}
.reference { .reference {
border-top: 1px solid $border-gray-normal; border-top: 1px solid $border-gray-normal;
} }
......
...@@ -134,11 +134,11 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -134,11 +134,11 @@ class Projects::IssuesController < Projects::ApplicationController
def can_create_branch def can_create_branch
can_create = current_user && can_create = current_user &&
can?(current_user, :push_code, @project) && can?(current_user, :push_code, @project) &&
@issue.can_be_worked_on?(current_user) @issue.can_be_worked_on?
respond_to do |format| respond_to do |format|
format.json do format.json do
render json: { can_create_branch: can_create, has_related_branch: @issue.has_related_branch? } render json: { can_create_branch: can_create, suggested_branch_name: @issue.suggested_branch_name }
end end
end end
end end
...@@ -177,7 +177,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -177,7 +177,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def authorize_create_merge_request! def authorize_create_merge_request!
render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?
end end
def render_issue_json def render_issue_json
......
...@@ -78,8 +78,6 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -78,8 +78,6 @@ class Projects::JobsController < Projects::ApplicationController
result.merge!(trace.to_h) result.merge!(trace.to_h)
end end
result[:html] = result[:html].presence || 'No job log'
render json: result render json: result
end end
end end
......
...@@ -28,11 +28,12 @@ class Projects::RepositoriesController < Projects::ApplicationController ...@@ -28,11 +28,12 @@ class Projects::RepositoriesController < Projects::ApplicationController
end end
def assign_archive_vars def assign_archive_vars
@id = params[:id] if params[:id]
@ref, @filename = extract_ref(params[:id])
return unless @id else
@ref = params[:ref]
@ref, @filename = extract_ref(@id) @filename = nil
end
rescue InvalidPathError rescue InvalidPathError
render_404 render_404
end end
......
...@@ -9,6 +9,32 @@ module IssuablesHelper ...@@ -9,6 +9,32 @@ module IssuablesHelper
"right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}" "right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}"
end end
def sidebar_gutter_tooltip_text
sidebar_gutter_collapsed? ? _('Expand sidebar') : _('Collapse sidebar')
end
def sidebar_assignee_tooltip_label(issuable)
if issuable.assignee
issuable.assignee.name
else
issuable.allows_multiple_assignees? ? _('Assignee(s)') : _('Assignee')
end
end
def sidebar_due_date_tooltip_label(issuable)
if issuable.due_date
"#{_('Due date')}<br />#{due_date_remaining_days(issuable)}"
else
_('Due date')
end
end
def due_date_remaining_days(issuable)
remaining_days_in_words = remaining_days_in_words(issuable)
"#{issuable.due_date.to_s(:medium)} (#{remaining_days_in_words})"
end
def multi_label_name(current_labels, default_label) def multi_label_name(current_labels, default_label)
if current_labels && current_labels.any? if current_labels && current_labels.any?
title = current_labels.first.try(:title) title = current_labels.first.try(:title)
...@@ -153,10 +179,14 @@ module IssuablesHelper ...@@ -153,10 +179,14 @@ module IssuablesHelper
def issuable_labels_tooltip(labels, limit: 5) def issuable_labels_tooltip(labels, limit: 5)
first, last = labels.partition.with_index { |_, i| i < limit } first, last = labels.partition.with_index { |_, i| i < limit }
label_names = first.collect(&:name) if labels && labels.any?
label_names << "and #{last.size} more" unless last.empty? label_names = first.collect(&:name)
label_names << "and #{last.size} more" unless last.empty?
label_names.join(', ') label_names.join(', ')
else
_("Labels")
end
end end
def issuables_state_counter_text(issuable_type, state, display_count) def issuables_state_counter_text(issuable_type, state, display_count)
...@@ -321,7 +351,7 @@ module IssuablesHelper ...@@ -321,7 +351,7 @@ module IssuablesHelper
def issuable_todo_button_data(issuable, todo, is_collapsed) def issuable_todo_button_data(issuable, todo, is_collapsed)
{ {
todo_text: "Add todo", todo_text: "Add todo",
mark_text: "Mark done", mark_text: "Mark todo as done",
todo_icon: (is_collapsed ? icon('plus-square') : nil), todo_icon: (is_collapsed ? icon('plus-square') : nil),
mark_icon: (is_collapsed ? icon('check-square', class: 'todo-undone') : nil), mark_icon: (is_collapsed ? icon('check-square', class: 'todo-undone') : nil),
issuable_id: issuable.id, issuable_id: issuable.id,
......
module MilestonesHelper module MilestonesHelper
include EntityDateHelper
def milestones_filter_path(opts = {}) def milestones_filter_path(opts = {})
if @project if @project
project_milestones_path(@project, opts) project_milestones_path(@project, opts)
...@@ -72,6 +74,19 @@ module MilestonesHelper ...@@ -72,6 +74,19 @@ module MilestonesHelper
end end
end end
def milestone_progress_tooltip_text(milestone)
has_issues = milestone.total_issues_count(current_user) > 0
if has_issues
[
_('Progress'),
_("%{percent}%% complete") % { percent: milestone.percent_complete(current_user) }
].join('<br />')
else
_('Progress')
end
end
def milestone_progress_bar(milestone) def milestone_progress_bar(milestone)
options = { options = {
class: 'progress-bar progress-bar-success', class: 'progress-bar progress-bar-success',
...@@ -95,27 +110,69 @@ module MilestonesHelper ...@@ -95,27 +110,69 @@ module MilestonesHelper
end end
def milestone_tooltip_title(milestone) def milestone_tooltip_title(milestone)
if milestone.due_date if milestone
[milestone.due_date.to_s(:medium), "(#{milestone_remaining_days(milestone)})"].join(' ') "#{milestone.title}<br />#{milestone_tooltip_due_date(milestone)}"
else
_('Milestone')
end end
end end
def milestone_remaining_days(milestone) def milestone_time_for(date, date_type)
if milestone.expired? title = date_type == :start ? "Start date" : "End date"
content_tag(:strong, 'Past due')
elsif milestone.upcoming? if date
content_tag(:strong, 'Upcoming') time_ago = time_ago_in_words(date)
elsif milestone.due_date time_ago.slice!("about ")
time_ago = time_ago_in_words(milestone.due_date)
content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" } time_ago << if date.past?
content.slice!("about ") " ago"
content << " remaining" else
content.html_safe " remaining"
elsif milestone.start_date && milestone.start_date.past? end
days = milestone.elapsed_days
content = content_tag(:strong, days) content = [
content << " #{'day'.pluralize(days)} elapsed" title,
"<br />",
date.to_s(:medium),
"(#{time_ago})"
].join(" ")
content.html_safe content.html_safe
else
title
end
end
def milestone_issues_tooltip_text(milestone)
issues = milestone.count_issues_by_state(current_user)
return _("Issues") if issues.empty?
content = []
content << n_("1 open issue", "%d open issues", issues["opened"]) % issues["opened"] if issues["opened"]
content << n_("1 closed issue", "%d closed issues", issues["closed"]) % issues["closed"] if issues["closed"]
content.join('<br />').html_safe
end
def milestone_merge_requests_tooltip_text(milestone)
merge_requests = milestone.merge_requests
return _("Merge requests") if merge_requests.empty?
content = []
content << n_("1 open merge request", "%d open merge requests", merge_requests.opened.count) % merge_requests.opened.count if merge_requests.opened.any?
content << n_("1 closed merge request", "%d closed merge requests", merge_requests.closed.count) % merge_requests.closed.count if merge_requests.closed.any?
content << n_("1 merged merge request", "%d merged merge requests", merge_requests.merged.count) % merge_requests.merged.count if merge_requests.merged.any?
content.join('<br />').html_safe
end
def milestone_tooltip_due_date(milestone)
if milestone.due_date
"#{milestone.due_date.to_s(:medium)} (#{remaining_days_in_words(milestone)})"
end end
end end
......
...@@ -248,7 +248,7 @@ class Commit ...@@ -248,7 +248,7 @@ class Commit
end end
def notes_with_associations def notes_with_associations
notes.includes(:author) notes.includes(:author, :award_emoji)
end end
def merge_requests def merge_requests
......
...@@ -27,8 +27,9 @@ module AtomicInternalId ...@@ -27,8 +27,9 @@ module AtomicInternalId
module ClassMethods module ClassMethods
def has_internal_id(column, scope:, init:) # rubocop:disable Naming/PredicateName def has_internal_id(column, scope:, init:) # rubocop:disable Naming/PredicateName
before_validation(on: :create) do before_validation(on: :create) do
if read_attribute(column).blank? scope_value = association(scope).reader
scope_attrs = { scope => association(scope).reader } if read_attribute(column).blank? && scope_value
scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value }
usage = self.class.table_name.to_sym usage = self.class.table_name.to_sym
new_iid = InternalId.generate_next(self, scope_attrs, usage, init) new_iid = InternalId.generate_next(self, scope_attrs, usage, init)
......
...@@ -102,14 +102,14 @@ module Milestoneish ...@@ -102,14 +102,14 @@ module Milestoneish
Gitlab::TimeTrackingFormatter.output(total_issue_time_estimate) Gitlab::TimeTrackingFormatter.output(total_issue_time_estimate)
end end
private
def count_issues_by_state(user) def count_issues_by_state(user)
memoize_per_user(user, :count_issues_by_state) do memoize_per_user(user, :count_issues_by_state) do
issues_visible_to_user(user).reorder(nil).group(:state).count issues_visible_to_user(user).reorder(nil).group(:state).count
end end
end end
private
def memoize_per_user(user, method_name) def memoize_per_user(user, method_name)
memoized_users[method_name][user&.id] ||= yield memoized_users[method_name][user&.id] ||= yield
end end
......
module NonatomicInternalId
extend ActiveSupport::Concern
included do
validate :set_iid, on: :create
validates :iid, presence: true, numericality: true
end
def set_iid
if iid.blank?
parent = project || group
records = parent.public_send(self.class.name.tableize) # rubocop:disable GitlabSecurity/PublicSend
max_iid = records.maximum(:iid)
self.iid = max_iid.to_i + 1
end
end
def to_param
iid.to_s
end
end
# Uniquify
#
# Return a version of the given 'base' string that is unique
# by appending a counter to it. Uniqueness is determined by
# repeated calls to the passed block.
#
# You can pass an initial value for the counter, if not given
# counting starts from 1.
#
# If `base` is a function/proc, we expect that calling it with a
# candidate counter returns a string to test/return.
class Uniquify class Uniquify
# Return a version of the given 'base' string that is unique def initialize(counter = nil)
# by appending a counter to it. Uniqueness is determined by @counter = counter
# repeated calls to the passed block. end
#
# If `base` is a function/proc, we expect that calling it with a
# candidate counter returns a string to test/return.
def string(base) def string(base)
@base = base @base = base
@counter = nil
increment_counter! while yield(base_string) increment_counter! while yield(base_string)
base_string base_string
......
class Deployment < ActiveRecord::Base class Deployment < ActiveRecord::Base
include NonatomicInternalId include AtomicInternalId
belongs_to :project, required: true belongs_to :project, required: true
belongs_to :environment, required: true belongs_to :environment, required: true
belongs_to :user belongs_to :user
belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :deployable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.deployments&.maximum(:iid) }
validates :sha, presence: true validates :sha, presence: true
validates :ref, presence: true validates :ref, presence: true
......
...@@ -12,8 +12,9 @@ ...@@ -12,8 +12,9 @@
# * (Optionally) add columns to `internal_ids` if needed for scope. # * (Optionally) add columns to `internal_ids` if needed for scope.
class InternalId < ActiveRecord::Base class InternalId < ActiveRecord::Base
belongs_to :project belongs_to :project
belongs_to :namespace
enum usage: { issues: 0 } enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4 }
validates :usage, presence: true validates :usage, presence: true
......
...@@ -194,6 +194,15 @@ class Issue < ActiveRecord::Base ...@@ -194,6 +194,15 @@ class Issue < ActiveRecord::Base
branches_with_iid - branches_with_merge_request branches_with_iid - branches_with_merge_request
end end
def suggested_branch_name
return to_branch_name unless project.repository.branch_exists?(to_branch_name)
start_counting_from = 2
Uniquify.new(start_counting_from).string(-> (counter) { "#{to_branch_name}-#{counter}" }) do |suggested_branch_name|
project.repository.branch_exists?(suggested_branch_name)
end
end
# Returns boolean if a related branch exists for the current issue # Returns boolean if a related branch exists for the current issue
# ignores merge requests branchs # ignores merge requests branchs
def has_related_branch? def has_related_branch?
...@@ -248,11 +257,8 @@ class Issue < ActiveRecord::Base ...@@ -248,11 +257,8 @@ class Issue < ActiveRecord::Base
end end
end end
def can_be_worked_on?(current_user) def can_be_worked_on?
!self.closed? && !self.closed? && !self.project.forked?
!self.project.forked? &&
self.related_branches(current_user).empty? &&
self.closed_by_merge_requests(current_user).empty?
end end
# Returns `true` if the current issue can be viewed by either a logged in User # Returns `true` if the current issue can be viewed by either a logged in User
......
class MergeRequest < ActiveRecord::Base class MergeRequest < ActiveRecord::Base
include NonatomicInternalId include AtomicInternalId
include Issuable include Issuable
include Noteable include Noteable
include Referable include Referable
...@@ -18,6 +18,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -18,6 +18,8 @@ class MergeRequest < ActiveRecord::Base
belongs_to :source_project, class_name: "Project" belongs_to :source_project, class_name: "Project"
belongs_to :merge_user, class_name: "User" belongs_to :merge_user, class_name: "User"
has_internal_id :iid, scope: :target_project, init: ->(s) { s&.target_project&.merge_requests&.maximum(:iid) }
has_many :merge_request_diffs has_many :merge_request_diffs
has_one :merge_request_diff, has_one :merge_request_diff,
......
...@@ -8,7 +8,7 @@ class Milestone < ActiveRecord::Base ...@@ -8,7 +8,7 @@ class Milestone < ActiveRecord::Base
Started = MilestoneStruct.new('Started', '#started', -3) Started = MilestoneStruct.new('Started', '#started', -3)
include CacheMarkdownField include CacheMarkdownField
include NonatomicInternalId include AtomicInternalId
include Sortable include Sortable
include Referable include Referable
include StripAttribute include StripAttribute
...@@ -21,6 +21,9 @@ class Milestone < ActiveRecord::Base ...@@ -21,6 +21,9 @@ class Milestone < ActiveRecord::Base
belongs_to :project belongs_to :project
belongs_to :group belongs_to :group
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.milestones&.maximum(:iid) }
has_internal_id :iid, scope: :group, init: ->(s) { s&.group&.milestones&.maximum(:iid) }
has_many :issues has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests has_many :merge_requests
......
...@@ -947,10 +947,13 @@ class User < ActiveRecord::Base ...@@ -947,10 +947,13 @@ class User < ActiveRecord::Base
end end
def manageable_groups def manageable_groups
union = Gitlab::SQL::Union.new([owned_groups.select(:id), union_sql = Gitlab::SQL::Union.new([owned_groups.select(:id), masters_groups.select(:id)]).to_sql
masters_groups.select(:id)])
arel_union = Arel::Nodes::SqlLiteral.new(union.to_sql) # Update this line to not use raw SQL when migrated to Rails 5.2.
owned_and_master_groups = Group.where(Group.arel_table[:id].in(arel_union)) # Either ActiveRecord or Arel constructions are fine.
# This was replaced with the raw SQL construction because of bugs in the arel gem.
# Bugs were fixed in arel 9.0.0 (Rails 5.2).
owned_and_master_groups = Group.where("namespaces.id IN (#{union_sql})") # rubocop:disable GitlabSecurity/SqlInjection
Gitlab::GroupHierarchy.new(owned_and_master_groups).base_and_descendants Gitlab::GroupHierarchy.new(owned_and_master_groups).base_and_descendants
end end
......
module EntityDateHelper module EntityDateHelper
include ActionView::Helpers::DateHelper include ActionView::Helpers::DateHelper
include ActionView::Helpers::TagHelper
def interval_in_words(diff) def interval_in_words(diff)
return 'Not started' unless diff return 'Not started' unless diff
...@@ -34,4 +35,30 @@ module EntityDateHelper ...@@ -34,4 +35,30 @@ module EntityDateHelper
duration_hash duration_hash
end end
# Generates an HTML-formatted string for remaining dates based on start_date and due_date
#
# It returns "Past due" for expired entities
# It returns "Upcoming" for upcoming entities
# If due date is provided, it returns "# days|weeks|months remaining|ago"
# If start date is provided and elapsed, with no due date, it returns "# days elapsed"
def remaining_days_in_words(entity)
if entity.try(:expired?)
content_tag(:strong, 'Past due')
elsif entity.try(:upcoming?)
content_tag(:strong, 'Upcoming')
elsif entity.due_date
is_upcoming = (entity.due_date - Date.today).to_i > 0
time_ago = time_ago_in_words(entity.due_date)
content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" }
content.slice!("about ")
content << " " + (is_upcoming ? _("remaining") : _("ago"))
content.html_safe
elsif entity.start_date && entity.start_date.past?
days = entity.elapsed_days
content = content_tag(:strong, days)
content << " #{'day'.pluralize(days)} elapsed"
content.html_safe
end
end
end end
...@@ -8,9 +8,10 @@ module Projects ...@@ -8,9 +8,10 @@ module Projects
template_name = params.delete(:template_name) template_name = params.delete(:template_name)
file = Gitlab::ProjectTemplate.find(template_name).file file = Gitlab::ProjectTemplate.find(template_name).file
override_params = params.dup
params[:file] = file params[:file] = file
GitlabProjectsImportService.new(current_user, params).execute GitlabProjectsImportService.new(current_user, params, override_params).execute
ensure ensure
file&.close file&.close
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
- if issue.milestone - if issue.milestone
%span.issuable-milestone.hidden-xs %span.issuable-milestone.hidden-xs
&nbsp; &nbsp;
= link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: issuable_milestone_tooltip_title(issue) } do = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_due_date(issue.milestone) } do
= icon('clock-o') = icon('clock-o')
= issue.milestone.title = issue.milestone.title
- if issue.due_date - if issue.due_date
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
- if merge_request.milestone - if merge_request.milestone
%span.issuable-milestone.hidden-xs %span.issuable-milestone.hidden-xs
&nbsp; &nbsp;
= link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: issuable_milestone_tooltip_title(merge_request) } do = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_due_date(merge_request.milestone) } do
= icon('clock-o') = icon('clock-o')
= merge_request.milestone.title = merge_request.milestone.title
- if merge_request.target_project.default_branch != merge_request.target_branch - if merge_request.target_project.default_branch != merge_request.target_branch
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
- if current_user - if current_user
%span.issuable-header-text.hide-collapsed.pull-left %span.issuable-header-text.hide-collapsed.pull-left
= _('Todo') = _('Todo')
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" } %a.gutter-toggle.pull-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left' } }
= sidebar_gutter_toggle_icon = sidebar_gutter_toggle_icon
- if current_user - if current_user
= render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable = render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable
...@@ -19,12 +19,11 @@ ...@@ -19,12 +19,11 @@
.block.assignee .block.assignee
= render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present? = render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present?
.block.milestone .block.milestone
.sidebar-collapsed-icon .sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } }
= icon('clock-o', 'aria-hidden': 'true') = icon('clock-o', 'aria-hidden': 'true')
%span.milestone-title %span.milestone-title
- if issuable.milestone - if issuable.milestone
%span.has-tooltip{ title: "#{issuable.milestone.title}<br>#{milestone_tooltip_title(issuable.milestone)}", data: { container: 'body', html: 1, placement: 'left' } } = issuable.milestone.title
= issuable.milestone.title
- else - else
= _('None') = _('None')
.title.hide-collapsed .title.hide-collapsed
...@@ -34,7 +33,7 @@ ...@@ -34,7 +33,7 @@
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed .value.hide-collapsed
- if issuable.milestone - if issuable.milestone
= link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_title(issuable.milestone), data: { container: "body", html: 1 } = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_due_date(issuable.milestone), data: { container: "body", html: 1 }
- else - else
%span.no-value %span.no-value
= _('None') = _('None')
...@@ -50,7 +49,7 @@ ...@@ -50,7 +49,7 @@
= icon('spinner spin', 'aria-hidden': 'true') = icon('spinner spin', 'aria-hidden': 'true')
- if issuable.has_attribute?(:due_date) - if issuable.has_attribute?(:due_date)
.block.due_date .block.due_date
.sidebar-collapsed-icon .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 1 }, title: sidebar_due_date_tooltip_label(issuable) }
= icon('calendar', 'aria-hidden': 'true') = icon('calendar', 'aria-hidden': 'true')
%span.js-due-date-sidebar-value %span.js-due-date-sidebar-value
= issuable.due_date.try(:to_s, :medium) || 'None' = issuable.due_date.try(:to_s, :medium) || 'None'
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
= _('Assignee') = _('Assignee')
= icon('spinner spin') = icon('spinner spin')
- else - else
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee.name if issuable.assignee) } .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: sidebar_assignee_tooltip_label(issuable) }
- if issuable.assignee - if issuable.assignee
= link_to_member(@project, issuable.assignee, size: 24) = link_to_member(@project, issuable.assignee, size: 24)
- else - else
......
- is_collapsed = local_assigns.fetch(:is_collapsed, false) - is_collapsed = local_assigns.fetch(:is_collapsed, false)
- mark_content = is_collapsed ? icon('check-square', class: 'todo-undone') : _('Mark done') - mark_content = is_collapsed ? icon('check-square', class: 'todo-undone') : _('Mark todo as done')
- todo_content = is_collapsed ? icon('plus-square') : _('Add todo') - todo_content = is_collapsed ? icon('plus-square') : _('Add todo')
%button.issuable-todo-btn.js-issuable-todo{ type: 'button', %button.issuable-todo-btn.js-issuable-todo{ type: 'button',
class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn pull-right'), class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn pull-right'),
title: (todo.nil? ? _('Add todo') : _('Mark done')), title: (todo.nil? ? _('Add todo') : _('Mark todo as done')),
'aria-label' => (todo.nil? ? _('Add todo') : _('Mark done')), 'aria-label' => (todo.nil? ? _('Add todo') : _('Mark todo as done')),
data: issuable_todo_button_data(issuable, todo, is_collapsed) } data: issuable_todo_button_data(issuable, todo, is_collapsed) }
%span.issuable-todo-inner.js-issuable-todo-inner< %span.issuable-todo-inner.js-issuable-todo-inner<
- if todo - if todo
......
- merge_request = issuable - merge_request = issuable
.block.assignee .block.assignee
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (merge_request.assignee.name if merge_request.assignee) } .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: sidebar_assignee_tooltip_label(issuable) }
- if merge_request.assignee - if merge_request.assignee
= link_to_member(@project, merge_request.assignee, size: 24) = link_to_member(@project, merge_request.assignee, size: 24)
- else - else
......
...@@ -4,12 +4,8 @@ ...@@ -4,12 +4,8 @@
%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } %aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar.milestone-sidebar .issuable-sidebar.milestone-sidebar
.block.milestone-progress.issuable-sidebar-header .block.milestone-progress.issuable-sidebar-header
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" } %a.gutter-toggle.pull-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left' } }
= sidebar_gutter_toggle_icon = sidebar_gutter_toggle_icon
.sidebar-collapsed-icon
%span== #{milestone.percent_complete(current_user)}%
= milestone_progress_bar(milestone)
.title.hide-collapsed .title.hide-collapsed
%strong.bold== #{milestone.percent_complete(current_user)}% %strong.bold== #{milestone.percent_complete(current_user)}%
%span.hide-collapsed %span.hide-collapsed
...@@ -17,6 +13,11 @@ ...@@ -17,6 +13,11 @@
.value.hide-collapsed .value.hide-collapsed
= milestone_progress_bar(milestone) = milestone_progress_bar(milestone)
.block.milestone-progress.hide-expanded
.sidebar-collapsed-icon.has-tooltip{ title: milestone_progress_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } }
%span== #{milestone.percent_complete(current_user)}%
= milestone_progress_bar(milestone)
.block.start_date.hide-collapsed .block.start_date.hide-collapsed
.title .title
Start date Start date
...@@ -35,19 +36,25 @@ ...@@ -35,19 +36,25 @@
%span.collapsed-milestone-date %span.collapsed-milestone-date
- if milestone.start_date && milestone.due_date - if milestone.start_date && milestone.due_date
- if milestone.start_date.year == milestone.due_date.year - if milestone.start_date.year == milestone.due_date.year
.milestone-date= milestone.start_date.strftime('%b %-d') .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
= milestone.start_date.strftime('%b %-d')
- else - else
.milestone-date= milestone.start_date.strftime('%b %-d %Y') .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
= milestone.start_date.strftime('%b %-d %Y')
.date-separator - .date-separator -
.due_date= milestone.due_date.strftime('%b %-d %Y') .due_date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 1, placement: 'left' } }
= milestone.due_date.strftime('%b %-d %Y')
- elsif milestone.start_date - elsif milestone.start_date
From From
.milestone-date= milestone.start_date.strftime('%b %-d %Y') .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
= milestone.start_date.strftime('%b %-d %Y')
- elsif milestone.due_date - elsif milestone.due_date
Until Until
.milestone-date= milestone.due_date.strftime('%b %-d %Y') .milestone-date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 1, placement: 'left' } }
= milestone.due_date.strftime('%b %-d %Y')
- else - else
None .has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
None
.title.hide-collapsed .title.hide-collapsed
Due date Due date
- if @project && can?(current_user, :admin_milestone, @project) - if @project && can?(current_user, :admin_milestone, @project)
...@@ -58,14 +65,14 @@ ...@@ -58,14 +65,14 @@
%span.bold= milestone.due_date.to_s(:medium) %span.bold= milestone.due_date.to_s(:medium)
- else - else
%span.no-value No due date %span.no-value No due date
- remaining_days = milestone_remaining_days(milestone) - remaining_days = remaining_days_in_words(milestone)
- if remaining_days.present? - if remaining_days.present?
= surround '(', ')' do = surround '(', ')' do
%span.remaining-days= remaining_days %span.remaining-days= remaining_days
- if !project || can?(current_user, :read_issue, project) - if !project || can?(current_user, :read_issue, project)
.block.issues .block.issues
.sidebar-collapsed-icon .sidebar-collapsed-icon.has-tooltip{ title: milestone_issues_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } }
%strong %strong
= custom_icon('issues') = custom_icon('issues')
%span= milestone.issues_visible_to_user(current_user).count %span= milestone.issues_visible_to_user(current_user).count
...@@ -93,7 +100,7 @@ ...@@ -93,7 +100,7 @@
= icon('spinner spin') = icon('spinner spin')
.block.merge-requests .block.merge-requests
.sidebar-collapsed-icon .sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } }
%strong %strong
= custom_icon('mr_bold') = custom_icon('mr_bold')
%span= milestone.merge_requests.count %span= milestone.merge_requests.count
......
---
title: Enable restore rake task to handle nested storage directories
merge_request: 17516
author: Balasankar C
type: fixed
---
title: Add support for patch link extension for commit links on GitLab Flavored Markdown
merge_request:
author:
type: added
---
title: Include matching branches and tags in protected branches / tags count
merge_request:
author: Jan Beckmann
type: fixed
---
title: Send notification emails when push to a merge request
merge_request: 7610
author: YarNayar
type: feature
---
title: Improve tooltips in collapsed right sidebar
merge_request: 17714
author:
type: changed
---
title: Atomic generation of internal ids for issues.
merge_request: 17580
author:
type: other
---
title: Create Deploy Tokens to allow permanent access to repository and registry
merge_request: 17894
author:
type: added
---
title: Drop JSON response in Project Milestone along with avoiding error
merge_request: 17977
author: Takuya Noguchi
type: fixed
---
title: Fix generated URL when listing repoitories for import
merge_request: 17692
author:
type: fixed
---
title: Fixed bug in dropdown selector when selecting the same selection again
merge_request: 14631
author: bitsapien
type: fixed
---
title: Add an API endpoint to download git repository snapshots
merge_request: 18173
author:
type: added
---
title: Apply NestingDepth (level 5) (framework/dropdowns.scss)
merge_request: 17820
author: Takuya Noguchi
type: other
---
title: 'API: Add parameter merge_method to projects'
merge_request: 18031
author: Jan Beckmann
type: added
---
title: Add object storage support for LFS objects, CI artifacts, and uploads.
merge_request: 17358
author:
type: added
---
title: Increase dropdown width in pipeline graph & center action icon
merge_request: 18089
author:
type: fixed
---
title: 'Introduce simpler env vars for auto devops REPLICAS and CANARY_REPLICAS #41436'
merge_request: 18036
author:
type: added
---
title: Added confirmation modal for changing username
merge_request: 17405
author:
type: added
---
title: Adds the option to the project export API to override the project description and display GitLab export description once imported
merge_request: 17744
author:
type: added
---
title: adds closed by informations in issue api
merge_request: 17042
author: haseebeqx
type: added
---
title: Fix XSS on diff view stored on filenames
merge_request:
author:
type: security
---
title: Long instance urls do not overflow anymore during project creation
merge_request: 17717
author:
type: fixed
---
title: Improved visual styles and consistency for commit hash and possible actions
across commit lists
merge_request: 17406
author:
type: changed
---
title: Improve empty state for canceled job
merge_request: 17646
author:
type: fixed
---
title: Fix hover style of dropdown items in the right sidebar
merge_request: 17519
author:
type: fixed
--- ---
title: Adds cancel btn to new pages domain page title: Show new branch/mr button even when branch exists
merge_request: 18026 merge_request: 17712
author: Jacopo Beschi @jacopo-beschi author: Jacopo Beschi @jacopo-beschi
type: added type: added
---
title: Fix Firefox stealing formatting characters on issue notes
merge_request:
author:
type: fixed
---
title: Improve performance of loading issues with lots of references to merge requests
merge_request: 17986
author:
type: performance
---
title: Polish design for verifying domains
merge_request: 17767
author:
type: changed
---
title: Require at least one filter when listing issues or merge requests on dashboard
page
merge_request:
author:
type: performance
---
title: Use specific names for filtered CI variable controller parameters
merge_request: 17796
author:
type: other
---
title: Add empty repo check before running AutoDevOps pipeline
merge_request: 17605
author:
type: changed
---
title: Adds support for OmniAuth JWT provider
merge_request: 17774
author:
type: added
---
title: Limit the number of failed logins when using LDAP for authentication
merge_request: 43525
author:
type: added
---
title: Improves the performance of projects list page
merge_request: 17934
author:
type: performance
---
title: Move ci/lint under project's namespace
merge_request: 17729
author:
type: added
---
title: Update wording to specify create/manage project vs group labels in labels dropdown
merge_request: 17640
author:
type: changed
---
title: Set breadcrumb for admin/runners/show
merge_request: 17431
author: Takuya Noguchi
type: fixed
---
title: Update documentation to reflect current minimum required versions of node and
yarn
merge_request: 17706
author:
type: other
---
title: Store sha256 checksum of artifact metadata
merge_request: 18149
author:
type: added
---
title: Change avatar error message to include allowed file formats
merge_request: 17747
author: Fabian Schneider
type: changed
---
title: Add tooltips to icons in lists of issues and merge requests
merge_request: 17700
author:
type: changed
---
title: Add Gitaly call details to performance bar
merge_request:
author:
type: added
---
title: Update ruby-saml to 1.7.2 and omniauth-saml to 1.10.0
merge_request: 17734
author: Takuya Noguchi
type: security
---
title: Send @mention notifications even if a user has explicitly unsubscribed from
item
merge_request:
author:
type: added
---
title: Implement foreground verification of CI artifacts
merge_request: 17578
author:
type: added
---
title: Fix personal access token clipboard button style
merge_request: 17978
author: Fabian Schneider
type: fixed
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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