Merge branch 'master' into 41249-clearing-the-cache
This commit is contained in:
commit
9c9f7dc639
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
29
CHANGELOG.md
29
CHANGELOG.md
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.60.0
|
||||
0.65.0
|
||||
|
|
6
Gemfile
6
Gemfile
|
@ -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
|
||||
|
||||
|
|
18
Gemfile.lock
18
Gemfile.lock
|
@ -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 |
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 = [];
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -19,4 +19,5 @@ export default () => ({
|
|||
projects: {},
|
||||
leftPanelCollapsed: false,
|
||||
rightPanelCollapsed: true,
|
||||
panelResizing: false,
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
});
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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() : ''] ||
|
||||
'';
|
||||
}
|
|
@ -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>
|
|
@ -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; }
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
color: $gl-text-color;
|
||||
font-weight: $gl-font-weight-normal;
|
||||
font-size: 14px;
|
||||
line-height: 36px;
|
||||
|
||||
&.diff-collapsed {
|
||||
padding: 5px;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -178,6 +178,10 @@
|
|||
font-weight: inherit;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left: $gl-padding;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding: 0;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -268,3 +268,7 @@
|
|||
margin: 0 0 5px 17px;
|
||||
}
|
||||
}
|
||||
|
||||
.deprecated-service {
|
||||
cursor: default;
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
#
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
class EventEntity < Grape::Entity
|
||||
expose :author, using: UserEntity
|
||||
expose :updated_at
|
||||
end
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -103,6 +103,6 @@ class EventCreateService
|
|||
author_id: current_user.id
|
||||
)
|
||||
|
||||
Event.create(attributes)
|
||||
Event.create!(attributes)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue