Merge branch 'master' into 41249-clearing-the-cache

This commit is contained in:
Matija Čupić 2018-01-04 23:38:13 +01:00
commit 9c9f7dc639
No known key found for this signature in database
GPG Key ID: 4BAF84FFACD2E5DE
732 changed files with 7487 additions and 2666 deletions

View File

@ -431,6 +431,7 @@ ee_compat_check:
- master
- tags
- /^[\d-]+-stable(-ee)?/
- /^security-/
- branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee
retry: 0
@ -508,7 +509,7 @@ db:rollback-mysql:
<<: *db-rollback
<<: *use-mysql
.db-seed_fu: &db-seed_fu
.gitlab-setup: &gitlab-setup
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
@ -517,22 +518,24 @@ db:rollback-mysql:
SIZE: "1"
SETUP_DB: "false"
CREATE_DB_USER: "true"
FIXTURE_PATH: db/fixtures/development
script:
- git clone https://gitlab.com/gitlab-org/gitlab-test.git
/home/git/repositories/gitlab-org/gitlab-test.git
- bundle exec rake db:setup db:seed_fu
- scripts/gitaly-test-spawn
- force=yes bundle exec rake gitlab:setup
artifacts:
when: on_failure
expire_in: 1d
paths:
- log/development.log
db:seed_fu-pg:
<<: *db-seed_fu
gitlab:setup-pg:
<<: *gitlab-setup
<<: *use-pg
db:seed_fu-mysql:
<<: *db-seed_fu
gitlab:setup-mysql:
<<: *gitlab-setup
<<: *use-mysql
# Frontend-related jobs
@ -600,6 +603,14 @@ codequality:
artifacts:
paths: [codeclimate.json]
sast:
image: registry.gitlab.com/gitlab-org/gl-sast:latest
before_script: []
script:
- /app/bin/run .
artifacts:
paths: [gl-sast-report.json]
qa:internal:
<<: *dedicated-runner
<<: *except-docs

View File

@ -3,6 +3,7 @@ inherit_gem:
- rubocop-default.yml
inherit_from: .rubocop_todo.yml
require: ./rubocop/rubocop
AllCops:
TargetRailsVersion: 4.2
@ -24,8 +25,10 @@ Gitlab/ModuleWithInstanceVariables:
Exclude:
# We ignore Rails helpers right now because it's hard to workaround it
- app/helpers/**/*_helper.rb
- ee/app/helpers/**/*_helper.rb
# We ignore Rails mailers right now because it's hard to workaround it
- app/mailers/emails/**/*.rb
- ee/**/emails/**/*.rb
# We ignore spec helpers because it usually doesn't matter
- spec/support/**/*.rb
- features/steps/**/*.rb

View File

@ -2,6 +2,35 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.3.3 (2018-01-02)
### Fixed (3 changes)
- Fix links to old commits in merge request comments.
- Fix 404 errors after a user edits an issue description and solves the reCAPTCHA.
- Gracefully handle orphaned write deploy keys in /internal/post_receive.
## 10.3.2 (2017-12-28)
### Fixed (1 change)
- Fix migration for removing orphaned issues.moved_to_id values in MySQL and PostgreSQL.
## 10.3.1 (2017-12-27)
### Fixed (3 changes)
- Don't link LFS objects to a project when unlinking forks when they were already linked. !16006
- Execute project hooks and services after commit when moving an issue.
- Fix Error 500s with anonymous clones for a project that has moved.
### Changed (1 change)
- Reduce the number of buckets in gitlab_cache_operation_duration_seconds metric. !15881
## 10.3.0 (2017-12-22)
### Security (1 change, 1 of them is from the community)

View File

@ -553,7 +553,7 @@ the feature you contribute through all of these steps.
1. Description explaining the relevancy (see following item)
1. Working and clean code that is commented where needed
1. [Unit and system tests][testing] that pass on the CI server
1. [Unit, integration, and system tests][testing] that pass on the CI server
1. Performance/scalability implications have been considered, addressed, and tested
1. [Documented][doc-styleguide] in the `/doc` directory
1. [Changelog entry added][changelog], if necessary

View File

@ -1 +1 @@
0.60.0
0.65.0

View File

@ -12,7 +12,7 @@ gem 'sprockets', '~> 3.7.0'
gem 'default_value_for', '~> 3.0.0'
# Supported DBs
gem 'mysql2', '~> 0.4.5', group: :mysql
gem 'mysql2', '~> 0.4.10', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.26.0'
@ -283,7 +283,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~> 0.7.0.beta43'
gem 'prometheus-client-mmap', '~> 0.7.0.beta44'
gem 'raindrops', '~> 0.18'
end
@ -402,7 +402,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.61.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.64.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false

View File

@ -284,7 +284,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.61.0)
gitaly-proto (0.64.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@ -505,7 +505,7 @@ GEM
mustermann (1.0.0)
mustermann-grape (1.0.0)
mustermann (~> 1.0.0)
mysql2 (0.4.5)
mysql2 (0.4.10)
net-ldap (0.16.0)
net-ssh (4.1.0)
netrc (0.11.0)
@ -634,7 +634,7 @@ GEM
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.7.0.beta43)
prometheus-client-mmap (0.7.0.beta44)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
@ -708,7 +708,7 @@ GEM
json
recursive-open-struct (1.0.0)
redcarpet (3.4.0)
redis (3.3.3)
redis (3.3.5)
redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
@ -839,11 +839,11 @@ GEM
rack
shoulda-matchers (3.1.2)
activesupport (>= 4.0.0)
sidekiq (5.0.4)
sidekiq (5.0.5)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
redis (~> 3.3, >= 3.3.3)
redis (>= 3.3.4, < 5)
sidekiq-cron (0.6.0)
rufus-scheduler (>= 3.3.0)
sidekiq (>= 4.2.1)
@ -1046,7 +1046,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.61.0)
gitaly-proto (~> 0.64.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
@ -1087,7 +1087,7 @@ DEPENDENCIES
method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.4.5)
mysql2 (~> 0.4.10)
net-ldap
net-ssh (~> 4.1.0)
nokogiri (~> 1.8.1)
@ -1122,7 +1122,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta43)
prometheus-client-mmap (~> 0.7.0.beta44)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,10 +1,8 @@
/* eslint-disable no-new */
import Vue from 'vue';
import VueResource from 'vue-resource';
import axios from '../../lib/utils/axios_utils';
import notebookLab from '../../notebook/index.vue';
Vue.use(VueResource);
export default () => {
const el = document.getElementById('js-notebook-viewer');
@ -50,14 +48,14 @@ export default () => {
`,
methods: {
loadFile() {
this.$http.get(el.dataset.endpoint)
.then(response => response.json())
.then((res) => {
this.json = res;
axios.get(el.dataset.endpoint)
.then(res => res.data)
.then((data) => {
this.json = data;
this.loading = false;
})
.catch((e) => {
if (e.status) {
if (e.status !== 200) {
this.loadError = true;
}

View File

@ -2,7 +2,6 @@
import _ from 'underscore';
import Vue from 'vue';
import VueResource from 'vue-resource';
import Flash from '../flash';
import { __ } from '../locale';
import FilteredSearchBoards from './filtered_search_boards';
@ -25,8 +24,6 @@ import './components/new_list_dropdown';
import './components/modal/index';
import '../vue_shared/vue_resource_interceptor';
Vue.use(VueResource);
$(() => {
const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore;
@ -95,14 +92,13 @@ $(() => {
Store.disabled = this.disabled;
gl.boardService.all()
.then(response => response.json())
.then((resp) => {
resp.forEach((board) => {
.then(res => res.data)
.then((data) => {
data.forEach((board) => {
const list = Store.addList(board, this.defaultAvatar);
if (list.type === 'closed') {
list.position = Infinity;
list.label = { description: 'Shows all closed issues. Moving an issue to this list closes it' };
} else if (list.type === 'backlog') {
list.position = -1;
}
@ -113,7 +109,9 @@ $(() => {
Store.addBlankState();
this.loading = false;
})
.catch(() => new Flash('An error occurred. Please try again.'));
.catch(() => {
Flash('An error occurred while fetching the board lists. Please try again.');
});
},
methods: {
updateTokens() {
@ -124,7 +122,7 @@ $(() => {
if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
newIssue.setFetchingState('subscriptions', true);
BoardService.getIssueInfo(sidebarInfoEndpoint)
.then(res => res.json())
.then(res => res.data)
.then((data) => {
newIssue.setFetchingState('subscriptions', false);
newIssue.updateData({

View File

@ -65,7 +65,7 @@ export default {
// Save the labels
gl.boardService.generateDefaultLists()
.then(resp => resp.json())
.then(res => res.data)
.then((data) => {
data.forEach((listObj) => {
const list = Store.findList('title', listObj.title);

View File

@ -115,7 +115,7 @@ export default {
},
mounted() {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
scroll: document.querySelectorAll('.boards-list')[0],
scroll: true,
group: 'issues',
disabled: this.disabled,
filter: '.board-list-count, .is-disabled',

View File

@ -1,5 +1,4 @@
/* eslint-disable comma-dangle, space-before-function-paren, no-new */
/* global MilestoneSelect */
import Vue from 'vue';
import Flash from '../../flash';
@ -12,6 +11,7 @@ import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context';
import LabelsSelect from '../../labels_select';
import subscriptions from '../../sidebar/components/subscriptions/subscriptions.vue';
import MilestoneSelect from '../../milestone_select';
const Store = gl.issueBoards.BoardsStore;

View File

@ -89,7 +89,7 @@ gl.issueBoards.IssuesModal = Vue.extend({
page: this.page,
per: this.perPage,
}))
.then(resp => resp.json())
.then(res => res.data)
.then((data) => {
if (clearIssues) {
this.issues = [];

View File

@ -40,7 +40,7 @@ class List {
save () {
return gl.boardService.createList(this.label.id)
.then(resp => resp.json())
.then(res => res.data)
.then((data) => {
this.id = data.id;
this.type = data.list_type;
@ -90,7 +90,7 @@ class List {
}
return gl.boardService.getIssuesForList(this.id, data)
.then(resp => resp.json())
.then(res => res.data)
.then((data) => {
this.loading = false;
this.issuesSize = data.size;
@ -108,7 +108,7 @@ class List {
this.issuesSize += 1;
return gl.boardService.newIssue(this.id, issue)
.then(resp => resp.json())
.then(res => res.data)
.then((data) => {
issue.id = data.id;
issue.iid = data.iid;

View File

@ -1,82 +1,79 @@
/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */
import Vue from 'vue';
import axios from '../../lib/utils/axios_utils';
import { mergeUrlParams } from '../../lib/utils/url_utility';
export default class BoardService {
constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
issues: {
method: 'GET',
url: `${gon.relative_url_root}/-/boards/${boardId}/issues.json`,
}
});
this.lists = Vue.resource(`${listsEndpoint}{/id}`, {}, {
generate: {
method: 'POST',
url: `${listsEndpoint}/generate.json`
}
});
this.issue = Vue.resource(`${gon.relative_url_root}/-/boards/${boardId}/issues{/id}`, {});
this.issues = Vue.resource(`${listsEndpoint}{/id}/issues`, {}, {
bulkUpdate: {
method: 'POST',
url: bulkUpdatePath,
constructor({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
this.boardsEndpoint = boardsEndpoint;
this.boardId = boardId;
this.listsEndpoint = listsEndpoint;
this.listsEndpointGenerate = `${listsEndpoint}/generate.json`;
this.bulkUpdatePath = bulkUpdatePath;
}
generateBoardsPath(id) {
return `${this.boardsEndpoint}${id ? `/${id}` : ''}.json`;
}
generateIssuesPath(id) {
return `${this.listsEndpoint}${id ? `/${id}` : ''}/issues`;
}
static generateIssuePath(boardId, id) {
return `${gon.relative_url_root}/-/boards/${boardId ? `/${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
}
all() {
return axios.get(this.listsEndpoint);
}
generateDefaultLists() {
return axios.post(this.listsEndpointGenerate, {});
}
createList(labelId) {
return axios.post(this.listsEndpoint, {
list: {
label_id: labelId,
},
});
}
all () {
return this.lists.get();
}
generateDefaultLists () {
return this.lists.generate({});
}
createList (label_id) {
return this.lists.save({}, {
updateList(id, position) {
return axios.put(`${this.listsEndpoint}/${id}`, {
list: {
label_id
}
position,
},
});
}
updateList (id, position) {
return this.lists.update({ id }, {
list: {
position
}
});
destroyList(id) {
return axios.delete(`${this.listsEndpoint}/${id}`);
}
destroyList (id) {
return this.lists.delete({ id });
}
getIssuesForList (id, filter = {}) {
getIssuesForList(id, filter = {}) {
const data = { id };
Object.keys(filter).forEach((key) => { data[key] = filter[key]; });
return this.issues.get(data);
return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
}
moveIssue (id, from_list_id = null, to_list_id = null, move_before_id = null, move_after_id = null) {
return this.issue.update({ id }, {
from_list_id,
to_list_id,
move_before_id,
move_after_id,
moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) {
return axios.put(BoardService.generateIssuePath(this.boardId, id), {
from_list_id: fromListId,
to_list_id: toListId,
move_before_id: moveBeforeId,
move_after_id: moveAfterId,
});
}
newIssue (id, issue) {
return this.issues.save({ id }, {
issue
newIssue(id, issue) {
return axios.post(this.generateIssuesPath(id), {
issue,
});
}
getBacklog(data) {
return this.boards.issues(data);
return axios.get(mergeUrlParams(data, `${gon.relative_url_root}/-/boards/${this.boardId}/issues.json`));
}
bulkUpdate(issueIds, extraData = {}) {
@ -86,15 +83,15 @@ export default class BoardService {
}),
};
return this.issues.bulkUpdate(data);
return axios.post(this.bulkUpdatePath, data);
}
static getIssueInfo(endpoint) {
return Vue.http.get(endpoint);
return axios.get(endpoint);
}
static toggleIssueSubscription(endpoint) {
return Vue.http.post(endpoint);
return axios.post(endpoint);
}
}

View File

@ -5,7 +5,7 @@ import IssuableIndex from './issuable_index';
import Milestone from './milestone';
import IssuableForm from './issuable_form';
import LabelsSelect from './labels_select';
/* global MilestoneSelect */
import MilestoneSelect from './milestone_select';
import NewBranchForm from './new_branch_form';
import NotificationsForm from './notifications_form';
import notificationsDropdown from './notifications_dropdown';

View File

@ -127,7 +127,7 @@ class FilteredSearchManager {
this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.onClearSearchWrapper = this.onClearSearch.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.call(this);
this.removeSelectedTokenKeydownWrapper = this.removeSelectedTokenKeydown.bind(this);
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
this.editTokenWrapper = this.editToken.bind(this);
@ -180,22 +180,34 @@ class FilteredSearchManager {
this.unbindStateEvents();
}
checkForBackspace(e) {
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
checkForBackspace() {
let backspaceCount = 0;
const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken);
const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
// closure for keeping track of the number of backspace keystrokes
return (e) => {
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken);
const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
backspaceCount += 1;
if (backspaceCount === 2) {
backspaceCount = 0;
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
}
}
// Reposition dropdown so that it is aligned with cursor
this.dropdownManager.updateCurrentDropdownOffset();
} else {
backspaceCount = 0;
}
// Reposition dropdown so that it is aligned with cursor
this.dropdownManager.updateCurrentDropdownOffset();
}
};
}
checkForEnter(e) {

View File

@ -77,7 +77,8 @@ export default {
class="group-row"
>
<div
class="group-row-contents">
class="group-row-contents"
:class="{ 'project-row-contents': !isGroup }">
<item-actions
v-if="isGroup"
:group="group"
@ -97,7 +98,7 @@ export default {
/>
</div>
<div
class="avatar-container s40 hidden-xs"
class="avatar-container prepend-top-8 prepend-left-5 s24 hidden-xs"
:class="{ 'content-loading': group.isChildrenLoading }"
>
<a
@ -106,11 +107,12 @@ export default {
>
<img
v-if="hasAvatar"
class="avatar s40"
class="avatar s24"
:src="group.avatarUrl"
/>
<identicon
v-else
size-class="s24"
:entity-id=group.id
:entity-name="group.name"
/>
@ -123,7 +125,7 @@ export default {
:href="group.relativePath"
:title="group.fullName"
class="no-expand"
data-placement="top"
data-placement="bottom"
>{{
// ending bracket must be by closing tag to prevent
// link hover text-decoration from over-extending

View File

@ -1,14 +1,14 @@
<script>
import { s__ } from '../../locale';
import tooltip from '../../vue_shared/directives/tooltip';
import modal from '../../vue_shared/components/modal.vue';
import { s__ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import modal from '~/vue_shared/components/modal.vue';
import eventHub from '../event_hub';
import { COMMON_STR } from '../constants';
import Icon from '../../vue_shared/components/icon.vue';
export default {
components: {
Icon,
icon,
modal,
},
directives: {
@ -64,10 +64,9 @@ export default {
:title="editBtnTitle"
:aria-label="editBtnTitle"
data-container="body"
data-placement="bottom"
class="edit-group btn no-expand">
<icon
name="settings">
</icon>
<icon name="settings"/>
</a>
<a
v-tooltip
@ -77,10 +76,9 @@ export default {
:title="leaveBtnTitle"
:aria-label="leaveBtnTitle"
data-container="body"
data-placement="bottom"
class="leave-group btn no-expand">
<i
class="fa fa-sign-out"
aria-hidden="true"/>
<icon name="leave"/>
</a>
<modal
v-show="modalStatus"

View File

@ -1,4 +1,6 @@
<script>
import icon from '~/vue_shared/components/icon.vue';
export default {
props: {
isGroupOpen: {
@ -7,9 +9,12 @@ export default {
default: false,
},
},
components: {
icon,
},
computed: {
iconClass() {
return this.isGroupOpen ? 'fa-caret-down' : 'fa-caret-right';
return this.isGroupOpen ? 'angle-down' : 'angle-right';
},
},
};
@ -17,9 +22,9 @@ export default {
<template>
<span class="folder-caret">
<i
:class="iconClass"
class="fa"
aria-hidden="true"/>
<icon
:size="12"
:name="iconClass"
/>
</span>
</template>

View File

@ -1,10 +1,14 @@
<script>
import tooltip from '../../vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { ITEM_TYPE, VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE, PROJECT_VISIBILITY_TYPE } from '../constants';
import itemStatsValue from './item_stats_value.vue';
export default {
directives: {
tooltip,
components: {
icon,
timeAgoTooltip,
itemStatsValue,
},
props: {
item: {
@ -34,65 +38,47 @@ export default {
<template>
<div class="stats">
<span
v-tooltip
<item-stats-value
v-if="isGroup"
css-class="number-subgroups"
icon-name="folder"
:title="s__('Subgroups')"
class="number-subgroups"
data-placement="top"
data-container="body">
<i
class="fa fa-folder"
aria-hidden="true"
/>
{{item.subgroupCount}}
</span>
<span
v-tooltip
:value=item.subgroupCount
/>
<item-stats-value
v-if="isGroup"
css-class="number-projects"
icon-name="bookmark"
:title="s__('Projects')"
class="number-projects"
data-placement="top"
data-container="body">
<i
class="fa fa-bookmark"
aria-hidden="true"
/>
{{item.projectCount}}
</span>
<span
v-tooltip
:value=item.projectCount
/>
<item-stats-value
v-if="isGroup"
css-class="number-users"
icon-name="users"
:title="s__('Members')"
class="number-users"
data-placement="top"
data-container="body">
<i
class="fa fa-users"
aria-hidden="true"
/>
{{item.memberCount}}
</span>
<span
:value=item.memberCount
/>
<item-stats-value
v-if="isProject"
class="project-stars">
<i
class="fa fa-star"
aria-hidden="true"
/>
{{item.starCount}}
</span>
<span
v-tooltip
css-class="project-stars"
icon-name="star"
:value=item.starCount
/>
<item-stats-value
css-class="item-visibility"
tooltip-placement="left"
:icon-name="visibilityIcon"
:title="visibilityTooltip"
data-placement="left"
data-container="body"
class="item-visibility">
<i
:class="visibilityIcon"
class="fa"
aria-hidden="true"
/>
<div
class="last-updated"
v-if="isProject"
>
<time-ago-tooltip
tooltip-placement="bottom"
:time="item.updatedAt"
/>
</span>
</div>
</div>
</template>

View File

@ -0,0 +1,68 @@
<script>
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
export default {
props: {
title: {
type: String,
required: false,
default: '',
},
cssClass: {
type: String,
required: false,
default: '',
},
iconName: {
type: String,
required: true,
},
tooltipPlacement: {
type: String,
required: false,
default: 'bottom',
},
/**
* value could either be number or string
* as `memberCount` is always passed as string
* while `subgroupCount` & `projectCount`
* are always number
*/
value: {
type: [Number, String],
required: false,
default: '',
},
},
directives: {
tooltip,
},
components: {
icon,
},
computed: {
isValuePresent() {
return this.value !== '';
},
},
};
</script>
<template>
<span
v-tooltip
data-container="body"
:data-placement="tooltipPlacement"
:class="cssClass"
:title="title"
>
<icon :name="iconName"/>
<span
v-if="isValuePresent"
class="stat-value"
>
{{value}}
</span>
</span>
</template>

View File

@ -1,7 +1,11 @@
<script>
import icon from '~/vue_shared/components/icon.vue';
import { ITEM_TYPE } from '../constants';
export default {
components: {
icon,
},
props: {
itemType: {
type: String,
@ -16,9 +20,9 @@ export default {
computed: {
iconClass() {
if (this.itemType === ITEM_TYPE.GROUP) {
return this.isGroupOpen ? 'fa-folder-open' : 'fa-folder';
return this.isGroupOpen ? 'folder-open' : 'folder';
}
return 'fa-bookmark';
return 'bookmark';
},
},
};
@ -26,9 +30,6 @@ export default {
<template>
<span class="item-type-icon">
<i
:class="iconClass"
class="fa"
aria-hidden="true"/>
<icon :name="iconClass"/>
</span>
</template>

View File

@ -29,7 +29,7 @@ export const PROJECT_VISIBILITY_TYPE = {
};
export const VISIBILITY_TYPE_ICON = {
public: 'fa-globe',
internal: 'fa-shield',
private: 'fa-lock',
public: 'earth',
internal: 'shield',
private: 'lock',
};

View File

@ -91,6 +91,7 @@ export default class GroupsStore {
subgroupCount: rawGroupItem.subgroup_count,
memberCount: rawGroupItem.number_users_with_delimiter,
starCount: rawGroupItem.star_count,
updatedAt: rawGroupItem.updated_at,
};
}

View File

@ -2,11 +2,18 @@
import { mapGetters, mapState, mapActions } from 'vuex';
import repoCommitSection from './repo_commit_section.vue';
import icon from '../../vue_shared/components/icon.vue';
import panelResizer from '../../vue_shared/components/panel_resizer.vue';
export default {
data() {
return {
width: 290,
};
},
components: {
repoCommitSection,
icon,
panelResizer,
},
computed: {
...mapState([
@ -18,10 +25,20 @@ export default {
currentIcon() {
return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
},
maxSize() {
return window.innerWidth / 2;
},
panelStyle() {
if (!this.rightPanelCollapsed) {
return { width: `${this.width}px` };
}
return {};
},
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
'setResizingStatus',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
@ -29,6 +46,12 @@ export default {
collapsed: !this.rightPanelCollapsed,
});
},
resizingStarted() {
this.setResizingStatus(true);
},
resizingEnded() {
this.setResizingStatus(false);
},
},
};
</script>
@ -39,6 +62,7 @@ export default {
:class="{
'is-collapsed': rightPanelCollapsed,
}"
:style="panelStyle"
>
<div
class="multi-file-commit-panel-section">
@ -71,5 +95,14 @@ export default {
<repo-commit-section
class=""/>
</div>
<panel-resizer
:size.sync="width"
:enabled="!rightPanelCollapsed"
:start-size="290"
:min-size="200"
:max-size="maxSize"
@resize-start="resizingStarted"
@resize-end="resizingEnded"
side="left"/>
</div>
</template>

View File

@ -2,11 +2,18 @@
import { mapState, mapActions } from 'vuex';
import projectTree from './ide_project_tree.vue';
import icon from '../../vue_shared/components/icon.vue';
import panelResizer from '../../vue_shared/components/panel_resizer.vue';
export default {
data() {
return {
width: 290,
};
},
components: {
projectTree,
icon,
panelResizer,
},
computed: {
...mapState([
@ -16,10 +23,20 @@ export default {
currentIcon() {
return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left';
},
maxSize() {
return window.innerWidth / 2;
},
panelStyle() {
if (!this.leftPanelCollapsed) {
return { width: `${this.width}px` };
}
return {};
},
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
'setResizingStatus',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
@ -27,6 +44,12 @@ export default {
collapsed: !this.leftPanelCollapsed,
});
},
resizingStarted() {
this.setResizingStatus(true);
},
resizingEnded() {
this.setResizingStatus(false);
},
},
};
</script>
@ -37,6 +60,7 @@ export default {
:class="{
'is-collapsed': leftPanelCollapsed,
}"
:style="panelStyle"
>
<div class="multi-file-commit-panel-inner">
<project-tree
@ -58,5 +82,14 @@ export default {
class="collapse-text"
>Collapse sidebar</span>
</button>
<panel-resizer
:size.sync="width"
:enabled="!leftPanelCollapsed"
:start-size="290"
:min-size="200"
:max-size="maxSize"
@resize-start="resizingStarted"
@resize-end="resizingEnded"
side="right"/>
</div>
</template>

View File

@ -90,6 +90,11 @@ export default {
rightPanelCollapsed() {
this.editor.updateDimensions();
},
panelResizing(isResizing) {
if (isResizing === false) {
this.editor.updateDimensions();
}
},
},
computed: {
...mapGetters([
@ -99,6 +104,7 @@ export default {
...mapState([
'leftPanelCollapsed',
'rightPanelCollapsed',
'panelResizing',
]),
shouldHideEditor() {
return this.activeFile.binary && !this.activeFile.raw;

View File

@ -3,6 +3,7 @@
import timeAgoMixin from '../../vue_shared/mixins/timeago';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
import newDropdown from './new_dropdown/index.vue';
import fileIcon from '../../vue_shared/components/file_icon.vue';
export default {
mixins: [
@ -11,6 +12,7 @@
components: {
skeletonLoadingContainer,
newDropdown,
fileIcon,
},
props: {
file: {
@ -26,13 +28,6 @@
...mapState([
'leftPanelCollapsed',
]),
fileIcon() {
return {
'fa-spinner fa-spin': this.file.loading,
[this.file.icon]: !this.file.loading,
'fa-folder-open': !this.file.loading && this.file.opened,
};
},
isSubmodule() {
return this.file.type === 'submodule';
},
@ -94,16 +89,18 @@
class="multi-file-table-name"
:colspan="submoduleColSpan"
>
<i
class="fa fa-fw file-icon"
:class="fileIcon"
:style="levelIndentation"
aria-hidden="true"
>
</i>
<a
class="repo-file-name"
>
<file-icon
:file-name="file.name"
:loading="file.loading"
:folder="file.type === 'tree'"
:opened="file.opened"
:style="levelIndentation"
:size="16"
>
</file-icon>
{{ file.name }}
</a>
<new-dropdown

View File

@ -1,5 +1,6 @@
<script>
import { mapActions } from 'vuex';
import fileIcon from '../../vue_shared/components/file_icon.vue';
export default {
props: {
@ -8,7 +9,9 @@ export default {
required: true,
},
},
components: {
fileIcon,
},
computed: {
closeLabel() {
if (this.tab.changed || this.tab.tempFile) {
@ -63,6 +66,11 @@ export default {
:class="{active : tab.active }"
:title="tab.url"
>
<file-icon
:file-name="tab.name"
:size="16"
>
</file-icon>
{{ tab.name }}
</div>
</li>

View File

@ -63,6 +63,10 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
}
};
export const setResizingStatus = ({ commit }, resizing) => {
commit(types.SET_RESIZING_STATUS, resizing);
};
export const checkCommitStatus = ({ state }) =>
service
.getBranchData(state.currentProjectId, state.currentBranchId)

View File

@ -5,6 +5,7 @@ export const SET_ROOT = 'SET_ROOT';
export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
// Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT';

View File

@ -49,6 +49,11 @@ export default {
rightPanelCollapsed: collapsed,
});
},
[types.SET_RESIZING_STATUS](state, resizing) {
Object.assign(state, {
panelResizing: resizing,
});
},
[types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
Object.assign(entry.lastCommit, {
id: lastCommit.commit.id,

View File

@ -19,4 +19,5 @@ export default () => ({
projects: {},
leftPanelCollapsed: false,
rightPanelCollapsed: true,
panelResizing: false,
});

View File

@ -1,5 +1,6 @@
/* eslint-disable no-new */
/* global MilestoneSelect */
import MilestoneSelect from './milestone_select';
import LabelsSelect from './labels_select';
import IssuableContext from './issuable_context';
import Sidebar from './right_sidebar';

View File

@ -1,9 +1,9 @@
/* eslint-disable no-new */
import LabelsSelect from './labels_select';
/* global MilestoneSelect */
import subscriptionSelect from './subscription_select';
import UsersSelect from './users_select';
import issueStatusSelect from './issue_status_select';
import MilestoneSelect from './milestone_select';
export default () => {
new UsersSelect();

View File

@ -1,8 +1,7 @@
/* eslint-disable class-methods-use-this, no-new */
/* global MilestoneSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import './milestone_select';
import MilestoneSelect from './milestone_select';
import issueStatusSelect from './issue_status_select';
import subscriptionSelect from './subscription_select';
import LabelsSelect from './labels_select';

View File

@ -172,8 +172,8 @@ export default {
},
updateIssuable() {
this.service.updateIssuable(this.store.formState)
.then(res => res.json())
return this.service.updateIssuable(this.store.formState)
.then(res => res.data)
.then(data => this.checkForSpam(data))
.then((data) => {
if (location.pathname !== data.web_url) {
@ -182,7 +182,7 @@ export default {
return this.service.getData();
})
.then(res => res.json())
.then(res => res.data)
.then((data) => {
this.store.updateState(data);
eventHub.$emit('close.form');
@ -207,7 +207,7 @@ export default {
deleteIssuable() {
this.service.deleteIssuable()
.then(res => res.json())
.then(res => res.data)
.then((data) => {
// Stop the poll so we don't get 404's with the issuable not existing
this.poll.stop();
@ -225,7 +225,7 @@ export default {
this.poll = new Poll({
resource: this.service,
method: 'getData',
successCallback: res => res.json().then(data => this.store.updateState(data)),
successCallback: res => this.store.updateState(res.data),
errorCallback(err) {
throw new Error(err);
},

View File

@ -1,29 +1,20 @@
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
import axios from '../../lib/utils/axios_utils';
export default class Service {
constructor(endpoint) {
this.endpoint = endpoint;
this.resource = Vue.resource(`${this.endpoint}.json`, {}, {
realtimeChanges: {
method: 'GET',
url: `${this.endpoint}/realtime_changes`,
},
});
this.endpoint = `${endpoint}.json`;
this.realtimeEndpoint = `${endpoint}/realtime_changes`;
}
getData() {
return this.resource.realtimeChanges();
return axios.get(this.realtimeEndpoint);
}
deleteIssuable() {
return this.resource.delete();
return axios.delete(this.endpoint);
}
updateIssuable(data) {
return this.resource.update(data);
return axios.put(this.endpoint, data);
}
}

View File

@ -1,7 +1,7 @@
import _ from 'underscore';
import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints';
import { bytesToKiB } from './lib/utils/number_utils';
import { numberToHumanSize } from './lib/utils/number_utils';
import { setCiStatusFavicon } from './lib/utils/common_utils';
import { timeFor } from './lib/utils/datetime_utility';
@ -96,14 +96,15 @@ export default class Job {
// eslint-disable-next-line class-methods-use-this
canScroll() {
return this.$document.height() > this.$window.height();
return $(document).height() > $(window).height();
}
toggleScroll() {
const currentPosition = this.$document.scrollTop();
const scrollHeight = this.$document.height();
const $document = $(document);
const currentPosition = $document.scrollTop();
const scrollHeight = $document.height();
const windowHeight = this.$window.height();
const windowHeight = $(window).height();
if (this.canScroll()) {
if (currentPosition > 0 &&
(scrollHeight - currentPosition !== windowHeight)) {
@ -127,18 +128,22 @@ export default class Job {
this.toggleDisableButton(this.$scrollBottomBtn, true);
}
}
// eslint-disable-next-line class-methods-use-this
isScrolledToBottom() {
const currentPosition = this.$document.scrollTop();
const scrollHeight = this.$document.height();
const $document = $(document);
const currentPosition = $document.scrollTop();
const scrollHeight = $document.height();
const windowHeight = $(window).height();
const windowHeight = this.$window.height();
return scrollHeight - currentPosition === windowHeight;
}
// eslint-disable-next-line class-methods-use-this
scrollDown() {
this.$document.scrollTop(this.$document.height());
const $document = $(document);
$document.scrollTop($document.height());
}
scrollToBottom() {
@ -148,7 +153,7 @@ export default class Job {
}
scrollToTop() {
this.$document.scrollTop(0);
$(document).scrollTop(0);
this.hasBeenScrolled = true;
this.toggleScroll();
}
@ -193,7 +198,7 @@ export default class Job {
// we need to show a message warning the user about that.
if (this.logBytes < log.total) {
// size is in bytes, we need to calculate KiB
const size = bytesToKiB(this.logBytes);
const size = numberToHumanSize(this.logBytes);
$('.js-truncated-info-size').html(`${size}`);
this.$truncatedInfo.removeClass('hidden');
} else {

View File

@ -2,6 +2,8 @@ import axios from 'axios';
import csrf from './csrf';
axios.defaults.headers.common[csrf.headerKey] = csrf.token;
// Used by Rails to check if it is a valid XHR request
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
// Maintain a global counter for active requests
// see: spec/support/wait_for_requests.rb

View File

@ -232,7 +232,7 @@ export const nodeMatchesSelector = (node, selector) => {
export const normalizeHeaders = (headers) => {
const upperCaseHeaders = {};
Object.keys(headers).forEach((e) => {
Object.keys(headers || {}).forEach((e) => {
upperCaseHeaders[e.toUpperCase()] = headers[e];
});

View File

@ -18,7 +18,7 @@ export function getParameterValues(sParam) {
// @param {String} url
export function mergeUrlParams(params, url) {
let newUrl = Object.keys(params).reduce((acc, paramName) => {
const paramValue = params[paramName];
const paramValue = encodeURIComponent(params[paramName]);
const pattern = new RegExp(`\\b(${paramName}=).*?(&|$)`);
if (paramValue === null) {

View File

@ -1,237 +1,228 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
/* global Issuable */
/* global ListMilestone */
import _ from 'underscore';
import { timeFor } from './lib/utils/datetime_utility';
(function() {
this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject, els, options = {}) {
var _this, $els;
if (currentProject != null) {
_this = this;
this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
}
$els = $(els);
if (!els) {
$els = $('.js-milestone-select');
}
$els.each(function(i, dropdown) {
var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, defaultNo, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, selectedMilestoneDefault, showAny, showNo, showUpcoming, showStarted, useId, showMenuAbove;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
milestonesUrl = $dropdown.data('milestones');
issueUpdateURL = $dropdown.data('issueUpdate');
showNo = $dropdown.data('show-no');
showAny = $dropdown.data('show-any');
showMenuAbove = $dropdown.data('showMenuAbove');
showUpcoming = $dropdown.data('show-upcoming');
showStarted = $dropdown.data('show-started');
useId = $dropdown.data('use-id');
defaultLabel = $dropdown.data('default-label');
defaultNo = $dropdown.data('default-no');
issuableId = $dropdown.data('issuable-id');
abilityName = $dropdown.data('ability-name');
$selectbox = $dropdown.closest('.selectbox');
$block = $selectbox.closest('.block');
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
selectedMilestoneDefault = (showAny ? '' : null);
selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault);
selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
if (issueUpdateURL) {
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>';
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>');
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
return $.ajax({
url: milestonesUrl
}).done(function(data) {
var extraOptions = [];
if (showAny) {
extraOptions.push({
id: 0,
name: '',
title: 'Any Milestone'
});
}
if (showNo) {
extraOptions.push({
id: -1,
name: 'No Milestone',
title: 'No Milestone'
});
}
if (showUpcoming) {
extraOptions.push({
id: -2,
name: '#upcoming',
title: 'Upcoming'
});
}
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
title: 'Started'
});
}
if (extraOptions.length) {
extraOptions.push('divider');
}
callback(extraOptions.concat(data));
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
$(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
});
},
renderRow: function(milestone) {
return `
<li data-milestone-id="${milestone.name}">
<a href='#' class='dropdown-menu-milestone-link'>
${_.escape(milestone.title)}
</a>
</li>
`;
},
filterable: true,
search: {
fields: ['title']
},
selectable: true,
toggleLabel: function(selected, el, e) {
if (selected && 'id' in selected && $(el).hasClass('is-active')) {
return selected.title;
} else {
return defaultLabel;
}
},
defaultLabel: defaultLabel,
fieldName: $dropdown.data('field-name'),
text: function(milestone) {
return _.escape(milestone.title);
},
id: function(milestone) {
if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name;
} else {
return milestone.id;
}
},
isSelected: function(milestone) {
return milestone.name === selectedMilestone;
},
hidden: function() {
$selectbox.hide();
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
opened: function(e) {
const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
}
$('a.is-active', $el).removeClass('is-active');
$(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(clickEvent) {
const { $el, e } = clickEvent;
let selected = clickEvent.selectedObj;
var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
if (!selected) return;
if (options.handleClick) {
e.preventDefault();
options.handleClick(selected);
return;
}
page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
isSelecting = (selected.name !== selectedMilestone);
selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
return;
}
if ($dropdown.closest('.add-issues-modal').length) {
boardsStore = gl.issueBoards.ModalStore.store.filter;
}
if (boardsStore) {
boardsStore[$dropdown.data('field-name')] = selected.name;
e.preventDefault();
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (selected.id !== -1 && isSelecting) {
gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
id: selected.id,
title: selected.name
}));
} else {
gl.issueBoards.boardStoreIssueDelete('milestone');
}
$dropdown.trigger('loading.gl.dropdown');
$loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
})
.catch(() => {
$loading.fadeOut();
});
} else {
selected = $selectbox.find('input[type="hidden"]').val();
data = {};
data[abilityName] = {};
data[abilityName].milestone_id = selected != null ? selected : null;
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
url: issueUpdateURL,
data: data
}).done(function(data) {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
$selectbox.hide();
$value.css('display', '');
if (data.milestone != null) {
data.milestone.full_path = _this.currentProject.full_path;
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
} else {
$value.html(milestoneLinkNoneTemplate);
return $sidebarCollapsedValue.find('span').text('No');
}
});
}
}
});
});
export default class MilestoneSelect {
constructor(currentProject, els, options = {}) {
if (currentProject !== null) {
this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
}
return MilestoneSelect;
})();
}).call(window);
this.init(els, options);
}
init(els, options) {
let $els = $(els);
if (!els) {
$els = $('.js-milestone-select');
}
$els.each((i, dropdown) => {
let collapsedSidebarLabelTemplate, milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
const $dropdown = $(dropdown);
const projectId = $dropdown.data('project-id');
const milestonesUrl = $dropdown.data('milestones');
const issueUpdateURL = $dropdown.data('issueUpdate');
const showNo = $dropdown.data('show-no');
const showAny = $dropdown.data('show-any');
const showMenuAbove = $dropdown.data('showMenuAbove');
const showUpcoming = $dropdown.data('show-upcoming');
const showStarted = $dropdown.data('show-started');
const useId = $dropdown.data('use-id');
const defaultLabel = $dropdown.data('default-label');
const defaultNo = $dropdown.data('default-no');
const issuableId = $dropdown.data('issuable-id');
const abilityName = $dropdown.data('ability-name');
const $selectBox = $dropdown.closest('.selectbox');
const $block = $selectBox.closest('.block');
const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
const $value = $block.find('.value');
const $loading = $block.find('.block-loading').fadeOut();
selectedMilestoneDefault = (showAny ? '' : null);
selectedMilestoneDefault = (showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault);
selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
if (issueUpdateURL) {
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>';
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>');
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: (term, callback) => $.ajax({
url: milestonesUrl
}).done((data) => {
const extraOptions = [];
if (showAny) {
extraOptions.push({
id: 0,
name: '',
title: 'Any Milestone'
});
}
if (showNo) {
extraOptions.push({
id: -1,
name: 'No Milestone',
title: 'No Milestone'
});
}
if (showUpcoming) {
extraOptions.push({
id: -2,
name: '#upcoming',
title: 'Upcoming'
});
}
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
title: 'Started'
});
}
if (extraOptions.length) {
extraOptions.push('divider');
}
callback(extraOptions.concat(data));
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
$(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
}),
renderRow: milestone => `
<li data-milestone-id="${milestone.name}">
<a href='#' class='dropdown-menu-milestone-link'>
${_.escape(milestone.title)}
</a>
</li>
`,
filterable: true,
search: {
fields: ['title']
},
selectable: true,
toggleLabel: (selected, el, e) => {
if (selected && 'id' in selected && $(el).hasClass('is-active')) {
return selected.title;
} else {
return defaultLabel;
}
},
defaultLabel: defaultLabel,
fieldName: $dropdown.data('field-name'),
text: milestone => _.escape(milestone.title),
id: (milestone) => {
if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name;
} else {
return milestone.id;
}
},
isSelected: milestone => milestone.name === selectedMilestone,
hidden: () => {
$selectBox.hide();
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
opened: (e) => {
const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
}
$('a.is-active', $el).removeClass('is-active');
$(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: (clickEvent) => {
const { $el, e } = clickEvent;
let selected = clickEvent.selectedObj;
let data, boardsStore;
if (!selected) return;
if (options.handleClick) {
e.preventDefault();
options.handleClick(selected);
return;
}
const page = $('body').attr('data-page');
const isIssueIndex = page === 'projects:issues:index';
const isMRIndex = (page === page && page === 'projects:merge_requests:index');
const isSelecting = (selected.name !== selectedMilestone);
selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
return;
}
if ($dropdown.closest('.add-issues-modal').length) {
boardsStore = gl.issueBoards.ModalStore.store.filter;
}
if (boardsStore) {
boardsStore[$dropdown.data('field-name')] = selected.name;
e.preventDefault();
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (selected.id !== -1 && isSelecting) {
gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
id: selected.id,
title: selected.name
}));
} else {
gl.issueBoards.boardStoreIssueDelete('milestone');
}
$dropdown.trigger('loading.gl.dropdown');
$loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(() => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
})
.catch(() => {
$loading.fadeOut();
});
} else {
selected = $selectBox.find('input[type="hidden"]').val();
data = {};
data[abilityName] = {};
data[abilityName].milestone_id = selected != null ? selected : null;
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
url: issueUpdateURL,
data: data
}).done((data) => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
$selectBox.hide();
$value.css('display', '');
if (data.milestone != null) {
data.milestone.full_path = this.currentProject.full_path;
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
} else {
$value.html(milestoneLinkNoneTemplate);
return $sidebarCollapsedValue.find('span').text('No');
}
});
}
}
});
});
}
}

View File

@ -1,8 +1,18 @@
import { timeFormat as time } from 'd3-time-format';
import { timeSecond, timeMinute, timeHour, timeDay, timeMonth, timeYear } from 'd3-time';
import { timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth, timeYear } from 'd3-time';
import { bisector } from 'd3-array';
const d3 = { time, bisector, timeSecond, timeMinute, timeHour, timeDay, timeMonth, timeYear };
const d3 = {
time,
bisector,
timeSecond,
timeMinute,
timeHour,
timeDay,
timeWeek,
timeMonth,
timeYear,
};
export const dateFormat = d3.time('%b %-d, %Y');
export const timeFormat = d3.time('%-I:%M%p');

View File

@ -1,4 +1,5 @@
/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
import Cookies from 'js-cookie';
import Flash from '../flash';
import { getPagePath } from '../lib/utils/common_utils';
@ -7,6 +8,8 @@ import { getPagePath } from '../lib/utils/common_utils';
constructor({ form } = {}) {
this.onSubmitForm = this.onSubmitForm.bind(this);
this.form = form || $('.edit-user');
this.newRepoActivated = Cookies.get('new_repo');
this.setRepoRadio();
this.bindEvents();
this.initAvatarGlCrop();
}
@ -25,6 +28,7 @@ import { getPagePath } from '../lib/utils/common_utils';
bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
$('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie);
$('#user_notification_email').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
$('.update-username').on('ajax:before', this.beforeUpdateUsername);
@ -82,6 +86,23 @@ import { getPagePath } from '../lib/utils/common_utils';
}
});
}
setNewRepoCookie() {
if (this.value === 'off') {
Cookies.remove('new_repo');
} else {
Cookies.set('new_repo', true, { expires_in: 365 });
}
}
setRepoRadio() {
const multiEditRadios = $('input[name="user[multi_file]"]');
if (this.newRepoActivated || this.newRepoActivated === 'true') {
multiEditRadios.filter('[value=on]').prop('checked', true);
} else {
multiEditRadios.filter('[value=off]').prop('checked', true);
}
}
}
$(function() {

View File

@ -34,10 +34,10 @@ export default {
if (isConfirmed) {
MRWidgetService.stopEnvironment(deployment.stop_url)
.then(res => res.json())
.then((res) => {
if (res.redirect_url) {
visitUrl(res.redirect_url);
.then(res => res.data)
.then((data) => {
if (data.redirect_url) {
visitUrl(data.redirect_url);
}
})
.catch(() => {

View File

@ -102,11 +102,11 @@ export default {
return res;
}
return res.json();
return res.data;
})
.then((res) => {
this.computeGraphData(res.metrics, res.deployment_time);
return res;
.then((data) => {
this.computeGraphData(data.metrics, data.deployment_time);
return data;
})
.catch(() => {
this.loadFailed = true;

View File

@ -16,9 +16,9 @@ export default {
<div class="media-body">
<mr-widget-author-and-time
actionText="Closed by"
:author="mr.closedEvent.author"
:dateTitle="mr.closedEvent.updatedAt"
:dateReadable="mr.closedEvent.formattedUpdatedAt"
:author="mr.metrics.closedBy"
:dateTitle="mr.metrics.closedAt"
:dateReadable="mr.metrics.readableClosedAt"
/>
<section class="mr-info-list">
<p>

View File

@ -31,9 +31,9 @@ export default {
cancelAutomaticMerge() {
this.isCancellingAutoMerge = true;
this.service.cancelAutomaticMerge()
.then(res => res.json())
.then((res) => {
eventHub.$emit('UpdateWidgetData', res);
.then(res => res.data)
.then((data) => {
eventHub.$emit('UpdateWidgetData', data);
})
.catch(() => {
this.isCancellingAutoMerge = false;
@ -49,9 +49,9 @@ export default {
this.isRemovingSourceBranch = true;
this.service.mergeResource.save(options)
.then(res => res.json())
.then((res) => {
if (res.status === 'merge_when_pipeline_succeeds') {
.then(res => res.data)
.then((data) => {
if (data.status === 'merge_when_pipeline_succeeds') {
eventHub.$emit('MRWidgetUpdateRequested');
}
})

View File

@ -47,9 +47,9 @@ export default {
removeSourceBranch() {
this.isMakingRequest = true;
this.service.removeSourceBranch()
.then(res => res.json())
.then((res) => {
if (res.message === 'Branch was removed') {
.then(res => res.data)
.then((data) => {
if (data.message === 'Branch was removed') {
eventHub.$emit('MRWidgetUpdateRequested', () => {
this.isMakingRequest = false;
});
@ -68,9 +68,9 @@ export default {
<div class="space-children">
<mr-widget-author-and-time
actionText="Merged by"
:author="mr.mergedEvent.author"
:date-title="mr.mergedEvent.updatedAt"
:date-readable="mr.mergedEvent.formattedUpdatedAt" />
:author="mr.metrics.mergedBy"
:date-title="mr.metrics.mergedAt"
:date-readable="mr.metrics.readableMergedAt" />
<a
v-if="mr.canRevertInCurrentMR"
v-tooltip

View File

@ -135,16 +135,16 @@ export default {
this.isMakingRequest = true;
this.service.merge(options)
.then(res => res.json())
.then((res) => {
const hasError = res.status === 'failed' || res.status === 'hook_validation_error';
.then(res => res.data)
.then((data) => {
const hasError = data.status === 'failed' || data.status === 'hook_validation_error';
if (res.status === 'merge_when_pipeline_succeeds') {
if (data.status === 'merge_when_pipeline_succeeds') {
eventHub.$emit('MRWidgetUpdateRequested');
} else if (res.status === 'success') {
} else if (data.status === 'success') {
this.initiateMergePolling();
} else if (hasError) {
eventHub.$emit('FailedToMerge', res.merge_error);
eventHub.$emit('FailedToMerge', data.merge_error);
}
})
.catch(() => {
@ -159,9 +159,9 @@ export default {
},
handleMergePolling(continuePolling, stopPolling) {
this.service.poll()
.then(res => res.json())
.then((res) => {
if (res.state === 'merged') {
.then(res => res.data)
.then((data) => {
if (data.state === 'merged') {
// If state is merged we should update the widget and stop the polling
eventHub.$emit('MRWidgetUpdateRequested');
eventHub.$emit('FetchActionsContent');
@ -174,11 +174,11 @@ export default {
// If user checked remove source branch and we didn't remove the branch yet
// we should start another polling for source branch remove process
if (this.removeSourceBranch && res.source_branch_exists) {
if (this.removeSourceBranch && data.source_branch_exists) {
this.initiateRemoveSourceBranchPolling();
}
} else if (res.merge_error) {
eventHub.$emit('FailedToMerge', res.merge_error);
} else if (data.merge_error) {
eventHub.$emit('FailedToMerge', data.merge_error);
stopPolling();
} else {
// MR is not merged yet, continue polling until the state becomes 'merged'
@ -199,11 +199,11 @@ export default {
},
handleRemoveBranchPolling(continuePolling, stopPolling) {
this.service.poll()
.then(res => res.json())
.then((res) => {
.then(res => res.data)
.then((data) => {
// If source branch exists then we should continue polling
// because removing a source branch is a background task and takes time
if (res.source_branch_exists) {
if (data.source_branch_exists) {
continuePolling();
} else {
// Branch is removed. Update widget, stop polling and hide the spinner

View File

@ -23,9 +23,9 @@ export default {
removeWIP() {
this.isMakingRequest = true;
this.service.removeWIP()
.then(res => res.json())
.then((res) => {
eventHub.$emit('UpdateWidgetData', res);
.then(res => res.data)
.then((data) => {
eventHub.$emit('UpdateWidgetData', data);
new window.Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line
$('.merge-request .detail-page-description .title').text(this.mr.title);
})

View File

@ -84,14 +84,14 @@ export default {
},
checkStatus(cb) {
return this.service.checkStatus()
.then(res => res.json())
.then((res) => {
this.handleNotification(res);
this.mr.setData(res);
.then(res => res.data)
.then((data) => {
this.handleNotification(data);
this.mr.setData(data);
this.setFaviconHelper();
if (cb) {
cb.call(null, res);
cb.call(null, data);
}
})
.catch(() => {
@ -124,10 +124,10 @@ export default {
},
fetchDeployments() {
return this.service.fetchDeployments()
.then(res => res.json())
.then((res) => {
if (res.length) {
this.mr.deployments = res;
.then(res => res.data)
.then((data) => {
if (data.length) {
this.mr.deployments = data;
}
})
.catch(() => {
@ -137,9 +137,9 @@ export default {
fetchActionsContent() {
this.service.fetchMergeActionsContent()
.then((res) => {
if (res.body) {
if (res.data) {
const el = document.createElement('div');
el.innerHTML = res.body;
el.innerHTML = res.data;
document.body.appendChild(el);
Project.initRefSwitcher();
}

View File

@ -1,57 +1,47 @@
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
import axios from '../../lib/utils/axios_utils';
export default class MRWidgetService {
constructor(endpoints) {
this.mergeResource = Vue.resource(endpoints.mergePath);
this.mergeCheckResource = Vue.resource(`${endpoints.statusPath}?serializer=widget`);
this.cancelAutoMergeResource = Vue.resource(endpoints.cancelAutoMergePath);
this.removeWIPResource = Vue.resource(endpoints.removeWIPPath);
this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath);
this.deploymentsResource = Vue.resource(endpoints.ciEnvironmentsStatusPath);
this.pollResource = Vue.resource(`${endpoints.statusPath}?serializer=basic`);
this.mergeActionsContentResource = Vue.resource(endpoints.mergeActionsContentPath);
this.endpoints = endpoints;
}
merge(data) {
return this.mergeResource.save(data);
return axios.post(this.endpoints.mergePath, data);
}
cancelAutomaticMerge() {
return this.cancelAutoMergeResource.save();
return axios.post(this.endpoints.cancelAutoMergePath);
}
removeWIP() {
return this.removeWIPResource.save();
return axios.post(this.endpoints.removeWIPPath);
}
removeSourceBranch() {
return this.removeSourceBranchResource.delete();
return axios.delete(this.endpoints.sourceBranchPath);
}
fetchDeployments() {
return this.deploymentsResource.get();
return axios.get(this.endpoints.ciEnvironmentsStatusPath);
}
poll() {
return this.pollResource.get();
return axios.get(`${this.endpoints.statusPath}?serializer=basic`);
}
checkStatus() {
return this.mergeCheckResource.get();
return axios.get(`${this.endpoints.statusPath}?serializer=widget`);
}
fetchMergeActionsContent() {
return this.mergeActionsContentResource.get();
return axios.get(this.endpoints.mergeActionsContentPath);
}
static stopEnvironment(url) {
return Vue.http.post(url);
return axios.post(url);
}
static fetchMetrics(metricsUrl) {
return Vue.http.get(`${metricsUrl}.json`);
return axios.get(`${metricsUrl}.json`);
}
}

View File

@ -39,9 +39,8 @@ export default class MergeRequestStore {
}
this.updatedAt = data.updated_at;
this.mergedEvent = MergeRequestStore.getEventObject(data.merge_event);
this.closedEvent = MergeRequestStore.getEventObject(data.closed_event);
this.setToMWPSBy = MergeRequestStore.getAuthorObject({ author: data.merge_user || {} });
this.metrics = MergeRequestStore.buildMetrics(data.metrics);
this.setToMWPSBy = MergeRequestStore.formatUserObject(data.merge_user || {});
this.mergeUserId = data.merge_user_id;
this.currentUserId = gon.current_user_id;
this.sourceBranchPath = data.source_branch_path;
@ -125,43 +124,42 @@ export default class MergeRequestStore {
return this.state === stateKey.nothingToMerge;
}
static getEventObject(event) {
return {
author: MergeRequestStore.getAuthorObject(event),
updatedAt: formatDate(MergeRequestStore.getEventUpdatedAtDate(event)),
formattedUpdatedAt: MergeRequestStore.getEventDate(event),
};
}
static getAuthorObject(event) {
if (!event) {
static buildMetrics(metrics) {
if (!metrics) {
return {};
}
return {
name: event.author.name || '',
username: event.author.username || '',
webUrl: event.author.web_url || '',
avatarUrl: event.author.avatar_url || '',
mergedBy: MergeRequestStore.formatUserObject(metrics.merged_by),
closedBy: MergeRequestStore.formatUserObject(metrics.closed_by),
mergedAt: formatDate(metrics.merged_at),
closedAt: formatDate(metrics.closed_at),
readableMergedAt: MergeRequestStore.getReadableDate(metrics.merged_at),
readableClosedAt: MergeRequestStore.getReadableDate(metrics.closed_at),
};
}
static getEventUpdatedAtDate(event) {
if (!event) {
return '';
static formatUserObject(user) {
if (!user) {
return {};
}
return event.updated_at;
return {
name: user.name || '',
username: user.username || '',
webUrl: user.web_url || '',
avatarUrl: user.avatar_url || '',
};
}
static getEventDate(event) {
const timeagoInstance = new Timeago();
if (!event) {
static getReadableDate(date) {
if (!date) {
return '';
}
return timeagoInstance.format(MergeRequestStore.getEventUpdatedAtDate(event));
const timeagoInstance = new Timeago();
return timeagoInstance.format(date);
}
}

View File

@ -0,0 +1,92 @@
<script>
import getIconForFile from './file_icon/file_icon_map';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import icon from '../../vue_shared/components/icon.vue';
/* This is a re-usable vue component for rendering a svg sprite
icon
Sample configuration:
<file-icon
name="retry"
:size="32"
css-classes="top"
/>
*/
export default {
props: {
fileName: {
type: String,
required: true,
},
folder: {
type: Boolean,
required: false,
default: false,
},
opened: {
type: Boolean,
required: false,
default: false,
},
loading: {
type: Boolean,
required: false,
default: false,
},
size: {
type: Number,
required: false,
default: 16,
},
cssClasses: {
type: String,
required: false,
default: '',
},
},
components: {
loadingIcon,
icon,
},
computed: {
spriteHref() {
const iconName = getIconForFile(this.fileName) || 'file';
return `${gon.sprite_file_icons}#${iconName}`;
},
folderIconName() {
// We don't have a open folder icon yet
return this.opened ? 'folder' : 'folder';
},
iconSizeClass() {
return this.size ? `s${this.size}` : '';
},
},
};
</script>
<template>
<span>
<svg
:class="[iconSizeClass, cssClasses]"
v-if="!loading && !folder">
<use
v-bind="{'xlink:href':spriteHref}"/>
</svg>
<icon
v-if="!loading && folder"
:name="folderIconName"
:size="size"
/>
<loading-icon
v-if="loading"
:inline="true"
/>
</span>
</template>

View File

@ -0,0 +1,589 @@
const fileExtensionIcons = {
html: 'html',
htm: 'html',
html_vm: 'html',
asp: 'html',
jade: 'pug',
pug: 'pug',
md: 'markdown',
'md.rendered': 'markdown',
markdown: 'markdown',
'markdown.rendered': 'markdown',
rst: 'markdown',
blink: 'blink',
css: 'css',
scss: 'sass',
sass: 'sass',
less: 'less',
json: 'json',
yaml: 'yaml',
'YAML-tmLanguage': 'yaml',
yml: 'yaml',
xml: 'xml',
plist: 'xml',
xsd: 'xml',
dtd: 'xml',
xsl: 'xml',
xslt: 'xml',
resx: 'xml',
iml: 'xml',
xquery: 'xml',
tmLanguage: 'xml',
manifest: 'xml',
project: 'xml',
png: 'image',
jpeg: 'image',
jpg: 'image',
gif: 'image',
svg: 'image',
ico: 'image',
tif: 'image',
tiff: 'image',
psd: 'image',
psb: 'image',
ami: 'image',
apx: 'image',
bmp: 'image',
bpg: 'image',
brk: 'image',
cur: 'image',
dds: 'image',
dng: 'image',
exr: 'image',
fpx: 'image',
gbr: 'image',
img: 'image',
jbig2: 'image',
jb2: 'image',
jng: 'image',
jxr: 'image',
pbm: 'image',
pgf: 'image',
pic: 'image',
raw: 'image',
webp: 'image',
js: 'javascript',
ejs: 'javascript',
esx: 'javascript',
jsx: 'react',
tsx: 'react',
ini: 'settings',
dlc: 'settings',
dll: 'settings',
config: 'settings',
conf: 'settings',
properties: 'settings',
prop: 'settings',
settings: 'settings',
option: 'settings',
props: 'settings',
toml: 'settings',
prefs: 'settings',
'sln.dotsettings': 'settings',
'sln.dotsettings.user': 'settings',
ts: 'typescript',
'd.ts': 'typescript-def',
marko: 'markojs',
pdf: 'pdf',
xlsx: 'table',
xls: 'table',
csv: 'table',
tsv: 'table',
vscodeignore: 'vscode',
vsixmanifest: 'vscode',
vsix: 'vscode',
'code-workplace': 'vscode',
suo: 'visualstudio',
sln: 'visualstudio',
csproj: 'visualstudio',
vb: 'visualstudio',
pdb: 'database',
sql: 'database',
pks: 'database',
pkb: 'database',
accdb: 'database',
mdb: 'database',
sqlite: 'database',
cs: 'csharp',
zip: 'zip',
tar: 'zip',
gz: 'zip',
xz: 'zip',
bzip2: 'zip',
gzip: 'zip',
'7z': 'zip',
rar: 'zip',
tgz: 'zip',
exe: 'exe',
msi: 'exe',
java: 'java',
jar: 'java',
jsp: 'java',
c: 'c',
m: 'c',
h: 'h',
cc: 'cpp',
cpp: 'cpp',
mm: 'cpp',
cxx: 'cpp',
hpp: 'hpp',
go: 'go',
py: 'python',
url: 'url',
sh: 'console',
ksh: 'console',
csh: 'console',
tcsh: 'console',
zsh: 'console',
bash: 'console',
bat: 'console',
cmd: 'console',
ps1: 'powershell',
psm1: 'powershell',
psd1: 'powershell',
ps1xml: 'powershell',
psc1: 'powershell',
pssc: 'powershell',
gradle: 'gradle',
doc: 'word',
docx: 'word',
rtf: 'word',
cer: 'certificate',
cert: 'certificate',
crt: 'certificate',
pub: 'key',
key: 'key',
pem: 'key',
asc: 'key',
gpg: 'key',
woff: 'font',
woff2: 'font',
ttf: 'font',
eot: 'font',
suit: 'font',
otf: 'font',
bmap: 'font',
fnt: 'font',
odttf: 'font',
ttc: 'font',
font: 'font',
fonts: 'font',
sui: 'font',
ntf: 'font',
mrf: 'font',
lib: 'lib',
bib: 'lib',
rb: 'ruby',
erb: 'ruby',
fs: 'fsharp',
fsx: 'fsharp',
fsi: 'fsharp',
fsproj: 'fsharp',
swift: 'swift',
ino: 'arduino',
dockerignore: 'docker',
dockerfile: 'docker',
tex: 'tex',
cls: 'tex',
sty: 'tex',
pptx: 'powerpoint',
ppt: 'powerpoint',
pptm: 'powerpoint',
potx: 'powerpoint',
pot: 'powerpoint',
potm: 'powerpoint',
ppsx: 'powerpoint',
ppsm: 'powerpoint',
pps: 'powerpoint',
ppam: 'powerpoint',
ppa: 'powerpoint',
webm: 'movie',
mkv: 'movie',
flv: 'movie',
vob: 'movie',
ogv: 'movie',
ogg: 'movie',
gifv: 'movie',
avi: 'movie',
mov: 'movie',
qt: 'movie',
wmv: 'movie',
yuv: 'movie',
rm: 'movie',
rmvb: 'movie',
mp4: 'movie',
m4v: 'movie',
mpg: 'movie',
mp2: 'movie',
mpeg: 'movie',
mpe: 'movie',
mpv: 'movie',
m2v: 'movie',
vdi: 'virtual',
vbox: 'virtual',
'vbox-prev': 'virtual',
ics: 'email',
mp3: 'music',
flac: 'music',
m4a: 'music',
wma: 'music',
aiff: 'music',
coffee: 'coffee',
txt: 'document',
graphql: 'graphql',
rs: 'rust',
raml: 'raml',
xaml: 'xaml',
hs: 'haskell',
kt: 'kotlin',
kts: 'kotlin',
patch: 'git',
lua: 'lua',
clj: 'clojure',
cljs: 'clojure',
groovy: 'groovy',
r: 'r',
rmd: 'r',
dart: 'dart',
as: 'actionscript',
mxml: 'mxml',
ahk: 'autohotkey',
swf: 'flash',
swc: 'swc',
cmake: 'cmake',
asm: 'assembly',
a51: 'assembly',
inc: 'assembly',
nasm: 'assembly',
s: 'assembly',
ms: 'assembly',
agc: 'assembly',
ags: 'assembly',
aea: 'assembly',
argus: 'assembly',
mitigus: 'assembly',
binsource: 'assembly',
vue: 'vue',
ml: 'ocaml',
mli: 'ocaml',
cmx: 'ocaml',
'js.map': 'javascript-map',
'css.map': 'css-map',
lock: 'lock',
hbs: 'handlebars',
mustache: 'handlebars',
pl: 'perl',
pm: 'perl',
hx: 'haxe',
'spec.ts': 'test-ts',
'test.ts': 'test-ts',
'ts.snap': 'test-ts',
'spec.tsx': 'test-jsx',
'test.tsx': 'test-jsx',
'tsx.snap': 'test-jsx',
'spec.jsx': 'test-jsx',
'test.jsx': 'test-jsx',
'jsx.snap': 'test-jsx',
'spec.js': 'test-js',
'test.js': 'test-js',
'js.snap': 'test-js',
'routing.ts': 'angular-routing',
'routing.js': 'angular-routing',
'module.ts': 'angular',
'module.js': 'angular',
'ng-template': 'angular',
'component.ts': 'angular-component',
'component.js': 'angular-component',
'guard.ts': 'angular-guard',
'guard.js': 'angular-guard',
'service.ts': 'angular-service',
'service.js': 'angular-service',
'pipe.ts': 'angular-pipe',
'pipe.js': 'angular-pipe',
'filter.js': 'angular-pipe',
'directive.ts': 'angular-directive',
'directive.js': 'angular-directive',
'resolver.ts': 'angular-resolver',
'resolver.js': 'angular-resolver',
pp: 'puppet',
ex: 'elixir',
exs: 'elixir',
ls: 'livescript',
erl: 'erlang',
twig: 'twig',
jl: 'julia',
elm: 'elm',
pure: 'purescript',
tpl: 'smarty',
styl: 'stylus',
re: 'reason',
rei: 'reason',
cmj: 'bucklescript',
merlin: 'merlin',
v: 'verilog',
vhd: 'verilog',
sv: 'verilog',
svh: 'verilog',
nb: 'mathematica',
wl: 'wolframlanguage',
wls: 'wolframlanguage',
njk: 'nunjucks',
nunjucks: 'nunjucks',
robot: 'robot',
sol: 'solidity',
au3: 'autoit',
haml: 'haml',
yang: 'yang',
tf: 'terraform',
'tf.json': 'terraform',
tfvars: 'terraform',
tfstate: 'terraform',
'blade.php': 'laravel',
'inky.php': 'laravel',
applescript: 'applescript',
cake: 'cake',
feature: 'cucumber',
nim: 'nim',
nimble: 'nim',
apib: 'apiblueprint',
apiblueprint: 'apiblueprint',
tag: 'riot',
vfl: 'vfl',
kl: 'kl',
pcss: 'postcss',
sss: 'postcss',
todo: 'todo',
cfml: 'coldfusion',
cfc: 'coldfusion',
lucee: 'coldfusion',
cabal: 'cabal',
nix: 'nix',
slim: 'slim',
http: 'http',
rest: 'http',
rql: 'restql',
restql: 'restql',
kv: 'kivy',
graphcool: 'graphcool',
sbt: 'sbt',
'reducer.ts': 'ngrx-reducer',
'rootReducer.ts': 'ngrx-reducer',
'state.ts': 'ngrx-state',
'actions.ts': 'ngrx-actions',
'effects.ts': 'ngrx-effects',
cr: 'crystal',
'drone.yml': 'drone',
cu: 'cuda',
cuh: 'cuda',
log: 'log',
};
const fileNameIcons = {
'.jscsrc': 'json',
'.jshintrc': 'json',
'tsconfig.json': 'json',
'tslint.json': 'json',
'composer.lock': 'json',
'.jsbeautifyrc': 'json',
'.esformatter': 'json',
'cdp.pid': 'json',
'.htaccess': 'xml',
'.jshintignore': 'settings',
'.buildignore': 'settings',
makefile: 'settings',
'.mrconfig': 'settings',
'.yardopts': 'settings',
'gradle.properties': 'gradle',
gradlew: 'gradle',
'gradle-wrapper.properties': 'gradle',
license: 'certificate',
'license.md': 'certificate',
'license.md.rendered': 'certificate',
'license.txt': 'certificate',
licence: 'certificate',
'licence.md': 'certificate',
'licence.md.rendered': 'certificate',
'licence.txt': 'certificate',
dockerfile: 'docker',
'docker-compose.yml': 'docker',
'.mailmap': 'email',
'.gitignore': 'git',
'.gitconfig': 'git',
'.gitattributes': 'git',
'.gitmodules': 'git',
'.gitkeep': 'git',
'git-history': 'git',
'.Rhistory': 'r',
'cmakelists.txt': 'cmake',
'cmakecache.txt': 'cmake',
'angular-cli.json': 'angular',
'.angular-cli.json': 'angular',
'.vfl': 'vfl',
'.kl': 'kl',
'postcss.config.js': 'postcss',
'.postcssrc.js': 'postcss',
'project.graphcool': 'graphcool',
'webpack.js': 'webpack',
'webpack.ts': 'webpack',
'webpack.base.js': 'webpack',
'webpack.base.ts': 'webpack',
'webpack.config.js': 'webpack',
'webpack.config.ts': 'webpack',
'webpack.common.js': 'webpack',
'webpack.common.ts': 'webpack',
'webpack.config.common.js': 'webpack',
'webpack.config.common.ts': 'webpack',
'webpack.config.common.babel.js': 'webpack',
'webpack.config.common.babel.ts': 'webpack',
'webpack.dev.js': 'webpack',
'webpack.dev.ts': 'webpack',
'webpack.config.dev.js': 'webpack',
'webpack.config.dev.ts': 'webpack',
'webpack.config.dev.babel.js': 'webpack',
'webpack.config.dev.babel.ts': 'webpack',
'webpack.prod.js': 'webpack',
'webpack.prod.ts': 'webpack',
'webpack.server.js': 'webpack',
'webpack.server.ts': 'webpack',
'webpack.client.js': 'webpack',
'webpack.client.ts': 'webpack',
'webpack.config.server.js': 'webpack',
'webpack.config.server.ts': 'webpack',
'webpack.config.client.js': 'webpack',
'webpack.config.client.ts': 'webpack',
'webpack.config.production.babel.js': 'webpack',
'webpack.config.production.babel.ts': 'webpack',
'webpack.config.prod.babel.js': 'webpack',
'webpack.config.prod.babel.ts': 'webpack',
'webpack.config.prod.js': 'webpack',
'webpack.config.prod.ts': 'webpack',
'webpack.config.production.js': 'webpack',
'webpack.config.production.ts': 'webpack',
'webpack.config.staging.js': 'webpack',
'webpack.config.staging.ts': 'webpack',
'webpack.config.babel.js': 'webpack',
'webpack.config.babel.ts': 'webpack',
'webpack.config.base.babel.js': 'webpack',
'webpack.config.base.babel.ts': 'webpack',
'webpack.config.base.js': 'webpack',
'webpack.config.base.ts': 'webpack',
'webpack.config.staging.babel.js': 'webpack',
'webpack.config.staging.babel.ts': 'webpack',
'webpack.config.coffee': 'webpack',
'webpack.config.test.js': 'webpack',
'webpack.config.test.ts': 'webpack',
'webpack.config.vendor.js': 'webpack',
'webpack.config.vendor.ts': 'webpack',
'webpack.config.vendor.production.js': 'webpack',
'webpack.config.vendor.production.ts': 'webpack',
'webpack.test.js': 'webpack',
'webpack.test.ts': 'webpack',
'webpack.dist.js': 'webpack',
'webpack.dist.ts': 'webpack',
'webpackfile.js': 'webpack',
'webpackfile.ts': 'webpack',
'ionic.config.json': 'ionic',
'.io-config.json': 'ionic',
'gulpfile.js': 'gulp',
'gulpfile.ts': 'gulp',
'gulpfile.babel.js': 'gulp',
'package.json': 'nodejs',
'package-lock.json': 'nodejs',
'.nvmrc': 'nodejs',
'.npmignore': 'npm',
'.npmrc': 'npm',
'.yarnrc': 'yarn',
'yarn.lock': 'yarn',
'.yarnclean': 'yarn',
'.yarn-integrity': 'yarn',
'yarn-error.log': 'yarn',
'androidmanifest.xml': 'android',
'.env': 'tune',
'.env.example': 'tune',
'.babelrc': 'babel',
'contributing.md': 'contributing',
'contributing.md.rendered': 'contributing',
'readme.md': 'readme',
'readme.md.rendered': 'readme',
changelog: 'changelog',
'changelog.md': 'changelog',
'changelog.md.rendered': 'changelog',
CREDITS: 'credits',
'credits.txt': 'credits',
'credits.md': 'credits',
'credits.md.rendered': 'credits',
'.flowconfig': 'flow',
'favicon.ico': 'favicon',
'karma.conf.js': 'karma',
'karma.conf.ts': 'karma',
'karma.conf.coffee': 'karma',
'karma.config.js': 'karma',
'karma.config.ts': 'karma',
'karma-main.js': 'karma',
'karma-main.ts': 'karma',
'.bithoundrc': 'bithound',
'appveyor.yml': 'appveyor',
'.travis.yml': 'travis',
'protractor.conf.js': 'protractor',
'protractor.conf.ts': 'protractor',
'protractor.conf.coffee': 'protractor',
'protractor.config.js': 'protractor',
'protractor.config.ts': 'protractor',
'fuse.js': 'fusebox',
procfile: 'heroku',
'.editorconfig': 'editorconfig',
'.gitlab-ci.yml': 'gitlab',
'.bowerrc': 'bower',
'bower.json': 'bower',
'.eslintrc.js': 'eslint',
'.eslintrc.yaml': 'eslint',
'.eslintrc.yml': 'eslint',
'.eslintrc.json': 'eslint',
'.eslintrc': 'eslint',
'.eslintignore': 'eslint',
'code_of_conduct.md': 'conduct',
'code_of_conduct.md.rendered': 'conduct',
'.watchmanconfig': 'watchman',
'aurelia.json': 'aurelia',
'mocha.opts': 'mocha',
jenkinsfile: 'jenkins',
'firebase.json': 'firebase',
'.firebaserc': 'firebase',
'rollup.config.js': 'rollup',
'rollup.config.ts': 'rollup',
'rollup-config.js': 'rollup',
'rollup-config.ts': 'rollup',
'rollup.config.prod.js': 'rollup',
'rollup.config.prod.ts': 'rollup',
'rollup.config.dev.js': 'rollup',
'rollup.config.dev.ts': 'rollup',
'rollup.config.prod.vendor.js': 'rollup',
'rollup.config.prod.vendor.ts': 'rollup',
'.hhconfig': 'hack',
'.stylelintrc': 'stylelint',
'stylelint.config.js': 'stylelint',
'.stylelintrc.json': 'stylelint',
'.stylelintrc.yaml': 'stylelint',
'.stylelintrc.yml': 'stylelint',
'.stylelintrc.js': 'stylelint',
'.stylelintignore': 'stylelint',
'.codeclimate.yml': 'code-climate',
'.prettierrc': 'prettier',
'prettier.config.js': 'prettier',
'.prettierrc.js': 'prettier',
'.prettierrc.json': 'prettier',
'.prettierrc.yaml': 'prettier',
'.prettierrc.yml': 'prettier',
'nodemon.json': 'nodemon',
'.sonarrc': 'sonar',
browserslist: 'browserlist',
'.browserslistrc': 'browserlist',
'.snyk': 'snyk',
'.drone.yml': 'drone',
};
export default function getIconForFile(name) {
return fileNameIcons[name] ||
fileExtensionIcons[name ? name.split('.').pop() : ''] ||
'';
}

View File

@ -0,0 +1,91 @@
<script>
export default {
props: {
startSize: {
type: Number,
required: true,
},
side: {
type: String,
required: true,
},
minSize: {
type: Number,
required: false,
default: 0,
},
maxSize: {
type: Number,
required: false,
default: Number.MAX_VALUE,
},
enabled: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
size: this.startSize,
};
},
computed: {
className() {
return `drag${this.side}`;
},
cursorStyle() {
if (this.enabled) {
return { cursor: 'ew-resize' };
}
return {};
},
},
methods: {
resetSize(e) {
e.preventDefault();
this.size = this.startSize;
this.$emit('update:size', this.size);
},
startDrag(e) {
if (this.enabled) {
e.preventDefault();
this.startPos = e.clientX;
this.currentStartSize = this.size;
document.addEventListener('mousemove', this.drag);
document.addEventListener('mouseup', this.endDrag, { once: true });
this.$emit('resize-start', this.size);
}
},
drag(e) {
e.preventDefault();
let moved = e.clientX - this.startPos;
if (this.side === 'left') moved = -moved;
let newSize = this.currentStartSize + moved;
if (newSize < this.minSize) {
newSize = this.minSize;
} else if (newSize > this.maxSize) {
newSize = this.maxSize;
}
this.size = newSize;
this.$emit('update:size', newSize);
},
endDrag(e) {
e.preventDefault();
document.removeEventListener('mousemove', this.drag);
this.$emit('resize-end', this.size);
},
},
};
</script>
<template>
<div
class="dragHandle"
:class="className"
:style="cursorStyle"
@mousedown="startDrag"
@dblclick="resetSize"
></div>
</template>

View File

@ -71,7 +71,7 @@
vertical-align: top;
&.s16 { font-size: 12px; line-height: 1.33; }
&.s24 { font-size: 14px; line-height: 1.8; }
&.s24 { font-size: 13px; line-height: 1.8; }
&.s26 { font-size: 20px; line-height: 1.33; }
&.s32 { font-size: 20px; line-height: 30px; }
&.s40 { font-size: 16px; line-height: 38px; }

View File

@ -10,7 +10,6 @@
color: $gl-text-color;
font-weight: $gl-font-weight-normal;
font-size: 14px;
line-height: 36px;
&.diff-collapsed {
padding: 5px;

View File

@ -23,6 +23,7 @@
.context-header {
position: relative;
margin-right: 2px;
width: $contextual-sidebar-width;
a {
transition: padding $sidebar-transition-duration;
@ -358,10 +359,6 @@
}
.sidebar-top-level-items > li {
&.active a {
padding-left: 12px;
}
.sidebar-sub-level-items {
&:not(.flyout-list) {
display: none;

View File

@ -516,7 +516,7 @@
.header-user {
.dropdown-menu-nav {
width: auto;
min-width: 140px;
min-width: 160px;
margin-top: 4px;
color: $gl-text-color;
left: auto;

View File

@ -12,6 +12,7 @@
padding: 10px 15px;
min-height: 20px;
border-bottom: 1px solid $list-border;
word-wrap: break-word;
&::after {
content: " ";
@ -125,10 +126,8 @@ ul.content-list {
}
.description {
p {
@include str-truncated;
margin-bottom: 0;
}
@include str-truncated;
color: $gl-text-color-secondary;
}
.controls {
@ -314,7 +313,7 @@ ul.indent-list {
border: 2px solid $white-normal;
&.identicon {
line-height: 30px;
line-height: 15px;
}
}
}
@ -348,14 +347,19 @@ ul.indent-list {
.folder-caret {
width: 15px;
svg {
margin-bottom: 2px;
}
}
.item-type-icon {
margin-top: 2px;
width: 20px;
}
> .group-row:not(.has-children) {
.folder-caret .fa {
.folder-caret {
opacity: 0;
}
}
@ -438,12 +442,61 @@ ul.indent-list {
.avatar-container > a {
width: 100%;
text-decoration: none;
}
&.has-more-items {
display: block;
padding: 20px 10px;
}
.stats {
position: relative;
line-height: 46px;
> span {
display: inline-flex;
align-items: center;
height: 16px;
min-width: 30px;
}
> span:last-child {
margin-right: 0;
}
.stat-value {
margin: 2px 0 0 5px;
}
}
.controls {
margin-left: 5px;
> .btn {
margin-right: $btn-xs-side-margin;
}
}
}
.project-row-contents .stats {
line-height: inherit;
> span:first-child {
margin-left: 25px;
}
.item-visibility {
margin-right: 0;
}
.last-updated {
position: absolute;
right: 12px;
min-width: 250px;
text-align: right;
color: $gl-text-color-secondary;
}
}
}
@ -455,12 +508,12 @@ ul.indent-list {
ul.group-list-tree {
li.group-row {
&.has-description .title {
line-height: inherit;
> .group-row-contents .title {
line-height: $list-text-height;
}
&:not(.has-description) .title {
line-height: $list-text-height;
&.has-description > .group-row-contents .title {
line-height: inherit;
}
}
}

View File

@ -178,6 +178,10 @@
font-weight: inherit;
}
dd {
margin-left: $gl-padding;
}
ul,
ol {
padding: 0;

View File

@ -727,3 +727,8 @@ Popup
$popup-triangle-size: 15px;
$popup-triangle-border-size: 1px;
$popup-box-shadow-color: rgba(90, 90, 90, 0.05);
/*
Multi file editor
*/
$border-color-settings: #e1e1e1;

View File

@ -159,7 +159,6 @@
}
}
/*
* Last push widget
*/
@ -182,6 +181,12 @@
.event-item {
padding-left: 0;
&.event-inline {
.event-title {
line-height: 20px;
}
}
.event-title {
white-space: normal;
overflow: visible;

View File

@ -20,6 +20,22 @@
}
}
.multi-file-editor-options {
label {
margin-right: 20px;
text-align: center;
}
.preview {
font-size: 0;
img {
border: 1px solid $border-color-settings;
border-radius: 4px;
}
}
}
.application-theme {
label {
margin-right: 20px;

View File

@ -36,10 +36,6 @@
}
}
.with-performance-bar .ide-view {
height: calc(100vh - #{$header-height});
}
.ide-file-list {
flex: 1;
@ -96,8 +92,14 @@
padding: 6px 12px;
}
.multi-file-table-name {
table.table tr td.multi-file-table-name {
width: 350px;
padding: 6px 12px;
svg {
vertical-align: middle;
margin-right: 2px;
}
}
.multi-file-table-col-commit-message {
@ -132,6 +134,10 @@
border-bottom: 1px solid $white-dark;
cursor: pointer;
svg {
vertical-align: middle;
}
&.active {
background-color: $white-light;
border-bottom-color: $white-light;
@ -232,12 +238,13 @@
.multi-file-commit-panel {
display: flex;
position: relative;
flex-direction: column;
height: 100%;
width: 290px;
padding: 0;
background-color: $gray-light;
border-left: 1px solid $white-dark;
padding-right: 3px;
.projects-sidebar {
display: flex;
@ -486,3 +493,30 @@
margin-top: $header-height;
margin-bottom: 0;
}
.with-performance-bar {
.ide-flash-container.flash-container {
margin-top: $header-height + $performance-bar-height;
}
.ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height});
}
}
.dragHandle {
position: absolute;
top: 0;
bottom: 0;
width: 3px;
background-color: $white-dark;
&.dragright {
right: 0;
}
&.dragleft {
left: 0;
}
}

View File

@ -268,3 +268,7 @@
margin: 0 0 5px 17px;
}
}
.deprecated-service {
cursor: default;
}

View File

@ -1,6 +1,8 @@
class PasswordsController < Devise::PasswordsController
include Gitlab::CurrentSettings
skip_before_action :require_no_authentication, only: [:edit, :update]
before_action :resource_from_email, only: [:create]
before_action :check_password_authentication_available, only: [:create]
before_action :throttle_reset, only: [:create]

View File

@ -46,14 +46,16 @@ class Projects::BranchesController < Projects::ApplicationController
result = CreateBranchService.new(project, current_user)
.execute(branch_name, ref)
if params[:issue_iid]
success = (result[:status] == :success)
if params[:issue_iid] && success
issue = IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:issue_iid])
SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue
end
respond_to do |format|
format.html do
if result[:status] == :success
if success
if redirect_to_autodeploy
redirect_to url_to_autodeploy_setup(project, branch_name),
notice: view_context.autodeploy_flash_notice(branch_name)
@ -67,7 +69,7 @@ class Projects::BranchesController < Projects::ApplicationController
end
format.json do
if result[:status] == :success
if success
render json: { name: branch_name, url: project_tree_url(@project, branch_name) }
else
render json: result[:messsage], status: :unprocessable_entity

View File

@ -29,17 +29,17 @@ class Projects::RunnersController < Projects::ApplicationController
def resume
if Ci::UpdateRunnerService.new(@runner).update(active: true)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
redirect_to runners_path(@project), notice: 'Runner was successfully updated.'
else
redirect_to runner_path(@runner), alert: 'Runner was not updated.'
redirect_to runners_path(@project), alert: 'Runner was not updated.'
end
end
def pause
if Ci::UpdateRunnerService.new(@runner).update(active: false)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
redirect_to runners_path(@project), notice: 'Runner was successfully updated.'
else
redirect_to runner_path(@runner), alert: 'Runner was not updated.'
redirect_to runners_path(@project), alert: 'Runner was not updated.'
end
end

View File

@ -30,6 +30,13 @@ module IconsHelper
ActionController::Base.helpers.image_path('icons.svg', host: sprite_base_url)
end
def sprite_file_icons_path
# SVG Sprites currently don't work across domains, so in the case of a CDN
# we have to set the current path deliberately to prevent addition of asset_host
sprite_base_url = Gitlab.config.gitlab.url if ActionController::Base.asset_host
ActionController::Base.helpers.image_path('file_icons.svg', host: sprite_base_url)
end
def sprite_icon(icon_name, size: nil, css_class: nil)
css_classes = size ? "s#{size}" : ""
css_classes << " #{css_class}" unless css_class.blank?

View File

@ -389,7 +389,7 @@ module ProjectsHelper
end
def add_special_file_path(project, file_name:, commit_message: nil, branch_name: nil, context: nil)
commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name.downcase }
commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name }
project_new_blob_path(
project,
project.default_branch || 'master',

View File

@ -27,5 +27,16 @@ module ServicesHelper
"#{event}_events"
end
def service_save_button(service)
button_tag(class: 'btn btn-save', type: 'submit', disabled: service.deprecated?) do
icon('spinner spin', class: 'hidden js-btn-spinner') +
content_tag(:span, 'Save changes', class: 'js-btn-label')
end
end
def disable_fields_service?(service)
!current_controller?("admin/services") && service.deprecated?
end
extend self
end

View File

@ -96,7 +96,7 @@ module Issuable
strip_attributes :title
after_save :record_metrics, unless: :imported?
after_save :ensure_metrics, unless: :imported?
# We want to use optimistic lock for cases when only title or description are involved
# http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
@ -335,11 +335,6 @@ module Issuable
false
end
def record_metrics
metrics = self.metrics || create_metrics
metrics.record!
end
##
# Override in issuable specialization
#
@ -347,6 +342,10 @@ module Issuable
false
end
def ensure_metrics
self.metrics || create_metrics
end
##
# Overriden in MergeRequest
#

View File

@ -34,6 +34,8 @@ module Storage
# So we basically we mute exceptions in next actions
begin
send_update_instructions
write_projects_repository_config
true
rescue
# Returning false does not rollback after_* transaction but gives

View File

@ -23,8 +23,13 @@ class DiffDiscussion < Discussion
def merge_request_version_params
return unless for_merge_request?
version_params = get_params
return version_params unless on_merge_request_commit? && commit_id
version_params ||= {}
version_params.tap do |params|
params[:commit_id] = commit_id if on_merge_request_commit?
params[:commit_id] = commit_id
end
end
@ -37,7 +42,7 @@ class DiffDiscussion < Discussion
private
def version_params
def get_params
return {} if active?
noteable.version_params_for(position.diff_refs)

View File

@ -48,7 +48,18 @@ class Event < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :project
belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :target, -> {
# If the association for "target" defines an "author" association we want to
# eager-load this so Banzai & friends don't end up performing N+1 queries to
# get the authors of notes, issues, etc.
if reflections['events'].active_record.reflect_on_association(:author)
includes(:author)
else
self
end
}, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
has_one :push_event_payload
# Callbacks

View File

@ -276,6 +276,11 @@ class Issue < ActiveRecord::Base
private
def ensure_metrics
super
metrics.record!
end
# Returns `true` if the given User can read the current Issue.
#
# This method duplicates the same check of issue_policy.rb

View File

@ -1,12 +1,6 @@
class MergeRequest::Metrics < ActiveRecord::Base
belongs_to :merge_request
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :pipeline_id
def record!
if merge_request.merged? && self.merged_at.blank?
self.merged_at = Time.now
end
self.save
end
belongs_to :latest_closed_by, class_name: 'User'
belongs_to :merged_by, class_name: 'User'
end

View File

@ -268,4 +268,11 @@ class Namespace < ActiveRecord::Base
def namespace_previously_created_with_same_path?
RedirectRoute.permanent.exists?(path: path)
end
def write_projects_repository_config
all_projects.find_each do |project|
project.expires_full_path_cache # we need to clear cache to validate renames correctly
project.write_repository_config
end
end
end

View File

@ -226,7 +226,7 @@ class Project < ActiveRecord::Base
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, :add_role, to: :team
# Validations
validates :creator, presence: true, on: :create
@ -639,7 +639,7 @@ class Project < ActiveRecord::Base
end
def import?
external_import? || forked? || gitlab_project_import?
external_import? || forked? || gitlab_project_import? || bare_repository_import?
end
def no_import?
@ -679,6 +679,10 @@ class Project < ActiveRecord::Base
Gitlab::UrlSanitizer.new(import_url).masked_url
end
def bare_repository_import?
import_type == 'bare_repository'
end
def gitlab_project_import?
import_type == 'gitlab_project'
end
@ -1416,6 +1420,8 @@ class Project < ActiveRecord::Base
end
def after_rename_repo
write_repository_config
path_before_change = previous_changes['path'].first
# We need to check if project had been rolled out to move resource to hashed storage or not and decide
@ -1428,6 +1434,16 @@ class Project < ActiveRecord::Base
Gitlab::PagesTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
end
def write_repository_config(gl_full_path: full_path)
# We'd need to keep track of project full path otherwise directory tree
# created with hashed storage enabled cannot be usefully imported using
# the import rake task.
repo.config['gitlab.fullpath'] = gl_full_path
rescue Gitlab::Git::Repository::NoRepository => e
Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.")
nil
end
def rename_repo_notify!
send_move_instructions(full_path_was)
expires_full_path_cache

View File

@ -31,6 +31,7 @@ class KubernetesService < DeploymentService
before_validation :enforce_namespace_to_lower_case
validate :deprecation_validation, unless: :template?
validates :namespace,
allow_blank: true,
length: 1..63,
@ -145,6 +146,17 @@ class KubernetesService < DeploymentService
@kubeclient ||= build_kubeclient!
end
def deprecated?
!active
end
def deprecation_message
content = <<-MESSAGE.strip_heredoc
Kubernetes service integration has been deprecated. #{deprecated_message_content} your clusters using the new <a href=\'#{Gitlab::Routing.url_helpers.project_clusters_path(project)}'/>Clusters</a> page
MESSAGE
content.html_safe
end
TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze
private
@ -226,4 +238,20 @@ class KubernetesService < DeploymentService
def enforce_namespace_to_lower_case
self.namespace = self.namespace&.downcase
end
def deprecation_validation
return if active_changed?(from: true, to: false)
if deprecated?
errors[:base] << deprecation_message
end
end
def deprecated_message_content
if active?
"Your cluster information on this page is still editable, but you are advised to disable and reconfigure"
else
"Fields on this page are now uneditable, you can configure"
end
end
end

View File

@ -7,36 +7,24 @@ class ProjectTeam
@project = project
end
# Shortcut to add users
#
# Use:
# @team << [@user, :master]
# @team << [@users, :master]
#
def <<(args)
users, access, current_user = *args
if users.respond_to?(:each)
add_users(users, access, current_user: current_user)
else
add_user(users, access, current_user: current_user)
end
end
def add_guest(user, current_user: nil)
self << [user, :guest, current_user]
add_user(user, :guest, current_user: current_user)
end
def add_reporter(user, current_user: nil)
self << [user, :reporter, current_user]
add_user(user, :reporter, current_user: current_user)
end
def add_developer(user, current_user: nil)
self << [user, :developer, current_user]
add_user(user, :developer, current_user: current_user)
end
def add_master(user, current_user: nil)
self << [user, :master, current_user]
add_user(user, :master, current_user: current_user)
end
def add_role(user, role, current_user: nil)
send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend
end
def find_member(user_id)

View File

@ -1010,10 +1010,6 @@ class Repository
raw_repository.fetch_source_branch!(source_repository.raw_repository, source_branch, local_ref)
end
def remote_exists?(name)
raw_repository.remote_exists?(name)
end
def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:)
raw_repository.compare_source_branch(target_branch_name, source_repository.raw_repository, source_branch_name, straight: straight)
end

View File

@ -263,6 +263,14 @@ class Service < ActiveRecord::Base
service
end
def deprecated?
false
end
def deprecation_message
nil
end
private
def cache_project_has_external_issue_tracker

View File

@ -94,8 +94,8 @@ class User < ActiveRecord::Base
has_one :user_synced_attributes_metadata, autosave: true
# Groups
has_many :members, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, source: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
has_many :members
has_many :group_members, -> { where(requested_at: nil) }, source: 'GroupMember'
has_many :groups, through: :group_members
has_many :owned_groups, -> { where members: { access_level: Gitlab::Access::OWNER } }, through: :group_members, source: :group
has_many :masters_groups, -> { where members: { access_level: Gitlab::Access::MASTER } }, through: :group_members, source: :group
@ -103,7 +103,7 @@ class User < ActiveRecord::Base
# Projects
has_many :groups_projects, through: :groups, source: :projects
has_many :personal_projects, through: :namespace, source: :projects
has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :project_members, -> { where(requested_at: nil) }
has_many :projects, through: :project_members
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@ -794,10 +794,7 @@ class User < ActiveRecord::Base
# `User.select(:id)` raises
# `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
# without this safeguard!
return unless has_attribute?(:projects_limit)
connection_default_value_defined = new_record? && !projects_limit_changed?
return unless projects_limit.nil? || connection_default_value_defined
return unless has_attribute?(:projects_limit) && projects_limit.nil?
self.projects_limit = current_application_settings.default_projects_limit
end

View File

@ -1,4 +0,0 @@
class EventEntity < Grape::Entity
expose :author, using: UserEntity
expose :updated_at
end

View File

@ -0,0 +1,6 @@
class MergeRequestMetricsEntity < Grape::Entity
expose :latest_closed_at, as: :closed_at
expose :merged_at
expose :latest_closed_by, as: :closed_by, using: UserEntity
expose :merged_by, using: UserEntity
end

View File

@ -17,9 +17,11 @@ class MergeRequestWidgetEntity < IssuableEntity
merge_request.project.merge_requests_ff_only_enabled
end
# Events
expose :merge_event, using: EventEntity
expose :closed_event, using: EventEntity
expose :metrics do |merge_request|
metrics = build_metrics(merge_request)
MergeRequestMetricsEntity.new(metrics).as_json
end
# User entities
expose :merge_user, using: UserEntity
@ -178,4 +180,27 @@ class MergeRequestWidgetEntity < IssuableEntity
@presenters ||= {}
@presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: current_user)
end
# Once SchedulePopulateMergeRequestMetricsWithEventsData fully runs,
# we can remove this method and just serialize MergeRequest#metrics
# instead. See https://gitlab.com/gitlab-org/gitlab-ce/issues/41587
def build_metrics(merge_request)
# There's no need to query and serialize metrics data for merge requests that are not
# merged or closed.
return unless merge_request.merged? || merge_request.closed?
return merge_request.metrics if merge_request.merged? && merge_request.metrics&.merged_by_id
return merge_request.metrics if merge_request.closed? && merge_request.metrics&.latest_closed_by_id
build_metrics_from_events(merge_request)
end
def build_metrics_from_events(merge_request)
closed_event = merge_request.closed_event
merge_event = merge_request.merge_event
MergeRequest::Metrics.new(latest_closed_at: closed_event&.updated_at,
latest_closed_by: closed_event&.author,
merged_at: merge_event&.updated_at,
merged_by: merge_event&.author)
end
end

View File

@ -103,6 +103,6 @@ class EventCreateService
author_id: current_user.id
)
Event.create(attributes)
Event.create!(attributes)
end
end

View File

@ -0,0 +1,19 @@
class MergeRequestMetricsService
delegate :update!, to: :@merge_request_metrics
def initialize(merge_request_metrics)
@merge_request_metrics = merge_request_metrics
end
def merge(event)
update!(merged_by_id: event.author_id, merged_at: event.created_at)
end
def close(event)
update!(latest_closed_by_id: event.author_id, latest_closed_at: event.created_at)
end
def reopen
update!(latest_closed_by_id: nil, latest_closed_at: nil)
end
end

View File

@ -24,6 +24,10 @@ module MergeRequests
private
def merge_request_metrics_service(merge_request)
MergeRequestMetricsService.new(merge_request.metrics)
end
def create_assignee_note(merge_request)
SystemNoteService.change_assignee(
merge_request, merge_request.project, current_user, merge_request.assignee)

View File

@ -8,7 +8,7 @@ module MergeRequests
merge_request.allow_broken = true
if merge_request.close
event_service.close_mr(merge_request, current_user)
create_event(merge_request)
create_note(merge_request)
notification_service.close_mr(merge_request, current_user)
todo_service.close_merge_request(merge_request, current_user)
@ -19,5 +19,16 @@ module MergeRequests
merge_request
end
private
def create_event(merge_request)
# Making sure MergeRequest::Metrics updates are in sync with
# Event creation.
Event.transaction do
close_event = event_service.close_mr(merge_request, current_user)
merge_request_metrics_service(merge_request).close(close_event)
end
end
end
end

View File

@ -23,7 +23,7 @@ module MergeRequests
# when there are no conflict files.
conflicts.files.each(&:lines)
@conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
rescue Rugged::OdbError, Gitlab::Git::Conflict::Parser::UnresolvableError, Gitlab::Git::Conflict::Resolver::ConflictSideMissing
rescue Gitlab::Git::CommandError, Gitlab::Git::Conflict::Parser::UnresolvableError, Gitlab::Git::Conflict::Resolver::ConflictSideMissing
@conflicts_can_be_resolved_in_ui = false
end
end

Some files were not shown because too many files have changed in this diff Show More