Merge branch 'master' into dz-merge-request-version
Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
This commit is contained in:
commit
6db65143db
53
CHANGELOG
53
CHANGELOG
|
@ -1,13 +1,17 @@
|
|||
Please view this file on the master branch, on stable branches it's out of date.
|
||||
|
||||
v 8.11.0 (unreleased)
|
||||
- Add test coverage report badge. !5708
|
||||
- Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar)
|
||||
- Ability to specify branches for Pivotal Tracker integration (Egor Lynko)
|
||||
- Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres)
|
||||
- Add delimiter to project stars and forks count (ClemMakesApps)
|
||||
- Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres)
|
||||
- Fix the title of the toggle dropdown button. !5515 (herminiotorres)
|
||||
- Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz)
|
||||
- Update to Ruby 2.3.1. !4948
|
||||
- Add Issues Board !5548
|
||||
- Allow resolving merge conflicts in the UI !5479
|
||||
- Improve diff performance by eliminating redundant checks for text blobs
|
||||
- Ensure that branch names containing escapable characters (e.g. %20) aren't unescaped indiscriminately. !5770 (ewiltshi)
|
||||
- Convert switch icon into icon font (ClemMakesApps)
|
||||
|
@ -15,7 +19,9 @@ v 8.11.0 (unreleased)
|
|||
- API: List access requests, request access, approve, and deny access requests to a project or a group. !4833
|
||||
- Use long options for curl examples in documentation !5703 (winniehell)
|
||||
- Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell)
|
||||
- GitLab Performance Monitoring can now track custom events such as the number of tags pushed to a repository
|
||||
- Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell)
|
||||
- Allow naming U2F devices !5833
|
||||
- Ignore URLs starting with // in Markdown links !5677 (winniehell)
|
||||
- Fix CI status icon link underline (ClemMakesApps)
|
||||
- The Repository class is now instrumented
|
||||
|
@ -24,25 +30,37 @@ v 8.11.0 (unreleased)
|
|||
- Expand commit message width in repo view (ClemMakesApps)
|
||||
- Cache highlighted diff lines for merge requests
|
||||
- Pre-create all builds for a Pipeline when the new Pipeline is created !5295
|
||||
- Allow merge request diff notes and discussions to be explicitly marked as resolved
|
||||
- API: Add deployment endpoints
|
||||
- API: Add Play endpoint on Builds
|
||||
- Fix of 'Commits being passed to custom hooks are already reachable when using the UI'
|
||||
- Show member roles to all users on members page
|
||||
- Project.visible_to_user is instrumented again
|
||||
- Fix awardable button mutuality loading spinners (ClemMakesApps)
|
||||
- Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable
|
||||
- Optimize maximum user access level lookup in loading of notes
|
||||
- Send notification emails to users newly mentioned in issue and MR edits !5800
|
||||
- Add "No one can push" as an option for protected branches. !5081
|
||||
- Improve performance of AutolinkFilter#text_parse by using XPath
|
||||
- Add experimental Redis Sentinel support !1877
|
||||
- Rendering of SVGs as blobs is now limited to SVGs with a size smaller or equal to 2MB
|
||||
- Fix branches page dropdown sort initial state (ClemMakesApps)
|
||||
- Environments have an url to link to
|
||||
- Various redundant database indexes have been removed
|
||||
- Update `timeago` plugin to use multiple string/locale settings
|
||||
- Remove unused images (ClemMakesApps)
|
||||
- Get issue and merge request description templates from repositories
|
||||
- Add hover state to todos !5361 (winniehell)
|
||||
- Fix icon alignment of star and fork buttons !5451 (winniehell)
|
||||
- Enforce 2FA restrictions on API authentication endpoints !5820
|
||||
- Limit git rev-list output count to one in forced push check
|
||||
- Show deployment status on merge requests with external URLs
|
||||
- Clean up unused routes (Josef Strzibny)
|
||||
- Fix issue on empty project to allow developers to only push to protected branches if given permission
|
||||
- API: Add enpoints for pipelines
|
||||
- Add green outline to New Branch button. !5447 (winniehell)
|
||||
- Optimize generating of cache keys for issues and notes
|
||||
- Fix repository push email formatting in Outlook
|
||||
- Improve performance of syntax highlighting Markdown code blocks
|
||||
- Update to gitlab_git 10.4.1 and take advantage of preserved Ref objects
|
||||
- Remove delay when hitting "Reply..." button on page with a lot of discussions
|
||||
|
@ -51,9 +69,12 @@ v 8.11.0 (unreleased)
|
|||
- Upgrade Grape from 0.13.0 to 0.15.0. !4601
|
||||
- Trigram indexes for the "ci_runners" table have been removed to speed up UPDATE queries
|
||||
- Fix devise deprecation warnings.
|
||||
- Check for 2FA when using Git over HTTP and only allow PersonalAccessTokens as password in that case !5764
|
||||
- Update version_sorter and use new interface for faster tag sorting
|
||||
- Optimize checking if a user has read access to a list of issues !5370
|
||||
- Store all DB secrets in secrets.yml, under descriptive names !5274
|
||||
- Fix syntax highlighting in file editor
|
||||
- Support slash commands in issue and merge request descriptions as well as comments. !5021
|
||||
- Nokogiri's various parsing methods are now instrumented
|
||||
- Add archived badge to project list !5798
|
||||
- Add simple identifier to public SSH keys (muteor)
|
||||
|
@ -70,6 +91,9 @@ v 8.11.0 (unreleased)
|
|||
- The overhead of instrumented method calls has been reduced
|
||||
- Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le)
|
||||
- Load project invited groups and members eagerly in `ProjectTeam#fetch_members`
|
||||
- Add pipeline events hook
|
||||
- Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
|
||||
- Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison)
|
||||
- Bump gitlab_git to speedup DiffCollection iterations
|
||||
- Rewrite description of a blocked user in admin settings. (Elias Werberich)
|
||||
- Make branches sortable without push permission !5462 (winniehell)
|
||||
|
@ -79,18 +103,24 @@ v 8.11.0 (unreleased)
|
|||
- Make "New issue" button in Issue page less obtrusive !5457 (winniehell)
|
||||
- Gitlab::Metrics.current_transaction needs to be public for RailsQueueDuration
|
||||
- Fix search for notes which belongs to deleted objects
|
||||
- Allow Akismet to be trained by submitting issues as spam or ham !5538
|
||||
- Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska)
|
||||
- Allow branch names ending with .json for graph and network page !5579 (winniehell)
|
||||
- Add the `sprockets-es6` gem
|
||||
- Improve OAuth2 client documentation (muteor)
|
||||
- Fix diff comments inverted toggle bug (ClemMakesApps)
|
||||
- Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska)
|
||||
- Profile requests when a header is passed
|
||||
- Avoid calculation of line_code and position for _line partial when showing diff notes on discussion tab.
|
||||
- Speedup DiffNote#active? on discussions, preloading noteables and avoid touching git repository to return diff_refs when possible
|
||||
- Add commit stats in commit api. !5517 (dixpac)
|
||||
- Add CI configuration button on project page
|
||||
- Fix merge request new view not changing code view rendering style
|
||||
- edit_blob_link will use blob passed onto the options parameter
|
||||
- Make error pages responsive (Takuya Noguchi)
|
||||
- The performance of the project dropdown used for moving issues has been improved
|
||||
- Fix skip_repo parameter being ignored when destroying a namespace
|
||||
- Add all builds into stage/job dropdowns on builds page
|
||||
- Change requests_profiles resource constraint to catch virtually any file
|
||||
- Bump gitlab_git to lazy load compare commits
|
||||
- Reduce number of queries made for merge_requests/:id/diffs
|
||||
|
@ -103,14 +133,26 @@ v 8.11.0 (unreleased)
|
|||
- Speed up and reduce memory usage of Commit#repo_changes, Repository#expire_avatar_cache and IrkerWorker
|
||||
- Add unfold links for Side-by-Side view. !5415 (Tim Masliuchenko)
|
||||
- Adds support for pending invitation project members importing projects
|
||||
- Add pipeline visualization/graph on pipeline page
|
||||
- Update devise initializer to turn on changed password notification emails. !5648 (tombell)
|
||||
- Avoid to show the original password field when password is automatically set. !5712 (duduribeiro)
|
||||
- Fix importing GitLab projects with an invalid MR source project
|
||||
- Sort folders with submodules in Files view !5521
|
||||
- Each `File::exists?` replaced to `File::exist?` because of deprecate since ruby version 2.2.0
|
||||
- Add auto-completition in pipeline (Katarzyna Kobierska Ula Budziszewska)
|
||||
- Add pipelines tab to merge requests
|
||||
- Fix a memory leak caused by Banzai::Filter::SanitizationFilter
|
||||
- Speed up todos queries by limiting the projects set we join with
|
||||
- Ensure file editing in UI does not overwrite commited changes without warning user
|
||||
- Eliminate unneeded calls to Repository#blob_at when listing commits with no path
|
||||
- Update gitlab_git gem to 10.4.7
|
||||
- Simplify SQL queries of marking a todo as done
|
||||
|
||||
v 8.10.6
|
||||
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
|
||||
- Restore "Largest repository" sort option on Admin > Projects page. !5797
|
||||
- Fix privilege escalation via project export.
|
||||
- Require administrator privileges to perform a project import.
|
||||
|
||||
v 8.10.5
|
||||
- Add a data migration to fix some missing timestamps in the members table. !5670
|
||||
|
@ -132,6 +174,9 @@ v 8.10.3
|
|||
- Fix importer for GitHub Pull Requests when a branch was removed. !5573
|
||||
- Ignore invalid IPs in X-Forwarded-For when trusted proxies are configured. !5584
|
||||
- Trim extra displayed carriage returns in diffs and files with CRLFs. !5588
|
||||
- Fix label already exist error message in the right sidebar.
|
||||
|
||||
v 8.10.3 (unreleased)
|
||||
|
||||
v 8.10.2
|
||||
- User can now search branches by name. !5144
|
||||
|
@ -279,6 +324,7 @@ v 8.10.0
|
|||
- Metrics for Rouge::Plugins::Redcarpet and Rouge::Formatters::HTMLGitlab
|
||||
- RailsCache metris now includes fetch_hit/fetch_miss and read_hit/read_miss info.
|
||||
- Allow [ci skip] to be in any case and allow [skip ci]. !4785 (simon_w)
|
||||
- Made project list visibility icon fixed width
|
||||
- Set import_url validation to be more strict
|
||||
- Memoize MR merged/closed events retrieval
|
||||
- Don't render discussion notes when requesting diff tab through AJAX
|
||||
|
@ -325,6 +371,10 @@ v 8.10.0
|
|||
- Fix migration corrupting import data for old version upgrades
|
||||
- Show tooltip on GitLab export link in new project page
|
||||
|
||||
v 8.9.7
|
||||
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
|
||||
- Require administrator privileges to perform a project import.
|
||||
|
||||
v 8.9.6
|
||||
- Fix importing of events under notes for GitLab projects. !5154
|
||||
- Fix log statements in import/export. !5129
|
||||
|
@ -590,6 +640,9 @@ v 8.9.0
|
|||
- Add tooltip to pin/unpin navbar
|
||||
- Add new sub nav style to Wiki and Graphs sub navigation
|
||||
|
||||
v 8.8.8
|
||||
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
|
||||
|
||||
v 8.8.7
|
||||
- Fix privilege escalation issue with OAuth external users.
|
||||
- Ensure references to private repos aren't shown to logged-out users.
|
||||
|
|
12
Gemfile
12
Gemfile
|
@ -20,7 +20,7 @@ gem 'pg', '~> 0.18.2', group: :postgres
|
|||
|
||||
# Authentication libraries
|
||||
gem 'devise', '~> 4.0'
|
||||
gem 'doorkeeper', '~> 4.0'
|
||||
gem 'doorkeeper', '~> 4.2.0'
|
||||
gem 'omniauth', '~> 1.3.1'
|
||||
gem 'omniauth-auth0', '~> 1.4.1'
|
||||
gem 'omniauth-azure-oauth2', '~> 0.0.6'
|
||||
|
@ -53,7 +53,7 @@ gem 'browser', '~> 2.2'
|
|||
|
||||
# Extracting information from a git repository
|
||||
# Provide access to Gitlab::Git library
|
||||
gem 'gitlab_git', '~> 10.4.5'
|
||||
gem 'gitlab_git', '~> 10.4.7'
|
||||
|
||||
# LDAP Auth
|
||||
# GitLab fork with several improvements to original library. For full list of changes
|
||||
|
@ -77,7 +77,7 @@ gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
|
|||
gem 'kaminari', '~> 0.17.0'
|
||||
|
||||
# HAML
|
||||
gem 'hamlit', '~> 2.5'
|
||||
gem 'hamlit', '~> 2.6.1'
|
||||
|
||||
# Files attachments
|
||||
gem 'carrierwave', '~> 0.10.0'
|
||||
|
@ -201,7 +201,7 @@ gem 'licensee', '~> 8.0.0'
|
|||
gem 'rack-attack', '~> 4.3.1'
|
||||
|
||||
# Ace editor
|
||||
gem 'ace-rails-ap', '~> 4.0.2'
|
||||
gem 'ace-rails-ap', '~> 4.1.0'
|
||||
|
||||
# Keyboard shortcuts
|
||||
gem 'mousetrap-rails', '~> 1.4.6'
|
||||
|
@ -209,7 +209,8 @@ gem 'mousetrap-rails', '~> 1.4.6'
|
|||
# Detect and convert string character encoding
|
||||
gem 'charlock_holmes', '~> 0.7.3'
|
||||
|
||||
# Parse duration
|
||||
# Parse time & duration
|
||||
gem 'chronic', '~> 0.10.2'
|
||||
gem 'chronic_duration', '~> 0.10.6'
|
||||
|
||||
gem 'sass-rails', '~> 5.0.0'
|
||||
|
@ -314,6 +315,7 @@ end
|
|||
group :test do
|
||||
gem 'shoulda-matchers', '~> 2.8.0', require: false
|
||||
gem 'email_spec', '~> 1.6.0'
|
||||
gem 'json-schema', '~> 2.6.2'
|
||||
gem 'webmock', '~> 1.21.0'
|
||||
gem 'test_after_commit', '~> 0.4.2'
|
||||
gem 'sham_rack', '~> 1.3.6'
|
||||
|
|
23
Gemfile.lock
23
Gemfile.lock
|
@ -2,7 +2,7 @@ GEM
|
|||
remote: https://rubygems.org/
|
||||
specs:
|
||||
RedCloth (4.3.2)
|
||||
ace-rails-ap (4.0.2)
|
||||
ace-rails-ap (4.1.0)
|
||||
actionmailer (4.2.7.1)
|
||||
actionpack (= 4.2.7.1)
|
||||
actionview (= 4.2.7.1)
|
||||
|
@ -128,6 +128,7 @@ GEM
|
|||
mime-types (>= 1.16)
|
||||
cause (0.1)
|
||||
charlock_holmes (0.7.3)
|
||||
chronic (0.10.2)
|
||||
chronic_duration (0.10.6)
|
||||
numerizer (~> 0.1.1)
|
||||
chunky_png (1.3.5)
|
||||
|
@ -175,7 +176,7 @@ GEM
|
|||
diff-lcs (1.2.5)
|
||||
diffy (3.0.7)
|
||||
docile (1.1.5)
|
||||
doorkeeper (4.0.0)
|
||||
doorkeeper (4.2.0)
|
||||
railties (>= 4.2)
|
||||
dropzonejs-rails (0.7.2)
|
||||
rails (> 3.1)
|
||||
|
@ -278,7 +279,7 @@ GEM
|
|||
diff-lcs (~> 1.1)
|
||||
mime-types (>= 1.16, < 3)
|
||||
posix-spawn (~> 0.3)
|
||||
gitlab_git (10.4.5)
|
||||
gitlab_git (10.4.7)
|
||||
activesupport (~> 4.0)
|
||||
charlock_holmes (~> 0.7.3)
|
||||
github-linguist (~> 4.7.0)
|
||||
|
@ -321,7 +322,7 @@ GEM
|
|||
grape-entity (0.4.8)
|
||||
activesupport
|
||||
multi_json (>= 1.3.2)
|
||||
hamlit (2.5.0)
|
||||
hamlit (2.6.1)
|
||||
temple (~> 0.7.6)
|
||||
thor
|
||||
tilt
|
||||
|
@ -338,7 +339,7 @@ GEM
|
|||
httparty (0.13.7)
|
||||
json (~> 1.8)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.7.0.1)
|
||||
httpclient (2.8.2)
|
||||
i18n (0.7.0)
|
||||
ice_nine (0.11.1)
|
||||
influxdb (0.2.3)
|
||||
|
@ -356,6 +357,8 @@ GEM
|
|||
jquery-ui-rails (5.0.5)
|
||||
railties (>= 3.2.16)
|
||||
json (1.8.3)
|
||||
json-schema (2.6.2)
|
||||
addressable (~> 2.3.8)
|
||||
jwt (1.5.4)
|
||||
kaminari (0.17.0)
|
||||
actionpack (>= 3.0.0)
|
||||
|
@ -796,7 +799,7 @@ PLATFORMS
|
|||
|
||||
DEPENDENCIES
|
||||
RedCloth (~> 4.3.2)
|
||||
ace-rails-ap (~> 4.0.2)
|
||||
ace-rails-ap (~> 4.1.0)
|
||||
activerecord-session_store (~> 1.0.0)
|
||||
acts-as-taggable-on (~> 3.4)
|
||||
addressable (~> 2.3.8)
|
||||
|
@ -822,6 +825,7 @@ DEPENDENCIES
|
|||
capybara-screenshot (~> 1.0.0)
|
||||
carrierwave (~> 0.10.0)
|
||||
charlock_holmes (~> 0.7.3)
|
||||
chronic (~> 0.10.2)
|
||||
chronic_duration (~> 0.10.6)
|
||||
coffee-rails (~> 4.1.0)
|
||||
connection_pool (~> 2.0)
|
||||
|
@ -832,7 +836,7 @@ DEPENDENCIES
|
|||
devise (~> 4.0)
|
||||
devise-two-factor (~> 3.0.0)
|
||||
diffy (~> 3.0.3)
|
||||
doorkeeper (~> 4.0)
|
||||
doorkeeper (~> 4.2.0)
|
||||
dropzonejs-rails (~> 0.7.1)
|
||||
email_reply_parser (~> 0.5.8)
|
||||
email_spec (~> 1.6.0)
|
||||
|
@ -855,7 +859,7 @@ DEPENDENCIES
|
|||
github-linguist (~> 4.7.0)
|
||||
github-markup (~> 1.4)
|
||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||
gitlab_git (~> 10.4.5)
|
||||
gitlab_git (~> 10.4.7)
|
||||
gitlab_meta (= 7.0)
|
||||
gitlab_omniauth-ldap (~> 1.2.1)
|
||||
gollum-lib (~> 4.2)
|
||||
|
@ -863,7 +867,7 @@ DEPENDENCIES
|
|||
gon (~> 6.1.0)
|
||||
grape (~> 0.15.0)
|
||||
grape-entity (~> 0.4.2)
|
||||
hamlit (~> 2.5)
|
||||
hamlit (~> 2.6.1)
|
||||
health_check (~> 2.1.0)
|
||||
hipchat (~> 1.5.0)
|
||||
html-pipeline (~> 1.11.0)
|
||||
|
@ -873,6 +877,7 @@ DEPENDENCIES
|
|||
jquery-rails (~> 4.1.0)
|
||||
jquery-turbolinks (~> 2.1.0)
|
||||
jquery-ui-rails (~> 5.0.0)
|
||||
json-schema (~> 2.6.2)
|
||||
jwt
|
||||
kaminari (~> 0.17.0)
|
||||
knapsack (~> 1.11.0)
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
window.gl = window.gl || {};
|
||||
((global) => {
|
||||
const MAX_MESSAGE_LENGTH = 500;
|
||||
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
|
||||
|
||||
class AbuseReports {
|
||||
constructor() {
|
||||
$(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage);
|
||||
$(document)
|
||||
.off('click', MESSAGE_CELL_SELECTOR)
|
||||
.on('click', MESSAGE_CELL_SELECTOR, this.toggleMessageTruncation);
|
||||
}
|
||||
|
||||
truncateLongMessage() {
|
||||
const $messageCellElement = $(this);
|
||||
const reportMessage = $messageCellElement.text();
|
||||
if (reportMessage.length > MAX_MESSAGE_LENGTH) {
|
||||
$messageCellElement.data('original-message', reportMessage);
|
||||
$messageCellElement.data('message-truncated', 'true');
|
||||
$messageCellElement.text(global.text.truncate(reportMessage, MAX_MESSAGE_LENGTH));
|
||||
}
|
||||
}
|
||||
|
||||
toggleMessageTruncation() {
|
||||
const $messageCellElement = $(this);
|
||||
const originalMessage = $messageCellElement.data('original-message');
|
||||
if (!originalMessage) return;
|
||||
if ($messageCellElement.data('message-truncated') === 'true') {
|
||||
$messageCellElement.data('message-truncated', 'false');
|
||||
$messageCellElement.text(originalMessage);
|
||||
} else {
|
||||
$messageCellElement.data('message-truncated', 'true');
|
||||
$messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
global.AbuseReports = AbuseReports;
|
||||
})(window.gl);
|
|
@ -9,10 +9,11 @@
|
|||
licensePath: "/api/:version/licenses/:key",
|
||||
gitignorePath: "/api/:version/gitignores/:key",
|
||||
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
|
||||
issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
|
||||
|
||||
group: function(group_id, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.groupPath);
|
||||
url = url.replace(':id', group_id);
|
||||
var url = Api.buildUrl(Api.groupPath)
|
||||
.replace(':id', group_id);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: {
|
||||
|
@ -24,8 +25,7 @@
|
|||
});
|
||||
},
|
||||
groups: function(query, skip_ldap, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.groupsPath);
|
||||
var url = Api.buildUrl(Api.groupsPath);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: {
|
||||
|
@ -39,8 +39,7 @@
|
|||
});
|
||||
},
|
||||
namespaces: function(query, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.namespacesPath);
|
||||
var url = Api.buildUrl(Api.namespacesPath);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: {
|
||||
|
@ -54,8 +53,7 @@
|
|||
});
|
||||
},
|
||||
projects: function(query, order, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.projectsPath);
|
||||
var url = Api.buildUrl(Api.projectsPath);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: {
|
||||
|
@ -70,9 +68,8 @@
|
|||
});
|
||||
},
|
||||
newLabel: function(project_id, data, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.labelsPath);
|
||||
url = url.replace(':id', project_id);
|
||||
var url = Api.buildUrl(Api.labelsPath)
|
||||
.replace(':id', project_id);
|
||||
data.private_token = gon.api_token;
|
||||
return $.ajax({
|
||||
url: url,
|
||||
|
@ -86,9 +83,8 @@
|
|||
});
|
||||
},
|
||||
groupProjects: function(group_id, query, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.groupProjectsPath);
|
||||
url = url.replace(':id', group_id);
|
||||
var url = Api.buildUrl(Api.groupProjectsPath)
|
||||
.replace(':id', group_id);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: {
|
||||
|
@ -102,8 +98,8 @@
|
|||
});
|
||||
},
|
||||
licenseText: function(key, data, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.licensePath).replace(':key', key);
|
||||
var url = Api.buildUrl(Api.licensePath)
|
||||
.replace(':key', key);
|
||||
return $.ajax({
|
||||
url: url,
|
||||
data: data
|
||||
|
@ -112,19 +108,32 @@
|
|||
});
|
||||
},
|
||||
gitignoreText: function(key, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
|
||||
var url = Api.buildUrl(Api.gitignorePath)
|
||||
.replace(':key', key);
|
||||
return $.get(url, function(gitignore) {
|
||||
return callback(gitignore);
|
||||
});
|
||||
},
|
||||
gitlabCiYml: function(key, callback) {
|
||||
var url;
|
||||
url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
|
||||
var url = Api.buildUrl(Api.gitlabCiYmlPath)
|
||||
.replace(':key', key);
|
||||
return $.get(url, function(file) {
|
||||
return callback(file);
|
||||
});
|
||||
},
|
||||
issueTemplate: function(namespacePath, projectPath, key, type, callback) {
|
||||
var url = Api.buildUrl(Api.issuableTemplatePath)
|
||||
.replace(':key', key)
|
||||
.replace(':type', type)
|
||||
.replace(':project_path', projectPath)
|
||||
.replace(':namespace_path', namespacePath);
|
||||
$.ajax({
|
||||
url: url,
|
||||
dataType: 'json'
|
||||
}).done(function(file) {
|
||||
callback(null, file);
|
||||
}).error(callback);
|
||||
},
|
||||
buildUrl: function(url) {
|
||||
if (gon.relative_url_root != null) {
|
||||
url = gon.relative_url_root + url;
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
/*= require bootstrap/tooltip */
|
||||
/*= require bootstrap/popover */
|
||||
/*= require select2 */
|
||||
/*= require ace/ace */
|
||||
/*= require ace-rails-ap */
|
||||
/*= require ace/ext-searchbox */
|
||||
/*= require underscore */
|
||||
/*= require dropzone */
|
||||
|
@ -41,6 +41,7 @@
|
|||
/*= require date.format */
|
||||
/*= require_directory ./behaviors */
|
||||
/*= require_directory ./blob */
|
||||
/*= require_directory ./templates */
|
||||
/*= require_directory ./commit */
|
||||
/*= require_directory ./extensions */
|
||||
/*= require_directory ./lib/utils */
|
||||
|
@ -223,8 +224,14 @@
|
|||
return $('.navbar-toggle').toggleClass('active');
|
||||
});
|
||||
$body.on("click", ".js-toggle-diff-comments", function(e) {
|
||||
$(this).toggleClass('active');
|
||||
$(this).closest(".diff-file").find(".notes_holder").toggle();
|
||||
var $this = $(this);
|
||||
$this.toggleClass('active');
|
||||
var notesHolders = $this.closest('.diff-file').find('.notes_holder');
|
||||
if ($this.hasClass('active')) {
|
||||
notesHolders.show();
|
||||
} else {
|
||||
notesHolders.hide();
|
||||
}
|
||||
return e.preventDefault();
|
||||
});
|
||||
$document.off("click", '.js-confirm-danger');
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
(function() {
|
||||
this.AwardsHandler = (function() {
|
||||
const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence
|
||||
function AwardsHandler() {
|
||||
this.aliases = gl.emojiAliases();
|
||||
$(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) {
|
||||
|
@ -130,7 +131,7 @@
|
|||
counter = $emojiButton.find('.js-counter');
|
||||
counter.text(parseInt(counter.text()) + 1);
|
||||
$emojiButton.addClass('active');
|
||||
this.addMeToUserList(votesBlock, emoji);
|
||||
this.addYouToUserList(votesBlock, emoji);
|
||||
return this.animateEmoji($emojiButton);
|
||||
}
|
||||
} else {
|
||||
|
@ -176,11 +177,11 @@
|
|||
counterNumber = parseInt(counter.text(), 10);
|
||||
if (counterNumber > 1) {
|
||||
counter.text(counterNumber - 1);
|
||||
this.removeMeFromUserList($emojiButton, emoji);
|
||||
this.removeYouFromUserList($emojiButton, emoji);
|
||||
} else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
|
||||
$emojiButton.tooltip('destroy');
|
||||
counter.text('0');
|
||||
this.removeMeFromUserList($emojiButton, emoji);
|
||||
this.removeYouFromUserList($emojiButton, emoji);
|
||||
if ($emojiButton.parents('.note').length) {
|
||||
this.removeEmoji($emojiButton);
|
||||
}
|
||||
|
@ -204,43 +205,48 @@
|
|||
return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || '';
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.removeMeFromUserList = function($emojiButton, emoji) {
|
||||
AwardsHandler.prototype.toSentence = function(list) {
|
||||
if(list.length <= 2){
|
||||
return list.join(' and ');
|
||||
}
|
||||
else{
|
||||
return list.slice(0, -1).join(', ') + ', and ' + list[list.length - 1];
|
||||
}
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.removeYouFromUserList = function($emojiButton, emoji) {
|
||||
var authors, awardBlock, newAuthors, originalTitle;
|
||||
awardBlock = $emojiButton;
|
||||
originalTitle = this.getAwardTooltip(awardBlock);
|
||||
authors = originalTitle.split(', ');
|
||||
authors.splice(authors.indexOf('me'), 1);
|
||||
newAuthors = authors.join(', ');
|
||||
awardBlock.closest('.js-emoji-btn').removeData('original-title').attr('data-original-title', newAuthors);
|
||||
return this.resetTooltip(awardBlock);
|
||||
authors = originalTitle.split(FROM_SENTENCE_REGEX);
|
||||
authors.splice(authors.indexOf('You'), 1);
|
||||
return awardBlock
|
||||
.closest('.js-emoji-btn')
|
||||
.removeData('title')
|
||||
.removeAttr('data-title')
|
||||
.removeAttr('data-original-title')
|
||||
.attr('title', this.toSentence(authors))
|
||||
.tooltip('fixTitle');
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.addMeToUserList = function(votesBlock, emoji) {
|
||||
AwardsHandler.prototype.addYouToUserList = function(votesBlock, emoji) {
|
||||
var awardBlock, origTitle, users;
|
||||
awardBlock = this.findEmojiIcon(votesBlock, emoji).parent();
|
||||
origTitle = this.getAwardTooltip(awardBlock);
|
||||
users = [];
|
||||
if (origTitle) {
|
||||
users = origTitle.trim().split(', ');
|
||||
users = origTitle.trim().split(FROM_SENTENCE_REGEX);
|
||||
}
|
||||
users.push('me');
|
||||
awardBlock.attr('title', users.join(', '));
|
||||
return this.resetTooltip(awardBlock);
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.resetTooltip = function(award) {
|
||||
var cb;
|
||||
award.tooltip('destroy');
|
||||
cb = function() {
|
||||
return award.tooltip();
|
||||
};
|
||||
return setTimeout(cb, 200);
|
||||
users.unshift('You');
|
||||
return awardBlock
|
||||
.attr('title', this.toSentence(users))
|
||||
.tooltip('fixTitle');
|
||||
};
|
||||
|
||||
AwardsHandler.prototype.createEmoji_ = function(votesBlock, emoji) {
|
||||
var $emojiButton, buttonHtml, emojiCssClass;
|
||||
emojiCssClass = this.resolveNameToCssClass(emoji);
|
||||
buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'> <div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div> <span class='award-control-text js-counter'>1</span> </button>";
|
||||
buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='You' data-placement='bottom'> <div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div> <span class='award-control-text js-counter'>1</span> </button>";
|
||||
$emojiButton = $(buttonHtml);
|
||||
$emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('emoji', emoji);
|
||||
this.animateEmoji($emojiButton);
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
}
|
||||
this.onClick = bind(this.onClick, this);
|
||||
this.dropdown = opts.dropdown, this.data = opts.data, this.pattern = opts.pattern, this.wrapper = opts.wrapper, this.editor = opts.editor, this.fileEndpoint = opts.fileEndpoint, this.$input = (ref = opts.$input) != null ? ref : $('#file_name');
|
||||
this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
|
||||
this.buildDropdown();
|
||||
this.bindEvents();
|
||||
this.onFilenameUpdate();
|
||||
|
@ -60,11 +61,26 @@
|
|||
return this.requestFile(item);
|
||||
};
|
||||
|
||||
TemplateSelector.prototype.requestFile = function(item) {};
|
||||
TemplateSelector.prototype.requestFile = function(item) {
|
||||
// This `requestFile` method is an abstract method that should
|
||||
// be added by all subclasses.
|
||||
};
|
||||
|
||||
TemplateSelector.prototype.requestFileSuccess = function(file) {
|
||||
TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
|
||||
this.editor.setValue(file.content, 1);
|
||||
return this.editor.focus();
|
||||
if (!skipFocus) this.editor.focus();
|
||||
};
|
||||
|
||||
TemplateSelector.prototype.startLoadingSpinner = function() {
|
||||
this.dropdownIcon
|
||||
.addClass('fa-spinner fa-spin')
|
||||
.removeClass('fa-chevron-down');
|
||||
};
|
||||
|
||||
TemplateSelector.prototype.stopLoadingSpinner = function() {
|
||||
this.dropdownIcon
|
||||
.addClass('fa-chevron-down')
|
||||
.removeClass('fa-spinner fa-spin');
|
||||
};
|
||||
|
||||
return TemplateSelector;
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
//= require vue
|
||||
//= require vue-resource
|
||||
//= require Sortable
|
||||
//= require_tree ./models
|
||||
//= require_tree ./stores
|
||||
//= require_tree ./services
|
||||
//= require_tree ./mixins
|
||||
//= require ./components/board
|
||||
//= require ./components/new_list_dropdown
|
||||
//= require ./vue_resource_interceptor
|
||||
|
||||
$(() => {
|
||||
const $boardApp = document.getElementById('board-app'),
|
||||
Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
|
||||
if (gl.IssueBoardsApp) {
|
||||
gl.IssueBoardsApp.$destroy(true);
|
||||
}
|
||||
|
||||
gl.IssueBoardsApp = new Vue({
|
||||
el: $boardApp,
|
||||
components: {
|
||||
'board': gl.issueBoards.Board
|
||||
},
|
||||
data: {
|
||||
state: Store.state,
|
||||
loading: true,
|
||||
endpoint: $boardApp.dataset.endpoint,
|
||||
disabled: $boardApp.dataset.disabled === 'true',
|
||||
issueLinkBase: $boardApp.dataset.issueLinkBase
|
||||
},
|
||||
init: Store.create.bind(Store),
|
||||
created () {
|
||||
gl.boardService = new BoardService(this.endpoint);
|
||||
},
|
||||
ready () {
|
||||
Store.disabled = this.disabled;
|
||||
gl.boardService.all()
|
||||
.then((resp) => {
|
||||
resp.json().forEach((board) => {
|
||||
const list = Store.addList(board);
|
||||
|
||||
if (list.type === 'done') {
|
||||
list.position = Infinity;
|
||||
} else if (list.type === 'backlog') {
|
||||
list.position = -1;
|
||||
}
|
||||
});
|
||||
|
||||
Store.addBlankState();
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
//= require ./board_blank_state
|
||||
//= require ./board_delete
|
||||
//= require ./board_list
|
||||
|
||||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.Board = Vue.extend({
|
||||
components: {
|
||||
'board-list': gl.issueBoards.BoardList,
|
||||
'board-delete': gl.issueBoards.BoardDelete,
|
||||
'board-blank-state': gl.issueBoards.BoardBlankState
|
||||
},
|
||||
props: {
|
||||
list: Object,
|
||||
disabled: Boolean,
|
||||
issueLinkBase: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
query: '',
|
||||
filters: Store.state.filters
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
query () {
|
||||
this.list.filters = this.getFilterData();
|
||||
this.list.getIssues(true);
|
||||
},
|
||||
filters: {
|
||||
handler () {
|
||||
this.list.page = 1;
|
||||
this.list.getIssues(true);
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getFilterData () {
|
||||
const filters = this.filters;
|
||||
let queryData = { search: this.query };
|
||||
|
||||
Object.keys(filters).forEach((key) => { queryData[key] = filters[key]; });
|
||||
|
||||
return queryData;
|
||||
}
|
||||
},
|
||||
ready () {
|
||||
const options = gl.issueBoards.getBoardSortableDefaultOptions({
|
||||
disabled: this.disabled,
|
||||
group: 'boards',
|
||||
draggable: '.is-draggable',
|
||||
handle: '.js-board-handle',
|
||||
onEnd: (e) => {
|
||||
document.body.classList.remove('is-dragging');
|
||||
|
||||
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
|
||||
const order = this.sortable.toArray(),
|
||||
$board = this.$parent.$refs.board[e.oldIndex + 1],
|
||||
list = $board.list;
|
||||
|
||||
$board.$destroy(true);
|
||||
|
||||
this.$nextTick(() => {
|
||||
Store.state.lists.splice(e.newIndex, 0, list);
|
||||
Store.moveList(list, order);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (bp.getBreakpointSize() === 'xs') {
|
||||
options.handle = '.js-board-drag-handle';
|
||||
}
|
||||
|
||||
this.sortable = Sortable.create(this.$el.parentNode, options);
|
||||
},
|
||||
beforeDestroy () {
|
||||
Store.state.lists.$remove(this.list);
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,49 @@
|
|||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.BoardBlankState = Vue.extend({
|
||||
data () {
|
||||
return {
|
||||
predefinedLabels: [
|
||||
new ListLabel({ title: 'Development', color: '#5CB85C' }),
|
||||
new ListLabel({ title: 'Testing', color: '#F0AD4E' }),
|
||||
new ListLabel({ title: 'Production', color: '#FF5F00' }),
|
||||
new ListLabel({ title: 'Ready', color: '#FF0000' })
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addDefaultLists () {
|
||||
this.clearBlankState();
|
||||
|
||||
this.predefinedLabels.forEach((label, i) => {
|
||||
Store.addList({
|
||||
title: label.title,
|
||||
position: i,
|
||||
list_type: 'label',
|
||||
label: {
|
||||
title: label.title,
|
||||
color: label.color
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Save the labels
|
||||
gl.boardService.generateDefaultLists()
|
||||
.then((resp) => {
|
||||
resp.json().forEach((listObj) => {
|
||||
const list = Store.findList('title', listObj.title);
|
||||
|
||||
list.id = listObj.id;
|
||||
list.label.id = listObj.label.id;
|
||||
list.getIssues();
|
||||
});
|
||||
});
|
||||
},
|
||||
clearBlankState: Store.removeBlankState.bind(Store)
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,43 @@
|
|||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.BoardCard = Vue.extend({
|
||||
props: {
|
||||
list: Object,
|
||||
issue: Object,
|
||||
issueLinkBase: String,
|
||||
disabled: Boolean,
|
||||
index: Number
|
||||
},
|
||||
methods: {
|
||||
filterByLabel (label, e) {
|
||||
let labelToggleText = label.title;
|
||||
const labelIndex = Store.state.filters['label_name'].indexOf(label.title);
|
||||
$(e.target).tooltip('hide');
|
||||
|
||||
if (labelIndex === -1) {
|
||||
Store.state.filters['label_name'].push(label.title);
|
||||
$('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`);
|
||||
} else {
|
||||
Store.state.filters['label_name'].splice(labelIndex, 1);
|
||||
labelToggleText = Store.state.filters['label_name'][0];
|
||||
$(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove();
|
||||
}
|
||||
|
||||
const selectedLabels = Store.state.filters['label_name'];
|
||||
if (selectedLabels.length === 0) {
|
||||
labelToggleText = 'Label';
|
||||
} else if (selectedLabels.length > 1) {
|
||||
labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`;
|
||||
}
|
||||
|
||||
$('.labels-filter .dropdown-toggle-text').text(labelToggleText);
|
||||
|
||||
Store.updateFiltersUrl();
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,19 @@
|
|||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.BoardDelete = Vue.extend({
|
||||
props: {
|
||||
list: Object
|
||||
},
|
||||
methods: {
|
||||
deleteBoard () {
|
||||
$(this.$el).tooltip('hide');
|
||||
|
||||
if (confirm('Are you sure you want to delete this list?')) {
|
||||
this.list.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,89 @@
|
|||
//= require ./board_card
|
||||
|
||||
(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.BoardList = Vue.extend({
|
||||
components: {
|
||||
'board-card': gl.issueBoards.BoardCard
|
||||
},
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
list: Object,
|
||||
issues: Array,
|
||||
loading: Boolean,
|
||||
issueLinkBase: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
scrollOffset: 250,
|
||||
filters: Store.state.filters
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
filters: {
|
||||
handler () {
|
||||
this.list.loadingMore = false;
|
||||
this.$els.list.scrollTop = 0;
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
listHeight () {
|
||||
return this.$els.list.getBoundingClientRect().height;
|
||||
},
|
||||
scrollHeight () {
|
||||
return this.$els.list.scrollHeight;
|
||||
},
|
||||
scrollTop () {
|
||||
return this.$els.list.scrollTop + this.listHeight();
|
||||
},
|
||||
loadNextPage () {
|
||||
const getIssues = this.list.nextPage();
|
||||
|
||||
if (getIssues) {
|
||||
this.list.loadingMore = true;
|
||||
getIssues.then(() => {
|
||||
this.list.loadingMore = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
ready () {
|
||||
const options = gl.issueBoards.getBoardSortableDefaultOptions({
|
||||
group: 'issues',
|
||||
sort: false,
|
||||
disabled: this.disabled,
|
||||
onStart: (e) => {
|
||||
const card = this.$refs.issue[e.oldIndex];
|
||||
|
||||
Store.moving.issue = card.issue;
|
||||
Store.moving.list = card.list;
|
||||
},
|
||||
onAdd: (e) => {
|
||||
gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue);
|
||||
},
|
||||
onRemove: (e) => {
|
||||
this.$refs.issue[e.oldIndex].$destroy(true);
|
||||
}
|
||||
});
|
||||
|
||||
if (bp.getBreakpointSize() === 'xs') {
|
||||
options.handle = '.js-card-drag-handle';
|
||||
}
|
||||
|
||||
this.sortable = Sortable.create(this.$els.list, options);
|
||||
|
||||
// Scroll event on list to load more
|
||||
this.$els.list.onscroll = () => {
|
||||
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
|
||||
this.loadNextPage();
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,54 @@
|
|||
$(() => {
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
$('.js-new-board-list').each(function () {
|
||||
const $this = $(this);
|
||||
|
||||
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('project-id'));
|
||||
|
||||
$this.glDropdown({
|
||||
data(term, callback) {
|
||||
$.get($this.attr('data-labels'))
|
||||
.then((resp) => {
|
||||
callback(resp);
|
||||
});
|
||||
},
|
||||
renderRow (label) {
|
||||
const active = Store.findList('title', label.title),
|
||||
$li = $('<li />'),
|
||||
$a = $('<a />', {
|
||||
class: (active ? `is-active js-board-list-${active.id}` : ''),
|
||||
text: label.title,
|
||||
href: '#'
|
||||
}),
|
||||
$labelColor = $('<span />', {
|
||||
class: 'dropdown-label-box',
|
||||
style: `background-color: ${label.color}`
|
||||
});
|
||||
|
||||
return $li.append($a.prepend($labelColor));
|
||||
},
|
||||
search: {
|
||||
fields: ['title']
|
||||
},
|
||||
filterable: true,
|
||||
selectable: true,
|
||||
clicked (label, $el, e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!Store.findList('title', label.title)) {
|
||||
Store.new({
|
||||
title: label.title,
|
||||
position: Store.state.lists.length - 2,
|
||||
list_type: 'label',
|
||||
label: {
|
||||
id: label.id,
|
||||
title: label.title,
|
||||
color: label.color
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
((w) => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
|
||||
let defaultSortOptions = {
|
||||
forceFallback: true,
|
||||
fallbackClass: 'is-dragging',
|
||||
fallbackOnBody: true,
|
||||
ghostClass: 'is-ghost',
|
||||
filter: '.has-tooltip',
|
||||
scrollSensitivity: 100,
|
||||
scrollSpeed: 20,
|
||||
onStart () {
|
||||
document.body.classList.add('is-dragging');
|
||||
},
|
||||
onEnd () {
|
||||
document.body.classList.remove('is-dragging');
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
|
||||
return defaultSortOptions;
|
||||
};
|
||||
})(window);
|
|
@ -0,0 +1,44 @@
|
|||
class ListIssue {
|
||||
constructor (obj) {
|
||||
this.id = obj.iid;
|
||||
this.title = obj.title;
|
||||
this.confidential = obj.confidential;
|
||||
this.labels = [];
|
||||
|
||||
if (obj.assignee) {
|
||||
this.assignee = new ListUser(obj.assignee);
|
||||
}
|
||||
|
||||
obj.labels.forEach((label) => {
|
||||
this.labels.push(new ListLabel(label));
|
||||
});
|
||||
|
||||
this.priority = this.labels.reduce((max, label) => {
|
||||
return (label.priority < max) ? label.priority : max;
|
||||
}, Infinity);
|
||||
}
|
||||
|
||||
addLabel (label) {
|
||||
if (!this.findLabel(label)) {
|
||||
this.labels.push(new ListLabel(label));
|
||||
}
|
||||
}
|
||||
|
||||
findLabel (findLabel) {
|
||||
return this.labels.filter( label => label.title === findLabel.title )[0];
|
||||
}
|
||||
|
||||
removeLabel (removeLabel) {
|
||||
if (removeLabel) {
|
||||
this.labels = this.labels.filter( label => removeLabel.title !== label.title );
|
||||
}
|
||||
}
|
||||
|
||||
removeLabels (labels) {
|
||||
labels.forEach(this.removeLabel.bind(this));
|
||||
}
|
||||
|
||||
getLists () {
|
||||
return gl.issueBoards.BoardsStore.state.lists.filter( list => list.findIssue(this.id) );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
class ListLabel {
|
||||
constructor (obj) {
|
||||
this.id = obj.id;
|
||||
this.title = obj.title;
|
||||
this.color = obj.color;
|
||||
this.description = obj.description;
|
||||
this.priority = (obj.priority !== null) ? obj.priority : Infinity;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
class List {
|
||||
constructor (obj) {
|
||||
this.id = obj.id;
|
||||
this._uid = this.guid();
|
||||
this.position = obj.position;
|
||||
this.title = obj.title;
|
||||
this.type = obj.list_type;
|
||||
this.preset = ['backlog', 'done', 'blank'].indexOf(this.type) > -1;
|
||||
this.filters = gl.issueBoards.BoardsStore.state.filters;
|
||||
this.page = 1;
|
||||
this.loading = true;
|
||||
this.loadingMore = false;
|
||||
this.issues = [];
|
||||
|
||||
if (obj.label) {
|
||||
this.label = new ListLabel(obj.label);
|
||||
}
|
||||
|
||||
if (this.type !== 'blank' && this.id) {
|
||||
this.getIssues();
|
||||
}
|
||||
}
|
||||
|
||||
guid() {
|
||||
const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
|
||||
return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
|
||||
}
|
||||
|
||||
save () {
|
||||
return gl.boardService.createList(this.label.id)
|
||||
.then((resp) => {
|
||||
const data = resp.json();
|
||||
|
||||
this.id = data.id;
|
||||
this.type = data.list_type;
|
||||
this.position = data.position;
|
||||
|
||||
return this.getIssues();
|
||||
});
|
||||
}
|
||||
|
||||
destroy () {
|
||||
gl.issueBoards.BoardsStore.state.lists.$remove(this);
|
||||
gl.issueBoards.BoardsStore.updateNewListDropdown(this.id);
|
||||
|
||||
gl.boardService.destroyList(this.id);
|
||||
}
|
||||
|
||||
update () {
|
||||
gl.boardService.updateList(this.id, this.position);
|
||||
}
|
||||
|
||||
nextPage () {
|
||||
if (Math.floor(this.issues.length / 20) === this.page) {
|
||||
this.page++;
|
||||
|
||||
return this.getIssues(false);
|
||||
}
|
||||
}
|
||||
|
||||
canSearch () {
|
||||
return this.type === 'backlog';
|
||||
}
|
||||
|
||||
getIssues (emptyIssues = true) {
|
||||
const filters = this.filters;
|
||||
let data = { page: this.page };
|
||||
|
||||
Object.keys(filters).forEach((key) => { data[key] = filters[key]; });
|
||||
|
||||
if (this.label) {
|
||||
data.label_name = data.label_name.filter( label => label !== this.label.title );
|
||||
}
|
||||
|
||||
if (emptyIssues) {
|
||||
this.loading = true;
|
||||
}
|
||||
|
||||
return gl.boardService.getIssuesForList(this.id, data)
|
||||
.then((resp) => {
|
||||
const data = resp.json();
|
||||
this.loading = false;
|
||||
|
||||
if (emptyIssues) {
|
||||
this.issues = [];
|
||||
}
|
||||
|
||||
this.createIssues(data);
|
||||
});
|
||||
}
|
||||
|
||||
createIssues (data) {
|
||||
data.forEach((issueObj) => {
|
||||
this.addIssue(new ListIssue(issueObj));
|
||||
});
|
||||
}
|
||||
|
||||
addIssue (issue, listFrom) {
|
||||
this.issues.push(issue);
|
||||
|
||||
if (this.label) {
|
||||
issue.addLabel(this.label);
|
||||
}
|
||||
|
||||
if (listFrom) {
|
||||
gl.boardService.moveIssue(issue.id, listFrom.id, this.id);
|
||||
}
|
||||
}
|
||||
|
||||
findIssue (id) {
|
||||
return this.issues.filter( issue => issue.id === id )[0];
|
||||
}
|
||||
|
||||
removeIssue (removeIssue) {
|
||||
this.issues = this.issues.filter((issue) => {
|
||||
const matchesRemove = removeIssue.id === issue.id;
|
||||
|
||||
if (matchesRemove) {
|
||||
issue.removeLabel(this.label);
|
||||
}
|
||||
|
||||
return !matchesRemove;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
class ListUser {
|
||||
constructor (user) {
|
||||
this.id = user.id;
|
||||
this.name = user.name;
|
||||
this.username = user.username;
|
||||
this.avatar = user.avatar_url;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
class BoardService {
|
||||
constructor (root) {
|
||||
Vue.http.options.root = root;
|
||||
|
||||
this.lists = Vue.resource(`${root}/lists{/id}`, {}, {
|
||||
generate: {
|
||||
method: 'POST',
|
||||
url: `${root}/lists/generate.json`
|
||||
}
|
||||
});
|
||||
this.issue = Vue.resource(`${root}/issues{/id}`, {});
|
||||
this.issues = Vue.resource(`${root}/lists{/id}/issues`, {});
|
||||
|
||||
Vue.http.interceptors.push((request, next) => {
|
||||
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
all () {
|
||||
return this.lists.get();
|
||||
}
|
||||
|
||||
generateDefaultLists () {
|
||||
return this.lists.generate({});
|
||||
}
|
||||
|
||||
createList (label_id) {
|
||||
return this.lists.save({}, {
|
||||
list: {
|
||||
label_id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateList (id, position) {
|
||||
return this.lists.update({ id }, {
|
||||
list: {
|
||||
position
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
destroyList (id) {
|
||||
return this.lists.delete({ id });
|
||||
}
|
||||
|
||||
getIssuesForList (id, filter = {}) {
|
||||
let data = { id };
|
||||
Object.keys(filter).forEach((key) => { data[key] = filter[key]; });
|
||||
|
||||
return this.issues.get(data);
|
||||
}
|
||||
|
||||
moveIssue (id, from_list_id, to_list_id) {
|
||||
return this.issue.update({ id }, {
|
||||
from_list_id,
|
||||
to_list_id
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,112 @@
|
|||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.issueBoards = window.gl.issueBoards || {};
|
||||
|
||||
gl.issueBoards.BoardsStore = {
|
||||
disabled: false,
|
||||
state: {},
|
||||
moving: {
|
||||
issue: {},
|
||||
list: {}
|
||||
},
|
||||
create () {
|
||||
this.state.lists = [];
|
||||
this.state.filters = {
|
||||
author_id: gl.utils.getParameterValues('author_id')[0],
|
||||
assignee_id: gl.utils.getParameterValues('assignee_id')[0],
|
||||
milestone_title: gl.utils.getParameterValues('milestone_title')[0],
|
||||
label_name: gl.utils.getParameterValues('label_name[]')
|
||||
};
|
||||
},
|
||||
addList (listObj) {
|
||||
const list = new List(listObj);
|
||||
this.state.lists.push(list);
|
||||
|
||||
return list;
|
||||
},
|
||||
new (listObj) {
|
||||
const list = this.addList(listObj),
|
||||
backlogList = this.findList('type', 'backlog', 'backlog');
|
||||
|
||||
list
|
||||
.save()
|
||||
.then(() => {
|
||||
// Remove any new issues from the backlog
|
||||
// as they will be visible in the new list
|
||||
list.issues.forEach(backlogList.removeIssue.bind(backlogList));
|
||||
});
|
||||
this.removeBlankState();
|
||||
},
|
||||
updateNewListDropdown (listId) {
|
||||
$(`.js-board-list-${listId}`).removeClass('is-active');
|
||||
},
|
||||
shouldAddBlankState () {
|
||||
// Decide whether to add the blank state
|
||||
return !(this.state.lists.filter( list => list.type !== 'backlog' && list.type !== 'done' )[0]);
|
||||
},
|
||||
addBlankState () {
|
||||
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
|
||||
|
||||
this.addList({
|
||||
id: 'blank',
|
||||
list_type: 'blank',
|
||||
title: 'Welcome to your Issue Board!',
|
||||
position: 0
|
||||
});
|
||||
},
|
||||
removeBlankState () {
|
||||
this.removeList('blank');
|
||||
|
||||
$.cookie('issue_board_welcome_hidden', 'true', {
|
||||
expires: 365 * 10
|
||||
});
|
||||
},
|
||||
welcomeIsHidden () {
|
||||
return $.cookie('issue_board_welcome_hidden') === 'true';
|
||||
},
|
||||
removeList (id, type = 'blank') {
|
||||
const list = this.findList('id', id, type);
|
||||
|
||||
if (!list) return;
|
||||
|
||||
this.state.lists = this.state.lists.filter( list => list.id !== id );
|
||||
},
|
||||
moveList (listFrom, orderLists) {
|
||||
orderLists.forEach((id, i) => {
|
||||
const list = this.findList('id', parseInt(id));
|
||||
|
||||
list.position = i;
|
||||
});
|
||||
listFrom.update();
|
||||
},
|
||||
moveIssueToList (listFrom, listTo, issue) {
|
||||
const issueTo = listTo.findIssue(issue.id),
|
||||
issueLists = issue.getLists(),
|
||||
listLabels = issueLists.map( listIssue => listIssue.label );
|
||||
|
||||
// Add to new lists issues if it doesn't already exist
|
||||
if (!issueTo) {
|
||||
listTo.addIssue(issue, listFrom);
|
||||
}
|
||||
|
||||
if (listTo.type === 'done' && listFrom.type !== 'backlog') {
|
||||
issueLists.forEach((list) => {
|
||||
list.removeIssue(issue);
|
||||
})
|
||||
issue.removeLabels(listLabels);
|
||||
} else {
|
||||
listFrom.removeIssue(issue);
|
||||
}
|
||||
},
|
||||
findList (key, val, type = 'label') {
|
||||
return this.state.lists.filter((list) => {
|
||||
const byType = type ? list['type'] === type : true;
|
||||
|
||||
return list[key] === val && byType;
|
||||
})[0];
|
||||
},
|
||||
updateFiltersUrl () {
|
||||
history.pushState(null, null, `?${$.param(this.state.filters)}`);
|
||||
}
|
||||
};
|
||||
})();
|
|
@ -0,0 +1,119 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
function simulateEvent(el, type, options) {
|
||||
var event;
|
||||
if (!el) return;
|
||||
var ownerDocument = el.ownerDocument;
|
||||
|
||||
options = options || {};
|
||||
|
||||
if (/^mouse/.test(type)) {
|
||||
event = ownerDocument.createEvent('MouseEvents');
|
||||
event.initMouseEvent(type, true, true, ownerDocument.defaultView,
|
||||
options.button, options.screenX, options.screenY, options.clientX, options.clientY,
|
||||
options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
|
||||
} else {
|
||||
event = ownerDocument.createEvent('CustomEvent');
|
||||
|
||||
event.initCustomEvent(type, true, true, ownerDocument.defaultView,
|
||||
options.button, options.screenX, options.screenY, options.clientX, options.clientY,
|
||||
options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
|
||||
|
||||
event.dataTransfer = {
|
||||
data: {},
|
||||
|
||||
setData: function (type, val) {
|
||||
this.data[type] = val;
|
||||
},
|
||||
|
||||
getData: function (type) {
|
||||
return this.data[type];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (el.dispatchEvent) {
|
||||
el.dispatchEvent(event);
|
||||
} else if (el.fireEvent) {
|
||||
el.fireEvent('on' + type, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
function getTraget(target) {
|
||||
var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
|
||||
var children = el.children;
|
||||
|
||||
return (
|
||||
children[target.index] ||
|
||||
children[target.index === 'first' ? 0 : -1] ||
|
||||
children[target.index === 'last' ? children.length - 1 : -1]
|
||||
);
|
||||
}
|
||||
|
||||
function getRect(el) {
|
||||
var rect = el.getBoundingClientRect();
|
||||
var width = rect.right - rect.left;
|
||||
var height = rect.bottom - rect.top;
|
||||
|
||||
return {
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
cx: rect.left + width / 2,
|
||||
cy: rect.top + height / 2,
|
||||
w: width,
|
||||
h: height,
|
||||
hw: width / 2,
|
||||
wh: height / 2
|
||||
};
|
||||
}
|
||||
|
||||
function simulateDrag(options, callback) {
|
||||
options.to.el = options.to.el || options.from.el;
|
||||
|
||||
var fromEl = getTraget(options.from);
|
||||
var toEl = getTraget(options.to);
|
||||
var scrollable = options.scrollable;
|
||||
|
||||
var fromRect = getRect(fromEl);
|
||||
var toRect = getRect(toEl);
|
||||
|
||||
var startTime = new Date().getTime();
|
||||
var duration = options.duration || 1000;
|
||||
simulateEvent(fromEl, 'mousedown', {button: 0});
|
||||
options.ontap && options.ontap();
|
||||
window.SIMULATE_DRAG_ACTIVE = 1;
|
||||
|
||||
var dragInterval = setInterval(function loop() {
|
||||
var progress = (new Date().getTime() - startTime) / duration;
|
||||
var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
|
||||
var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop;
|
||||
var overEl = fromEl.ownerDocument.elementFromPoint(x, y);
|
||||
|
||||
simulateEvent(overEl, 'mousemove', {
|
||||
clientX: x,
|
||||
clientY: y
|
||||
});
|
||||
|
||||
if (progress >= 1) {
|
||||
options.ondragend && options.ondragend();
|
||||
simulateEvent(toEl, 'mouseup');
|
||||
clearInterval(dragInterval);
|
||||
window.SIMULATE_DRAG_ACTIVE = 0;
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return {
|
||||
target: fromEl,
|
||||
fromList: fromEl.parentNode,
|
||||
toList: toEl.parentNode
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Export
|
||||
window.simulateEvent = simulateEvent;
|
||||
window.simulateDrag = simulateDrag;
|
||||
})();
|
|
@ -0,0 +1,10 @@
|
|||
Vue.http.interceptors.push((request, next) => {
|
||||
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
|
||||
|
||||
Vue.nextTick(() => {
|
||||
setTimeout(() => {
|
||||
Vue.activeResources--;
|
||||
}, 500);
|
||||
});
|
||||
next();
|
||||
});
|
|
@ -6,19 +6,26 @@
|
|||
|
||||
Build.state = null;
|
||||
|
||||
function Build(page_url, build_url, build_status, state1) {
|
||||
this.page_url = page_url;
|
||||
this.build_url = build_url;
|
||||
this.build_status = build_status;
|
||||
this.state = state1;
|
||||
function Build(options) {
|
||||
this.page_url = options.page_url;
|
||||
this.build_url = options.build_url;
|
||||
this.build_status = options.build_status;
|
||||
this.state = options.state1;
|
||||
this.build_stage = options.build_stage;
|
||||
this.hideSidebar = bind(this.hideSidebar, this);
|
||||
this.toggleSidebar = bind(this.toggleSidebar, this);
|
||||
this.updateDropdown = bind(this.updateDropdown, this);
|
||||
clearInterval(Build.interval);
|
||||
this.bp = Breakpoints.get();
|
||||
this.hideSidebar();
|
||||
$('.js-build-sidebar').niceScroll();
|
||||
|
||||
this.populateJobs(this.build_stage);
|
||||
this.updateStageDropdownText(this.build_stage);
|
||||
this.hideSidebar();
|
||||
|
||||
$(document).off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
|
||||
$(window).off('resize.build').on('resize.build', this.hideSidebar);
|
||||
$(document).off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
|
||||
this.updateArtifactRemoveDate();
|
||||
if ($('#build-trace').length) {
|
||||
this.getInitialBuildTrace();
|
||||
|
@ -132,6 +139,22 @@
|
|||
}
|
||||
};
|
||||
|
||||
Build.prototype.populateJobs = function(stage) {
|
||||
$('.build-job').hide();
|
||||
$('.build-job[data-stage="' + stage + '"]').show();
|
||||
};
|
||||
|
||||
Build.prototype.updateStageDropdownText = function(stage) {
|
||||
$('.stage-selection').text(stage);
|
||||
};
|
||||
|
||||
Build.prototype.updateDropdown = function(e) {
|
||||
e.preventDefault();
|
||||
var stage = e.currentTarget.text;
|
||||
this.updateStageDropdownText(stage);
|
||||
this.populateJobs(stage);
|
||||
};
|
||||
|
||||
return Build;
|
||||
|
||||
})();
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
|
||||
$(function() {
|
||||
var clipboard;
|
||||
|
||||
clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
|
||||
clipboard.on('success', genericSuccess);
|
||||
return clipboard.on('error', genericError);
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
(function (w) {
|
||||
class CreateLabelDropdown {
|
||||
constructor ($el, projectId) {
|
||||
this.$el = $el;
|
||||
this.projectId = projectId;
|
||||
this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
|
||||
this.$cancelButton = $('.js-cancel-label-btn', this.$el);
|
||||
this.$newLabelField = $('#new_label_name', this.$el);
|
||||
this.$newColorField = $('#new_label_color', this.$el);
|
||||
this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el);
|
||||
this.$newLabelError = $('.js-label-error', this.$el);
|
||||
this.$newLabelCreateButton = $('.js-new-label-btn', this.$el);
|
||||
this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el);
|
||||
|
||||
this.$newLabelError.hide();
|
||||
this.$newLabelCreateButton.disable();
|
||||
|
||||
this.cleanBinding();
|
||||
this.addBinding();
|
||||
}
|
||||
|
||||
cleanBinding () {
|
||||
this.$colorSuggestions.off('click');
|
||||
this.$newLabelField.off('keyup change');
|
||||
this.$newColorField.off('keyup change');
|
||||
this.$dropdownBack.off('click');
|
||||
this.$cancelButton.off('click');
|
||||
this.$newLabelCreateButton.off('click');
|
||||
}
|
||||
|
||||
addBinding () {
|
||||
const self = this;
|
||||
|
||||
this.$colorSuggestions.on('click', function (e) {
|
||||
const $this = $(this);
|
||||
self.addColorValue(e, $this);
|
||||
});
|
||||
|
||||
this.$newLabelField.on('keyup change', this.enableLabelCreateButton.bind(this));
|
||||
this.$newColorField.on('keyup change', this.enableLabelCreateButton.bind(this));
|
||||
|
||||
this.$dropdownBack.on('click', this.resetForm.bind(this));
|
||||
|
||||
this.$cancelButton.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
self.resetForm();
|
||||
self.$dropdownBack.trigger('click');
|
||||
});
|
||||
|
||||
this.$newLabelCreateButton.on('click', this.saveLabel.bind(this));
|
||||
}
|
||||
|
||||
addColorValue (e, $this) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.$newColorField.val($this.data('color')).trigger('change');
|
||||
this.$colorPreview
|
||||
.css('background-color', $this.data('color'))
|
||||
.parent()
|
||||
.addClass('is-active');
|
||||
}
|
||||
|
||||
enableLabelCreateButton () {
|
||||
if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') {
|
||||
this.$newLabelError.hide();
|
||||
this.$newLabelCreateButton.enable();
|
||||
} else {
|
||||
this.$newLabelCreateButton.disable();
|
||||
}
|
||||
}
|
||||
|
||||
resetForm () {
|
||||
this.$newLabelField
|
||||
.val('')
|
||||
.trigger('change');
|
||||
|
||||
this.$newColorField
|
||||
.val('')
|
||||
.trigger('change');
|
||||
|
||||
this.$colorPreview
|
||||
.css('background-color', '')
|
||||
.parent()
|
||||
.removeClass('is-active');
|
||||
}
|
||||
|
||||
saveLabel (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
Api.newLabel(this.projectId, {
|
||||
name: this.$newLabelField.val(),
|
||||
color: this.$newColorField.val()
|
||||
}, (label) => {
|
||||
this.$newLabelCreateButton.enable();
|
||||
|
||||
if (label.message) {
|
||||
let errors;
|
||||
|
||||
if (typeof label.message === 'string') {
|
||||
errors = label.message;
|
||||
} else {
|
||||
errors = label.message.map(function (value, key) {
|
||||
return key + " " + value[0];
|
||||
}).join("<br/>");
|
||||
}
|
||||
|
||||
this.$newLabelError
|
||||
.html(errors)
|
||||
.show();
|
||||
} else {
|
||||
this.$dropdownBack.trigger('click');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!w.gl) {
|
||||
w.gl = {};
|
||||
}
|
||||
|
||||
gl.CreateLabelDropdown = CreateLabelDropdown;
|
||||
})(window);
|
|
@ -0,0 +1,49 @@
|
|||
((w) => {
|
||||
w.CommentAndResolveBtn = Vue.extend({
|
||||
props: {
|
||||
discussionId: String,
|
||||
textareaIsEmpty: Boolean
|
||||
},
|
||||
computed: {
|
||||
discussion: function () {
|
||||
return CommentsStore.state[this.discussionId];
|
||||
},
|
||||
showButton: function () {
|
||||
if (this.discussion) {
|
||||
return this.discussion.isResolvable();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
isDiscussionResolved: function () {
|
||||
return this.discussion.isResolved();
|
||||
},
|
||||
buttonText: function () {
|
||||
if (this.isDiscussionResolved) {
|
||||
if (this.textareaIsEmpty) {
|
||||
return "Unresolve discussion";
|
||||
} else {
|
||||
return "Comment & unresolve discussion";
|
||||
}
|
||||
} else {
|
||||
if (this.textareaIsEmpty) {
|
||||
return "Resolve discussion";
|
||||
} else {
|
||||
return "Comment & resolve discussion";
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ready: function () {
|
||||
const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`);
|
||||
this.textareaIsEmpty = $textarea.val() === '';
|
||||
|
||||
$textarea.on('input.comment-and-resolve-btn', () => {
|
||||
this.textareaIsEmpty = $textarea.val() === '';
|
||||
});
|
||||
},
|
||||
destroyed: function () {
|
||||
$(`#new-discussion-note-form-${this.discussionId} .note-textarea`).off('input.comment-and-resolve-btn');
|
||||
}
|
||||
});
|
||||
})(window);
|
|
@ -0,0 +1,188 @@
|
|||
(() => {
|
||||
JumpToDiscussion = Vue.extend({
|
||||
mixins: [DiscussionMixins],
|
||||
props: {
|
||||
discussionId: String
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
discussions: CommentsStore.state,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
discussion: function () {
|
||||
return this.discussions[this.discussionId];
|
||||
},
|
||||
allResolved: function () {
|
||||
return this.unresolvedDiscussionCount === 0;
|
||||
},
|
||||
showButton: function () {
|
||||
if (this.discussionId) {
|
||||
if (this.unresolvedDiscussionCount > 1) {
|
||||
return true;
|
||||
} else {
|
||||
return this.discussionId !== this.lastResolvedId;
|
||||
}
|
||||
} else {
|
||||
return this.unresolvedDiscussionCount >= 1;
|
||||
}
|
||||
},
|
||||
lastResolvedId: function () {
|
||||
let lastId;
|
||||
for (const discussionId in this.discussions) {
|
||||
const discussion = this.discussions[discussionId];
|
||||
|
||||
if (!discussion.isResolved()) {
|
||||
lastId = discussion.id;
|
||||
}
|
||||
}
|
||||
return lastId;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
jumpToNextUnresolvedDiscussion: function () {
|
||||
let discussionsSelector,
|
||||
discussionIdsInScope,
|
||||
firstUnresolvedDiscussionId,
|
||||
nextUnresolvedDiscussionId,
|
||||
activeTab = window.mrTabs.currentAction,
|
||||
hasDiscussionsToJumpTo = true,
|
||||
jumpToFirstDiscussion = !this.discussionId;
|
||||
|
||||
const discussionIdsForElements = function(elements) {
|
||||
return elements.map(function() {
|
||||
return $(this).attr('data-discussion-id');
|
||||
}).toArray();
|
||||
};
|
||||
|
||||
const discussions = this.discussions;
|
||||
|
||||
if (activeTab === 'diffs') {
|
||||
discussionsSelector = '.diffs .notes[data-discussion-id]';
|
||||
discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
|
||||
|
||||
let unresolvedDiscussionCount = 0;
|
||||
|
||||
for (let i = 0; i < discussionIdsInScope.length; i++) {
|
||||
const discussionId = discussionIdsInScope[i];
|
||||
const discussion = discussions[discussionId];
|
||||
if (discussion && !discussion.isResolved()) {
|
||||
unresolvedDiscussionCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.discussionId && !this.discussion.isResolved()) {
|
||||
// If this is the last unresolved discussion on the diffs tab,
|
||||
// there are no discussions to jump to.
|
||||
if (unresolvedDiscussionCount === 1) {
|
||||
hasDiscussionsToJumpTo = false;
|
||||
}
|
||||
} else {
|
||||
// If there are no unresolved discussions on the diffs tab at all,
|
||||
// there are no discussions to jump to.
|
||||
if (unresolvedDiscussionCount === 0) {
|
||||
hasDiscussionsToJumpTo = false;
|
||||
}
|
||||
}
|
||||
} else if (activeTab !== 'notes') {
|
||||
// If we are on the commits or builds tabs,
|
||||
// there are no discussions to jump to.
|
||||
hasDiscussionsToJumpTo = false;
|
||||
}
|
||||
|
||||
if (!hasDiscussionsToJumpTo) {
|
||||
// If there are no discussions to jump to on the current page,
|
||||
// switch to the notes tab and jump to the first disucssion there.
|
||||
window.mrTabs.activateTab('notes');
|
||||
activeTab = 'notes';
|
||||
jumpToFirstDiscussion = true;
|
||||
}
|
||||
|
||||
if (activeTab === 'notes') {
|
||||
discussionsSelector = '.discussion[data-discussion-id]';
|
||||
discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
|
||||
}
|
||||
|
||||
let currentDiscussionFound = false;
|
||||
for (let i = 0; i < discussionIdsInScope.length; i++) {
|
||||
const discussionId = discussionIdsInScope[i];
|
||||
const discussion = discussions[discussionId];
|
||||
|
||||
if (!discussion) {
|
||||
// Discussions for comments on commits in this MR don't have a resolved status.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!firstUnresolvedDiscussionId && !discussion.isResolved()) {
|
||||
firstUnresolvedDiscussionId = discussionId;
|
||||
|
||||
if (jumpToFirstDiscussion) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!jumpToFirstDiscussion) {
|
||||
if (currentDiscussionFound) {
|
||||
if (!discussion.isResolved()) {
|
||||
nextUnresolvedDiscussionId = discussionId;
|
||||
break;
|
||||
}
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (discussionId === this.discussionId) {
|
||||
currentDiscussionFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId;
|
||||
|
||||
if (!nextUnresolvedDiscussionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`);
|
||||
|
||||
if (activeTab === 'notes') {
|
||||
$target = $target.closest('.note-discussion');
|
||||
|
||||
// If the next discussion is closed, toggle it open.
|
||||
if ($target.find('.js-toggle-content').is(':hidden')) {
|
||||
$target.find('.js-toggle-button i').trigger('click')
|
||||
}
|
||||
} else if (activeTab === 'diffs') {
|
||||
// Resolved discussions are hidden in the diffs tab by default.
|
||||
// If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab.
|
||||
// When jumping between unresolved discussions on the diffs tab, we show them.
|
||||
$target.closest(".content").show();
|
||||
|
||||
$target = $target.closest("tr.notes_holder");
|
||||
$target.show();
|
||||
|
||||
// If we are on the diffs tab, we don't scroll to the discussion itself, but to
|
||||
// 4 diff lines above it: the line the discussion was in response to + 3 context
|
||||
let prevEl;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
prevEl = $target.prev();
|
||||
|
||||
// If the discussion doesn't have 4 lines above it, we'll have to do with fewer.
|
||||
if (!prevEl.hasClass("line_holder")) {
|
||||
break;
|
||||
}
|
||||
|
||||
$target = prevEl;
|
||||
}
|
||||
}
|
||||
|
||||
$.scrollTo($target, {
|
||||
offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight())
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('jump-to-discussion', JumpToDiscussion);
|
||||
})();
|
|
@ -0,0 +1,107 @@
|
|||
((w) => {
|
||||
w.ResolveBtn = Vue.extend({
|
||||
mixins: [
|
||||
ButtonMixins
|
||||
],
|
||||
props: {
|
||||
noteId: Number,
|
||||
discussionId: String,
|
||||
resolved: Boolean,
|
||||
namespacePath: String,
|
||||
projectPath: String,
|
||||
canResolve: Boolean,
|
||||
resolvedBy: String
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
discussions: CommentsStore.state,
|
||||
loading: false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
'discussions': {
|
||||
handler: 'updateTooltip',
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
discussion: function () {
|
||||
return this.discussions[this.discussionId];
|
||||
},
|
||||
note: function () {
|
||||
if (this.discussion) {
|
||||
return this.discussion.getNote(this.noteId);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
buttonText: function () {
|
||||
if (this.isResolved) {
|
||||
return `Resolved by ${this.resolvedByName}`;
|
||||
} else if (this.canResolve) {
|
||||
return 'Mark as resolved';
|
||||
} else {
|
||||
return 'Unable to resolve';
|
||||
}
|
||||
},
|
||||
isResolved: function () {
|
||||
if (this.note) {
|
||||
return this.note.resolved;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
resolvedByName: function () {
|
||||
return this.note.resolved_by;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateTooltip: function () {
|
||||
$(this.$els.button)
|
||||
.tooltip('hide')
|
||||
.tooltip('fixTitle');
|
||||
},
|
||||
resolve: function () {
|
||||
if (!this.canResolve) return;
|
||||
|
||||
let promise;
|
||||
this.loading = true;
|
||||
|
||||
if (this.isResolved) {
|
||||
promise = ResolveService
|
||||
.unresolve(this.namespace, this.noteId);
|
||||
} else {
|
||||
promise = ResolveService
|
||||
.resolve(this.namespace, this.noteId);
|
||||
}
|
||||
|
||||
promise.then((response) => {
|
||||
this.loading = false;
|
||||
|
||||
if (response.status === 200) {
|
||||
const data = response.json();
|
||||
const resolved_by = data ? data.resolved_by : null;
|
||||
|
||||
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
|
||||
this.discussion.updateHeadline(data);
|
||||
} else {
|
||||
new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
|
||||
}
|
||||
|
||||
this.$nextTick(this.updateTooltip);
|
||||
});
|
||||
}
|
||||
},
|
||||
compiled: function () {
|
||||
$(this.$els.button).tooltip({
|
||||
container: 'body'
|
||||
});
|
||||
},
|
||||
beforeDestroy: function () {
|
||||
CommentsStore.delete(this.discussionId, this.noteId);
|
||||
},
|
||||
created: function () {
|
||||
CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy);
|
||||
}
|
||||
});
|
||||
})(window);
|
|
@ -0,0 +1,18 @@
|
|||
((w) => {
|
||||
w.ResolveCount = Vue.extend({
|
||||
mixins: [DiscussionMixins],
|
||||
props: {
|
||||
loggedOut: Boolean
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
discussions: CommentsStore.state
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
allResolved: function () {
|
||||
return this.resolvedDiscussionCount === this.discussionCount;
|
||||
}
|
||||
}
|
||||
});
|
||||
})(window);
|
|
@ -0,0 +1,60 @@
|
|||
((w) => {
|
||||
w.ResolveDiscussionBtn = Vue.extend({
|
||||
mixins: [
|
||||
ButtonMixins
|
||||
],
|
||||
props: {
|
||||
discussionId: String,
|
||||
mergeRequestId: Number,
|
||||
namespacePath: String,
|
||||
projectPath: String,
|
||||
canResolve: Boolean,
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
discussions: CommentsStore.state
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
discussion: function () {
|
||||
return this.discussions[this.discussionId];
|
||||
},
|
||||
showButton: function () {
|
||||
if (this.discussion) {
|
||||
return this.discussion.isResolvable();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
isDiscussionResolved: function () {
|
||||
if (this.discussion) {
|
||||
return this.discussion.isResolved();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
buttonText: function () {
|
||||
if (this.isDiscussionResolved) {
|
||||
return "Unresolve discussion";
|
||||
} else {
|
||||
return "Resolve discussion";
|
||||
}
|
||||
},
|
||||
loading: function () {
|
||||
if (this.discussion) {
|
||||
return this.discussion.loading;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resolve: function () {
|
||||
ResolveService.toggleResolveForDiscussion(this.namespace, this.mergeRequestId, this.discussionId);
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
CommentsStore.createDiscussion(this.discussionId, this.canResolve);
|
||||
}
|
||||
});
|
||||
})(window);
|
|
@ -0,0 +1,35 @@
|
|||
//= require vue
|
||||
//= require vue-resource
|
||||
//= require_directory ./models
|
||||
//= require_directory ./stores
|
||||
//= require_directory ./services
|
||||
//= require_directory ./mixins
|
||||
//= require_directory ./components
|
||||
|
||||
$(() => {
|
||||
window.DiffNotesApp = new Vue({
|
||||
el: '#diff-notes-app',
|
||||
components: {
|
||||
'resolve-btn': ResolveBtn,
|
||||
'resolve-discussion-btn': ResolveDiscussionBtn,
|
||||
'comment-and-resolve-btn': CommentAndResolveBtn
|
||||
},
|
||||
methods: {
|
||||
compileComponents: function () {
|
||||
const $components = $('resolve-btn, resolve-discussion-btn, jump-to-discussion');
|
||||
if ($components.length) {
|
||||
$components.each(function () {
|
||||
DiffNotesApp.$compile($(this).get(0));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
new Vue({
|
||||
el: '#resolve-count-app',
|
||||
components: {
|
||||
'resolve-count': ResolveCount
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
((w) => {
|
||||
w.DiscussionMixins = {
|
||||
computed: {
|
||||
discussionCount: function () {
|
||||
return Object.keys(this.discussions).length;
|
||||
},
|
||||
resolvedDiscussionCount: function () {
|
||||
let resolvedCount = 0;
|
||||
|
||||
for (const discussionId in this.discussions) {
|
||||
const discussion = this.discussions[discussionId];
|
||||
|
||||
if (discussion.isResolved()) {
|
||||
resolvedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedCount;
|
||||
},
|
||||
unresolvedDiscussionCount: function () {
|
||||
let unresolvedCount = 0;
|
||||
|
||||
for (const discussionId in this.discussions) {
|
||||
const discussion = this.discussions[discussionId];
|
||||
|
||||
if (!discussion.isResolved()) {
|
||||
unresolvedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return unresolvedCount;
|
||||
}
|
||||
}
|
||||
};
|
||||
})(window);
|
|
@ -0,0 +1,9 @@
|
|||
((w) => {
|
||||
w.ButtonMixins = {
|
||||
computed: {
|
||||
namespace: function () {
|
||||
return `${this.namespacePath}/${this.projectPath}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
})(window);
|
|
@ -0,0 +1,87 @@
|
|||
class DiscussionModel {
|
||||
constructor (discussionId) {
|
||||
this.id = discussionId;
|
||||
this.notes = {};
|
||||
this.loading = false;
|
||||
this.canResolve = false;
|
||||
}
|
||||
|
||||
createNote (noteId, canResolve, resolved, resolved_by) {
|
||||
Vue.set(this.notes, noteId, new NoteModel(this.id, noteId, canResolve, resolved, resolved_by));
|
||||
}
|
||||
|
||||
deleteNote (noteId) {
|
||||
Vue.delete(this.notes, noteId);
|
||||
}
|
||||
|
||||
getNote (noteId) {
|
||||
return this.notes[noteId];
|
||||
}
|
||||
|
||||
notesCount() {
|
||||
return Object.keys(this.notes).length;
|
||||
}
|
||||
|
||||
isResolved () {
|
||||
for (const noteId in this.notes) {
|
||||
const note = this.notes[noteId];
|
||||
|
||||
if (!note.resolved) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
resolveAllNotes (resolved_by) {
|
||||
for (const noteId in this.notes) {
|
||||
const note = this.notes[noteId];
|
||||
|
||||
if (!note.resolved) {
|
||||
note.resolved = true;
|
||||
note.resolved_by = resolved_by;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unResolveAllNotes () {
|
||||
for (const noteId in this.notes) {
|
||||
const note = this.notes[noteId];
|
||||
|
||||
if (note.resolved) {
|
||||
note.resolved = false;
|
||||
note.resolved_by = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateHeadline (data) {
|
||||
const $discussionHeadline = $(`.discussion[data-discussion-id="${this.id}"] .js-discussion-headline`);
|
||||
|
||||
if (data.discussion_headline_html) {
|
||||
if ($discussionHeadline.length) {
|
||||
$discussionHeadline.replaceWith(data.discussion_headline_html);
|
||||
} else {
|
||||
$(`.discussion[data-discussion-id="${this.id}"] .discussion-header`).append(data.discussion_headline_html);
|
||||
}
|
||||
} else {
|
||||
$discussionHeadline.remove();
|
||||
}
|
||||
}
|
||||
|
||||
isResolvable () {
|
||||
if (!this.canResolve) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const noteId in this.notes) {
|
||||
const note = this.notes[noteId];
|
||||
|
||||
if (note.canResolve) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
class NoteModel {
|
||||
constructor (discussionId, noteId, canResolve, resolved, resolved_by) {
|
||||
this.discussionId = discussionId;
|
||||
this.id = noteId;
|
||||
this.canResolve = canResolve;
|
||||
this.resolved = resolved;
|
||||
this.resolved_by = resolved_by;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
((w) => {
|
||||
class ResolveServiceClass {
|
||||
constructor() {
|
||||
this.noteResource = Vue.resource('notes{/noteId}/resolve');
|
||||
this.discussionResource = Vue.resource('merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve');
|
||||
}
|
||||
|
||||
setCSRF() {
|
||||
Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken();
|
||||
}
|
||||
|
||||
prepareRequest(namespace) {
|
||||
this.setCSRF();
|
||||
Vue.http.options.root = `/${namespace}`;
|
||||
}
|
||||
|
||||
resolve(namespace, noteId) {
|
||||
this.prepareRequest(namespace);
|
||||
|
||||
return this.noteResource.save({ noteId }, {});
|
||||
}
|
||||
|
||||
unresolve(namespace, noteId) {
|
||||
this.prepareRequest(namespace);
|
||||
|
||||
return this.noteResource.delete({ noteId }, {});
|
||||
}
|
||||
|
||||
toggleResolveForDiscussion(namespace, mergeRequestId, discussionId) {
|
||||
const discussion = CommentsStore.state[discussionId],
|
||||
isResolved = discussion.isResolved();
|
||||
let promise;
|
||||
|
||||
if (isResolved) {
|
||||
promise = this.unResolveAll(namespace, mergeRequestId, discussionId);
|
||||
} else {
|
||||
promise = this.resolveAll(namespace, mergeRequestId, discussionId);
|
||||
}
|
||||
|
||||
promise.then((response) => {
|
||||
discussion.loading = false;
|
||||
|
||||
if (response.status === 200) {
|
||||
const data = response.json();
|
||||
const resolved_by = data ? data.resolved_by : null;
|
||||
|
||||
if (isResolved) {
|
||||
discussion.unResolveAllNotes();
|
||||
} else {
|
||||
discussion.resolveAllNotes(resolved_by);
|
||||
}
|
||||
|
||||
discussion.updateHeadline(data);
|
||||
} else {
|
||||
new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
resolveAll(namespace, mergeRequestId, discussionId) {
|
||||
const discussion = CommentsStore.state[discussionId];
|
||||
|
||||
this.prepareRequest(namespace);
|
||||
|
||||
discussion.loading = true;
|
||||
|
||||
return this.discussionResource.save({
|
||||
mergeRequestId,
|
||||
discussionId
|
||||
}, {});
|
||||
}
|
||||
|
||||
unResolveAll(namespace, mergeRequestId, discussionId) {
|
||||
const discussion = CommentsStore.state[discussionId];
|
||||
|
||||
this.prepareRequest(namespace);
|
||||
|
||||
discussion.loading = true;
|
||||
|
||||
return this.discussionResource.delete({
|
||||
mergeRequestId,
|
||||
discussionId
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
w.ResolveService = new ResolveServiceClass();
|
||||
})(window);
|
|
@ -0,0 +1,53 @@
|
|||
((w) => {
|
||||
w.CommentsStore = {
|
||||
state: {},
|
||||
get: function (discussionId, noteId) {
|
||||
return this.state[discussionId].getNote(noteId);
|
||||
},
|
||||
createDiscussion: function (discussionId, canResolve) {
|
||||
let discussion = this.state[discussionId];
|
||||
if (!this.state[discussionId]) {
|
||||
discussion = new DiscussionModel(discussionId);
|
||||
Vue.set(this.state, discussionId, discussion);
|
||||
}
|
||||
|
||||
if (canResolve !== undefined) {
|
||||
discussion.canResolve = canResolve;
|
||||
}
|
||||
|
||||
return discussion;
|
||||
},
|
||||
create: function (discussionId, noteId, canResolve, resolved, resolved_by) {
|
||||
const discussion = this.createDiscussion(discussionId);
|
||||
|
||||
discussion.createNote(noteId, canResolve, resolved, resolved_by);
|
||||
},
|
||||
update: function (discussionId, noteId, resolved, resolved_by) {
|
||||
const discussion = this.state[discussionId];
|
||||
const note = discussion.getNote(noteId);
|
||||
note.resolved = resolved;
|
||||
note.resolved_by = resolved_by;
|
||||
},
|
||||
delete: function (discussionId, noteId) {
|
||||
const discussion = this.state[discussionId];
|
||||
discussion.deleteNote(noteId);
|
||||
|
||||
if (discussion.notesCount() === 0) {
|
||||
Vue.delete(this.state, discussionId);
|
||||
}
|
||||
},
|
||||
unresolvedDiscussionIds: function () {
|
||||
let ids = [];
|
||||
|
||||
for (const discussionId in this.state) {
|
||||
const discussion = this.state[discussionId];
|
||||
|
||||
if (!discussion.isResolved()) {
|
||||
ids.push(discussion.id);
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
};
|
||||
})(window);
|
|
@ -55,6 +55,7 @@
|
|||
shortcut_handler = new ShortcutsNavigation();
|
||||
new GLForm($('.issue-form'));
|
||||
new IssuableForm($('.issue-form'));
|
||||
new IssuableTemplateSelectors();
|
||||
break;
|
||||
case 'projects:merge_requests:new':
|
||||
case 'projects:merge_requests:edit':
|
||||
|
@ -62,6 +63,7 @@
|
|||
shortcut_handler = new ShortcutsNavigation();
|
||||
new GLForm($('.merge-request-form'));
|
||||
new IssuableForm($('.merge-request-form'));
|
||||
new IssuableTemplateSelectors();
|
||||
break;
|
||||
case 'projects:tags:new':
|
||||
new ZenMode();
|
||||
|
@ -86,6 +88,8 @@
|
|||
new ZenMode();
|
||||
new MergedButtons();
|
||||
break;
|
||||
case "projects:merge_requests:conflicts":
|
||||
window.mcui = new MergeConflictResolver()
|
||||
case 'projects:merge_requests:index':
|
||||
shortcut_handler = new ShortcutsNavigation();
|
||||
Issuable.init();
|
||||
|
@ -192,6 +196,9 @@
|
|||
case 'edit':
|
||||
new Labels();
|
||||
}
|
||||
case 'abuse_reports':
|
||||
new gl.AbuseReports();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'dashboard':
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
this.render = bind(this.render, this);
|
||||
this.VIEW_TYPE = $('input#view[type=hidden]').val();
|
||||
debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION);
|
||||
$(document).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy);
|
||||
$(this.filesContainerElement).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy);
|
||||
}
|
||||
|
||||
FilesCommentButton.prototype.render = function(e) {
|
||||
|
|
|
@ -223,7 +223,7 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
return this.input.atwho({
|
||||
this.input.atwho({
|
||||
at: '~',
|
||||
alias: 'labels',
|
||||
searchKey: 'search',
|
||||
|
@ -249,6 +249,68 @@
|
|||
}
|
||||
}
|
||||
});
|
||||
// We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
|
||||
this.input.filter('[data-supports-slash-commands="true"]').atwho({
|
||||
at: '/',
|
||||
alias: 'commands',
|
||||
searchKey: 'search',
|
||||
displayTpl: function(value) {
|
||||
var tpl = '<li>/${name}';
|
||||
if (value.aliases.length > 0) {
|
||||
tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
|
||||
}
|
||||
if (value.params.length > 0) {
|
||||
tpl += ' <small><%- params.join(" ") %></small>';
|
||||
}
|
||||
if (value.description !== '') {
|
||||
tpl += '<small class="description"><i><%- description %></i></small>';
|
||||
}
|
||||
tpl += '</li>';
|
||||
return _.template(tpl)(value);
|
||||
},
|
||||
insertTpl: function(value) {
|
||||
var tpl = "/${name} ";
|
||||
var reference_prefix = null;
|
||||
if (value.params.length > 0) {
|
||||
reference_prefix = value.params[0][0];
|
||||
if (/^[@%~]/.test(reference_prefix)) {
|
||||
tpl += '<%- reference_prefix %>';
|
||||
}
|
||||
}
|
||||
return _.template(tpl)({ reference_prefix: reference_prefix });
|
||||
},
|
||||
suffix: '',
|
||||
callbacks: {
|
||||
sorter: this.DefaultOptions.sorter,
|
||||
filter: this.DefaultOptions.filter,
|
||||
beforeInsert: this.DefaultOptions.beforeInsert,
|
||||
beforeSave: function(commands) {
|
||||
return $.map(commands, function(c) {
|
||||
var search = c.name;
|
||||
if (c.aliases.length > 0) {
|
||||
search = search + " " + c.aliases.join(" ");
|
||||
}
|
||||
return {
|
||||
name: c.name,
|
||||
aliases: c.aliases,
|
||||
params: c.params,
|
||||
description: c.description,
|
||||
search: search
|
||||
};
|
||||
});
|
||||
},
|
||||
matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
|
||||
var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi
|
||||
var match = regexp.exec(subtext);
|
||||
if (match) {
|
||||
return match[1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
},
|
||||
destroyAtWho: function() {
|
||||
return this.input.atwho('destroy');
|
||||
|
@ -265,6 +327,7 @@
|
|||
this.input.atwho('load', 'mergerequests', data.mergerequests);
|
||||
this.input.atwho('load', ':', data.emojis);
|
||||
this.input.atwho('load', '~', data.labels);
|
||||
this.input.atwho('load', '/', data.commands);
|
||||
return $(':focus').trigger('keyup');
|
||||
}
|
||||
};
|
|
@ -4,7 +4,7 @@
|
|||
var _this;
|
||||
_this = this;
|
||||
$('.js-label-select').each(function(i, dropdown) {
|
||||
var $block, $colorPreview, $dropdown, $form, $loading, $newLabelCreateButton, $newLabelError, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, newColorField, newLabelField, projectId, resetForm, saveLabel, saveLabelData, selectedLabel, showAny, showNo;
|
||||
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo;
|
||||
$dropdown = $(dropdown);
|
||||
projectId = $dropdown.data('project-id');
|
||||
labelUrl = $dropdown.data('labels');
|
||||
|
@ -13,8 +13,6 @@
|
|||
if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) {
|
||||
selectedLabel = selectedLabel.split(',');
|
||||
}
|
||||
newLabelField = $('#new_label_name');
|
||||
newColorField = $('#new_label_color');
|
||||
showNo = $dropdown.data('show-no');
|
||||
showAny = $dropdown.data('show-any');
|
||||
defaultLabel = $dropdown.data('default-label');
|
||||
|
@ -24,10 +22,6 @@
|
|||
$form = $dropdown.closest('form');
|
||||
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
|
||||
$value = $block.find('.value');
|
||||
$newLabelError = $('.js-label-error');
|
||||
$colorPreview = $('.js-dropdown-label-color-preview');
|
||||
$newLabelCreateButton = $('.js-new-label-btn');
|
||||
$newLabelError.hide();
|
||||
$loading = $block.find('.block-loading').fadeOut();
|
||||
if (issueUpdateURL != null) {
|
||||
issueURLSplit = issueUpdateURL.split('/');
|
||||
|
@ -36,60 +30,9 @@
|
|||
labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
|
||||
labelNoneHTMLTemplate = '<span class="no-value">None</span>';
|
||||
}
|
||||
if (newLabelField.length) {
|
||||
$('.suggest-colors-dropdown a').on("click", function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
newColorField.val($(this).data('color')).trigger('change');
|
||||
return $colorPreview.css('background-color', $(this).data('color')).parent().addClass('is-active');
|
||||
});
|
||||
resetForm = function() {
|
||||
newLabelField.val('').trigger('change');
|
||||
newColorField.val('').trigger('change');
|
||||
return $colorPreview.css('background-color', '').parent().removeClass('is-active');
|
||||
};
|
||||
$('.dropdown-menu-back').on('click', function() {
|
||||
return resetForm();
|
||||
});
|
||||
$('.js-cancel-label-btn').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
resetForm();
|
||||
return $('.dropdown-menu-back', $dropdown.parent()).trigger('click');
|
||||
});
|
||||
enableLabelCreateButton = function() {
|
||||
if (newLabelField.val() !== '' && newColorField.val() !== '') {
|
||||
$newLabelError.hide();
|
||||
return $newLabelCreateButton.enable();
|
||||
} else {
|
||||
return $newLabelCreateButton.disable();
|
||||
}
|
||||
};
|
||||
saveLabel = function() {
|
||||
return Api.newLabel(projectId, {
|
||||
name: newLabelField.val(),
|
||||
color: newColorField.val()
|
||||
}, function(label) {
|
||||
var errors;
|
||||
$newLabelCreateButton.enable();
|
||||
if (label.message != null) {
|
||||
errors = _.map(label.message, function(value, key) {
|
||||
return key + " " + value[0];
|
||||
});
|
||||
return $newLabelError.html(errors.join("<br/>")).show();
|
||||
} else {
|
||||
return $('.dropdown-menu-back', $dropdown.parent()).trigger('click');
|
||||
}
|
||||
});
|
||||
};
|
||||
newLabelField.on('keyup change', enableLabelCreateButton);
|
||||
newColorField.on('keyup change', enableLabelCreateButton);
|
||||
$newLabelCreateButton.disable().on('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return saveLabel();
|
||||
});
|
||||
}
|
||||
|
||||
new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId);
|
||||
|
||||
saveLabelData = function() {
|
||||
var data, selected;
|
||||
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() {
|
||||
|
@ -270,6 +213,9 @@
|
|||
isMRIndex = page === 'projects:merge_requests:index';
|
||||
$selectbox.hide();
|
||||
$value.removeAttr('style');
|
||||
if (page === 'projects:boards:show') {
|
||||
return;
|
||||
}
|
||||
if ($dropdown.hasClass('js-multiselect')) {
|
||||
if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
|
||||
selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
|
||||
|
@ -289,7 +235,7 @@
|
|||
}
|
||||
},
|
||||
multiSelect: $dropdown.hasClass('js-multiselect'),
|
||||
clicked: function(label) {
|
||||
clicked: function(label, $el, e) {
|
||||
var isIssueIndex, isMRIndex, page;
|
||||
_this.enableBulkLabelDropdown();
|
||||
if ($dropdown.hasClass('js-filter-bulk-update')) {
|
||||
|
@ -298,7 +244,23 @@
|
|||
page = $('body').data('page');
|
||||
isIssueIndex = page === 'projects:issues:index';
|
||||
isMRIndex = page === 'projects:merge_requests:index';
|
||||
if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
|
||||
if (page === 'projects:boards:show') {
|
||||
if (label.isAny) {
|
||||
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
|
||||
} else if (label.title) {
|
||||
gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title);
|
||||
} else {
|
||||
var filters = gl.issueBoards.BoardsStore.state.filters['label_name'];
|
||||
filters = filters.filter(function (label) {
|
||||
return label !== $el.text().trim();
|
||||
});
|
||||
gl.issueBoards.BoardsStore.state.filters['label_name'] = filters;
|
||||
}
|
||||
|
||||
gl.issueBoards.BoardsStore.updateFiltersUrl();
|
||||
e.preventDefault();
|
||||
return;
|
||||
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
|
||||
if (!$dropdown.hasClass('js-multiselect')) {
|
||||
selectedLabel = label.title;
|
||||
return Issuable.filterResults($dropdown.closest('form'));
|
||||
|
|
|
@ -104,9 +104,12 @@
|
|||
return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
|
||||
});
|
||||
};
|
||||
return gl.text.removeListeners = function(form) {
|
||||
gl.text.removeListeners = function(form) {
|
||||
return $('.js-md', form).off();
|
||||
};
|
||||
return gl.text.truncate = function(string, maxLength) {
|
||||
return string.substr(0, (maxLength - 3)) + '...';
|
||||
};
|
||||
})(window);
|
||||
|
||||
}).call(this);
|
||||
|
|
|
@ -0,0 +1,341 @@
|
|||
const HEAD_HEADER_TEXT = 'HEAD//our changes';
|
||||
const ORIGIN_HEADER_TEXT = 'origin//their changes';
|
||||
const HEAD_BUTTON_TITLE = 'Use ours';
|
||||
const ORIGIN_BUTTON_TITLE = 'Use theirs';
|
||||
|
||||
|
||||
class MergeConflictDataProvider {
|
||||
|
||||
getInitialData() {
|
||||
const diffViewType = $.cookie('diff_view');
|
||||
|
||||
return {
|
||||
isLoading : true,
|
||||
hasError : false,
|
||||
isParallel : diffViewType === 'parallel',
|
||||
diffViewType : diffViewType,
|
||||
isSubmitting : false,
|
||||
conflictsData : {},
|
||||
resolutionData : {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
decorateData(vueInstance, data) {
|
||||
this.vueInstance = vueInstance;
|
||||
|
||||
if (data.type === 'error') {
|
||||
vueInstance.hasError = true;
|
||||
data.errorMessage = data.message;
|
||||
}
|
||||
else {
|
||||
data.shortCommitSha = data.commit_sha.slice(0, 7);
|
||||
data.commitMessage = data.commit_message;
|
||||
|
||||
this.setParallelLines(data);
|
||||
this.setInlineLines(data);
|
||||
this.updateResolutionsData(data);
|
||||
}
|
||||
|
||||
vueInstance.conflictsData = data;
|
||||
vueInstance.isSubmitting = false;
|
||||
|
||||
const conflictsText = this.getConflictsCount() > 1 ? 'conflicts' : 'conflict';
|
||||
vueInstance.conflictsData.conflictsText = conflictsText;
|
||||
}
|
||||
|
||||
|
||||
updateResolutionsData(data) {
|
||||
const vi = this.vueInstance;
|
||||
|
||||
data.files.forEach( (file) => {
|
||||
file.sections.forEach( (section) => {
|
||||
if (section.conflict) {
|
||||
vi.$set(`resolutionData['${section.id}']`, false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
setParallelLines(data) {
|
||||
data.files.forEach( (file) => {
|
||||
file.filePath = this.getFilePath(file);
|
||||
file.iconClass = `fa-${file.blob_icon}`;
|
||||
file.blobPath = file.blob_path;
|
||||
file.parallelLines = [];
|
||||
const linesObj = { left: [], right: [] };
|
||||
|
||||
file.sections.forEach( (section) => {
|
||||
const { conflict, lines, id } = section;
|
||||
|
||||
if (conflict) {
|
||||
linesObj.left.push(this.getOriginHeaderLine(id));
|
||||
linesObj.right.push(this.getHeadHeaderLine(id));
|
||||
}
|
||||
|
||||
lines.forEach( (line) => {
|
||||
const { type } = line;
|
||||
|
||||
if (conflict) {
|
||||
if (type === 'old') {
|
||||
linesObj.left.push(this.getLineForParallelView(line, id, 'conflict'));
|
||||
}
|
||||
else if (type === 'new') {
|
||||
linesObj.right.push(this.getLineForParallelView(line, id, 'conflict', true));
|
||||
}
|
||||
}
|
||||
else {
|
||||
const lineType = type || 'context';
|
||||
|
||||
linesObj.left.push (this.getLineForParallelView(line, id, lineType));
|
||||
linesObj.right.push(this.getLineForParallelView(line, id, lineType, true));
|
||||
}
|
||||
});
|
||||
|
||||
this.checkLineLengths(linesObj);
|
||||
});
|
||||
|
||||
for (let i = 0, len = linesObj.left.length; i < len; i++) {
|
||||
file.parallelLines.push([
|
||||
linesObj.right[i],
|
||||
linesObj.left[i]
|
||||
]);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
checkLineLengths(linesObj) {
|
||||
let { left, right } = linesObj;
|
||||
|
||||
if (left.length !== right.length) {
|
||||
if (left.length > right.length) {
|
||||
const diff = left.length - right.length;
|
||||
for (let i = 0; i < diff; i++) {
|
||||
right.push({ lineType: 'emptyLine', richText: '' });
|
||||
}
|
||||
}
|
||||
else {
|
||||
const diff = right.length - left.length;
|
||||
for (let i = 0; i < diff; i++) {
|
||||
left.push({ lineType: 'emptyLine', richText: '' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setInlineLines(data) {
|
||||
data.files.forEach( (file) => {
|
||||
file.iconClass = `fa-${file.blob_icon}`;
|
||||
file.blobPath = file.blob_path;
|
||||
file.filePath = this.getFilePath(file);
|
||||
file.inlineLines = []
|
||||
|
||||
file.sections.forEach( (section) => {
|
||||
let currentLineType = 'new';
|
||||
const { conflict, lines, id } = section;
|
||||
|
||||
if (conflict) {
|
||||
file.inlineLines.push(this.getHeadHeaderLine(id));
|
||||
}
|
||||
|
||||
lines.forEach( (line) => {
|
||||
const { type } = line;
|
||||
|
||||
if ((type === 'new' || type === 'old') && currentLineType !== type) {
|
||||
currentLineType = type;
|
||||
file.inlineLines.push({ lineType: 'emptyLine', richText: '' });
|
||||
}
|
||||
|
||||
this.decorateLineForInlineView(line, id, conflict);
|
||||
file.inlineLines.push(line);
|
||||
})
|
||||
|
||||
if (conflict) {
|
||||
file.inlineLines.push(this.getOriginHeaderLine(id));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
handleSelected(sectionId, selection) {
|
||||
const vi = this.vueInstance;
|
||||
|
||||
vi.resolutionData[sectionId] = selection;
|
||||
vi.conflictsData.files.forEach( (file) => {
|
||||
file.inlineLines.forEach( (line) => {
|
||||
if (line.id === sectionId && (line.hasConflict || line.isHeader)) {
|
||||
this.markLine(line, selection);
|
||||
}
|
||||
});
|
||||
|
||||
file.parallelLines.forEach( (lines) => {
|
||||
const left = lines[0];
|
||||
const right = lines[1];
|
||||
const hasSameId = right.id === sectionId || left.id === sectionId;
|
||||
const isLeftMatch = left.hasConflict || left.isHeader;
|
||||
const isRightMatch = right.hasConflict || right.isHeader;
|
||||
|
||||
if (hasSameId && (isLeftMatch || isRightMatch)) {
|
||||
this.markLine(left, selection);
|
||||
this.markLine(right, selection);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
updateViewType(newType) {
|
||||
const vi = this.vueInstance;
|
||||
|
||||
if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) {
|
||||
return;
|
||||
}
|
||||
|
||||
vi.diffView = newType;
|
||||
vi.isParallel = newType === 'parallel';
|
||||
$.cookie('diff_view', newType); // TODO: Make sure that cookie path added.
|
||||
$('.content-wrapper .container-fluid').toggleClass('container-limited');
|
||||
}
|
||||
|
||||
|
||||
markLine(line, selection) {
|
||||
if (selection === 'head' && line.isHead) {
|
||||
line.isSelected = true;
|
||||
line.isUnselected = false;
|
||||
}
|
||||
else if (selection === 'origin' && line.isOrigin) {
|
||||
line.isSelected = true;
|
||||
line.isUnselected = false;
|
||||
}
|
||||
else {
|
||||
line.isSelected = false;
|
||||
line.isUnselected = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getConflictsCount() {
|
||||
return Object.keys(this.vueInstance.resolutionData).length;
|
||||
}
|
||||
|
||||
|
||||
getResolvedCount() {
|
||||
let count = 0;
|
||||
const data = this.vueInstance.resolutionData;
|
||||
|
||||
for (const id in data) {
|
||||
const resolution = data[id];
|
||||
if (resolution) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
|
||||
isReadyToCommit() {
|
||||
const { conflictsData, isSubmitting } = this.vueInstance
|
||||
const allResolved = this.getConflictsCount() === this.getResolvedCount();
|
||||
const hasCommitMessage = $.trim(conflictsData.commitMessage).length;
|
||||
|
||||
return !isSubmitting && hasCommitMessage && allResolved;
|
||||
}
|
||||
|
||||
|
||||
getCommitButtonText() {
|
||||
const initial = 'Commit conflict resolution';
|
||||
const inProgress = 'Committing...';
|
||||
const vue = this.vueInstance;
|
||||
|
||||
return vue ? vue.isSubmitting ? inProgress : initial : initial;
|
||||
}
|
||||
|
||||
|
||||
decorateLineForInlineView(line, id, conflict) {
|
||||
const { type } = line;
|
||||
line.id = id;
|
||||
line.hasConflict = conflict;
|
||||
line.isHead = type === 'new';
|
||||
line.isOrigin = type === 'old';
|
||||
line.hasMatch = type === 'match';
|
||||
line.richText = line.rich_text;
|
||||
line.isSelected = false;
|
||||
line.isUnselected = false;
|
||||
}
|
||||
|
||||
getLineForParallelView(line, id, lineType, isHead) {
|
||||
const { old_line, new_line, rich_text } = line;
|
||||
const hasConflict = lineType === 'conflict';
|
||||
|
||||
return {
|
||||
id,
|
||||
lineType,
|
||||
hasConflict,
|
||||
isHead : hasConflict && isHead,
|
||||
isOrigin : hasConflict && !isHead,
|
||||
hasMatch : lineType === 'match',
|
||||
lineNumber : isHead ? new_line : old_line,
|
||||
section : isHead ? 'head' : 'origin',
|
||||
richText : rich_text,
|
||||
isSelected : false,
|
||||
isUnselected : false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getHeadHeaderLine(id) {
|
||||
return {
|
||||
id : id,
|
||||
richText : HEAD_HEADER_TEXT,
|
||||
buttonTitle : HEAD_BUTTON_TITLE,
|
||||
type : 'new',
|
||||
section : 'head',
|
||||
isHeader : true,
|
||||
isHead : true,
|
||||
isSelected : false,
|
||||
isUnselected: false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getOriginHeaderLine(id) {
|
||||
return {
|
||||
id : id,
|
||||
richText : ORIGIN_HEADER_TEXT,
|
||||
buttonTitle : ORIGIN_BUTTON_TITLE,
|
||||
type : 'old',
|
||||
section : 'origin',
|
||||
isHeader : true,
|
||||
isOrigin : true,
|
||||
isSelected : false,
|
||||
isUnselected: false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
handleFailedRequest(vueInstance, data) {
|
||||
vueInstance.hasError = true;
|
||||
vueInstance.conflictsData.errorMessage = 'Something went wrong!';
|
||||
}
|
||||
|
||||
|
||||
getCommitData() {
|
||||
return {
|
||||
commit_message: this.vueInstance.conflictsData.commitMessage,
|
||||
sections: this.vueInstance.resolutionData
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getFilePath(file) {
|
||||
const { old_path, new_path } = file;
|
||||
return old_path === new_path ? new_path : `${old_path} → ${new_path}`;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
//= require vue
|
||||
|
||||
class MergeConflictResolver {
|
||||
|
||||
constructor() {
|
||||
this.dataProvider = new MergeConflictDataProvider()
|
||||
this.initVue()
|
||||
}
|
||||
|
||||
|
||||
initVue() {
|
||||
const that = this;
|
||||
this.vue = new Vue({
|
||||
el : '#conflicts',
|
||||
name : 'MergeConflictResolver',
|
||||
data : this.dataProvider.getInitialData(),
|
||||
created : this.fetchData(),
|
||||
computed : this.setComputedProperties(),
|
||||
methods : {
|
||||
handleSelected(sectionId, selection) {
|
||||
that.dataProvider.handleSelected(sectionId, selection);
|
||||
},
|
||||
handleViewTypeChange(newType) {
|
||||
that.dataProvider.updateViewType(newType);
|
||||
},
|
||||
commit() {
|
||||
that.commit();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
setComputedProperties() {
|
||||
const dp = this.dataProvider;
|
||||
|
||||
return {
|
||||
conflictsCount() { return dp.getConflictsCount() },
|
||||
resolvedCount() { return dp.getResolvedCount() },
|
||||
readyToCommit() { return dp.isReadyToCommit() },
|
||||
commitButtonText() { return dp.getCommitButtonText() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fetchData() {
|
||||
const dp = this.dataProvider;
|
||||
|
||||
$.get($('#conflicts').data('conflictsPath'))
|
||||
.done((data) => {
|
||||
dp.decorateData(this.vue, data);
|
||||
})
|
||||
.error((data) => {
|
||||
dp.handleFailedRequest(this.vue, data);
|
||||
})
|
||||
.always(() => {
|
||||
this.vue.isLoading = false;
|
||||
|
||||
this.vue.$nextTick(() => {
|
||||
$('#conflicts .js-syntax-highlight').syntaxHighlight();
|
||||
});
|
||||
|
||||
if (this.vue.diffViewType === 'parallel') {
|
||||
$('.content-wrapper .container-fluid').removeClass('container-limited');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
commit() {
|
||||
this.vue.isSubmitting = true;
|
||||
|
||||
$.post($('#conflicts').data('resolveConflictsPath'), this.dataProvider.getCommitData())
|
||||
.done((data) => {
|
||||
window.location.href = data.redirect_to;
|
||||
})
|
||||
.error(() => {
|
||||
new Flash('Something went wrong!');
|
||||
})
|
||||
.always(() => {
|
||||
this.vue.isSubmitting = false;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -34,7 +34,7 @@
|
|||
|
||||
MergeRequest.prototype.initTabs = function() {
|
||||
if (this.opts.action !== 'new') {
|
||||
return new MergeRequestTabs(this.opts);
|
||||
window.mrTabs = new MergeRequestTabs(this.opts);
|
||||
} else {
|
||||
return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show');
|
||||
}
|
||||
|
|
|
@ -9,10 +9,13 @@
|
|||
|
||||
MergeRequestTabs.prototype.buildsLoaded = false;
|
||||
|
||||
MergeRequestTabs.prototype.pipelinesLoaded = false;
|
||||
|
||||
MergeRequestTabs.prototype.commitsLoaded = false;
|
||||
|
||||
function MergeRequestTabs(opts) {
|
||||
this.opts = opts != null ? opts : {};
|
||||
this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
|
||||
this.setCurrentAction = bind(this.setCurrentAction, this);
|
||||
this.tabShown = bind(this.tabShown, this);
|
||||
this.showTab = bind(this.showTab, this);
|
||||
|
@ -50,10 +53,15 @@
|
|||
} else if (action === 'builds') {
|
||||
this.loadBuilds($target.attr('href'));
|
||||
this.expandView();
|
||||
} else if (action === 'pipelines') {
|
||||
this.loadPipelines($target.attr('href'));
|
||||
this.expandView();
|
||||
} else {
|
||||
this.expandView();
|
||||
}
|
||||
return this.setCurrentAction(action);
|
||||
if (this.opts.setUrl) {
|
||||
this.setCurrentAction(action);
|
||||
}
|
||||
};
|
||||
|
||||
MergeRequestTabs.prototype.scrollToElement = function(container) {
|
||||
|
@ -81,7 +89,8 @@
|
|||
if (action === 'show') {
|
||||
action = 'notes';
|
||||
}
|
||||
new_state = this._location.pathname.replace(/\/(commits|diffs|builds)(\.html)?\/?$/, '');
|
||||
this.currentAction = action;
|
||||
new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '');
|
||||
if (action !== 'notes') {
|
||||
new_state += "/" + action;
|
||||
}
|
||||
|
@ -119,6 +128,11 @@
|
|||
success: (function(_this) {
|
||||
return function(data) {
|
||||
$('#diffs').html(data.html);
|
||||
|
||||
if (typeof DiffNotesApp !== 'undefined') {
|
||||
DiffNotesApp.compileComponents();
|
||||
}
|
||||
|
||||
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
|
||||
$('#diffs .js-syntax-highlight').syntaxHighlight();
|
||||
$('#diffs .diff-file').singleFileDiff();
|
||||
|
@ -177,6 +191,21 @@
|
|||
});
|
||||
};
|
||||
|
||||
MergeRequestTabs.prototype.loadPipelines = function(source) {
|
||||
if (this.pipelinesLoaded) {
|
||||
return;
|
||||
}
|
||||
return this._get({
|
||||
url: source + ".json",
|
||||
success: function(data) {
|
||||
$('#pipelines').html(data.html);
|
||||
gl.utils.localTimeAgo($('.js-timeago', '#pipelines'));
|
||||
this.pipelinesLoaded = true;
|
||||
return this.scrollToElement("#pipelines");
|
||||
}.bind(this)
|
||||
});
|
||||
};
|
||||
|
||||
MergeRequestTabs.prototype.toggleLoading = function(status) {
|
||||
return $('.mr-loading-status .loading').toggle(status);
|
||||
};
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
|
||||
MergeRequestWidget.prototype.addEventListeners = function() {
|
||||
var allowedPages;
|
||||
allowedPages = ['show', 'commits', 'builds', 'changes'];
|
||||
allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'];
|
||||
return $(document).on('page:change.merge_request', (function(_this) {
|
||||
return function() {
|
||||
var page;
|
||||
|
@ -53,7 +53,7 @@
|
|||
return function(data) {
|
||||
var callback, urlSuffix;
|
||||
if (data.state === "merged") {
|
||||
urlSuffix = deleteSourceBranch ? '?delete_source=true' : '';
|
||||
urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
|
||||
return window.location.href = window.location.pathname + urlSuffix;
|
||||
} else if (data.merge_error) {
|
||||
return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>");
|
||||
|
|
|
@ -94,7 +94,7 @@
|
|||
$selectbox.hide();
|
||||
return $value.css('display', '');
|
||||
},
|
||||
clicked: function(selected) {
|
||||
clicked: function(selected, $el, e) {
|
||||
var data, isIssueIndex, isMRIndex, page;
|
||||
page = $('body').data('page');
|
||||
isIssueIndex = page === 'projects:issues:index';
|
||||
|
@ -102,7 +102,11 @@
|
|||
if ($dropdown.hasClass('js-filter-bulk-update')) {
|
||||
return;
|
||||
}
|
||||
if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
|
||||
if (page === 'projects:boards:show') {
|
||||
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name;
|
||||
gl.issueBoards.BoardsStore.updateFiltersUrl();
|
||||
e.preventDefault();
|
||||
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
|
||||
if (selected.name != null) {
|
||||
selectedMilestone = selected.name;
|
||||
} else {
|
||||
|
|
|
@ -68,6 +68,7 @@
|
|||
$(document).on("click", ".note-edit-cancel", this.cancelEdit);
|
||||
$(document).on("click", ".js-comment-button", this.updateCloseButton);
|
||||
$(document).on("keyup input", ".js-note-text", this.updateTargetButtons);
|
||||
$(document).on('click', '.js-comment-resolve-button', this.resolveDiscussion);
|
||||
$(document).on("click", ".js-note-delete", this.removeNote);
|
||||
$(document).on("click", ".js-note-attachment-delete", this.removeAttachment);
|
||||
$(document).on("ajax:complete", ".js-main-target-form", this.reenableTargetFormSubmitButton);
|
||||
|
@ -100,6 +101,7 @@
|
|||
$(document).off("click", ".js-note-target-close");
|
||||
$(document).off("click", ".js-note-discard");
|
||||
$(document).off("keydown", ".js-note-text");
|
||||
$(document).off('click', '.js-comment-resolve-button');
|
||||
$('.note .js-task-list-container').taskList('disable');
|
||||
return $(document).off('tasklist:changed', '.note .js-task-list-container');
|
||||
};
|
||||
|
@ -201,7 +203,7 @@
|
|||
Increase @pollingInterval up to 120 seconds on every function call,
|
||||
if `shouldReset` has a truthy value, 'null' or 'undefined' the variable
|
||||
will reset to @basePollingInterval.
|
||||
|
||||
|
||||
Note: this function is used to gradually increase the polling interval
|
||||
if there aren't new notes coming from the server
|
||||
*/
|
||||
|
@ -223,7 +225,7 @@
|
|||
|
||||
/*
|
||||
Render note in main comments area.
|
||||
|
||||
|
||||
Note: for rendering inline notes use renderDiscussionNote
|
||||
*/
|
||||
|
||||
|
@ -231,7 +233,13 @@
|
|||
var $notesList, votesBlock;
|
||||
if (!note.valid) {
|
||||
if (note.award) {
|
||||
new Flash('You have already awarded this emoji!', 'alert');
|
||||
new Flash('You have already awarded this emoji!', 'alert', this.parentTimeline);
|
||||
}
|
||||
else {
|
||||
if (note.errors.commands_only) {
|
||||
new Flash(note.errors.commands_only, 'notice', this.parentTimeline);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -245,6 +253,7 @@
|
|||
$notesList.append(note.html).syntaxHighlight();
|
||||
gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
|
||||
this.initTaskList();
|
||||
this.refresh();
|
||||
return this.updateNotesCount(1);
|
||||
}
|
||||
};
|
||||
|
@ -265,7 +274,7 @@
|
|||
|
||||
/*
|
||||
Render note in discussion area.
|
||||
|
||||
|
||||
Note: for rendering inline notes use renderDiscussionNote
|
||||
*/
|
||||
|
||||
|
@ -297,6 +306,11 @@
|
|||
} else {
|
||||
discussionContainer.append(note_html);
|
||||
}
|
||||
|
||||
if (typeof DiffNotesApp !== 'undefined') {
|
||||
DiffNotesApp.compileComponents();
|
||||
}
|
||||
|
||||
gl.utils.localTimeAgo($('.js-timeago', note_html), false);
|
||||
return this.updateNotesCount(1);
|
||||
};
|
||||
|
@ -304,7 +318,7 @@
|
|||
|
||||
/*
|
||||
Called in response the main target form has been successfully submitted.
|
||||
|
||||
|
||||
Removes any errors.
|
||||
Resets text and preview.
|
||||
Resets buttons.
|
||||
|
@ -329,7 +343,7 @@
|
|||
|
||||
/*
|
||||
Shows the main form and does some setup on it.
|
||||
|
||||
|
||||
Sets some hidden fields in the form.
|
||||
*/
|
||||
|
||||
|
@ -343,13 +357,14 @@
|
|||
form.find("#note_line_code").remove();
|
||||
form.find("#note_position").remove();
|
||||
form.find("#note_type").remove();
|
||||
form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove();
|
||||
return this.parentTimeline = form.parents('.timeline');
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
General note form setup.
|
||||
|
||||
|
||||
deactivates the submit button when text is empty
|
||||
hides the preview button when text is empty
|
||||
setup GFM auto complete
|
||||
|
@ -366,7 +381,7 @@
|
|||
|
||||
/*
|
||||
Called in response to the new note form being submitted
|
||||
|
||||
|
||||
Adds new note to list.
|
||||
*/
|
||||
|
||||
|
@ -381,19 +396,33 @@
|
|||
|
||||
/*
|
||||
Called in response to the new note form being submitted
|
||||
|
||||
|
||||
Adds new note to list.
|
||||
*/
|
||||
|
||||
Notes.prototype.addDiscussionNote = function(xhr, note, status) {
|
||||
var $form = $(xhr.target);
|
||||
|
||||
if ($form.attr('data-resolve-all') != null) {
|
||||
var namespacePath = $form.attr('data-namespace-path'),
|
||||
projectPath = $form.attr('data-project-path')
|
||||
discussionId = $form.attr('data-discussion-id'),
|
||||
mergeRequestId = $form.attr('data-noteable-iid'),
|
||||
namespace = namespacePath + '/' + projectPath;
|
||||
|
||||
if (ResolveService != null) {
|
||||
ResolveService.toggleResolveForDiscussion(namespace, mergeRequestId, discussionId);
|
||||
}
|
||||
}
|
||||
|
||||
this.renderDiscussionNote(note);
|
||||
return this.removeDiscussionNoteForm($(xhr.target));
|
||||
this.removeDiscussionNoteForm($form);
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
Called in response to the edit note form being submitted
|
||||
|
||||
|
||||
Updates the current note field.
|
||||
*/
|
||||
|
||||
|
@ -404,13 +433,18 @@
|
|||
$html.syntaxHighlight();
|
||||
$html.find('.js-task-list-container').taskList('enable');
|
||||
$note_li = $('.note-row-' + note.id);
|
||||
return $note_li.replaceWith($html);
|
||||
|
||||
$note_li.replaceWith($html);
|
||||
|
||||
if (typeof DiffNotesApp !== 'undefined') {
|
||||
DiffNotesApp.compileComponents();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
Called in response to clicking the edit note link
|
||||
|
||||
|
||||
Replaces the note text with the note edit form
|
||||
Adds a data attribute to the form with the original content of the note for cancellations
|
||||
*/
|
||||
|
@ -450,7 +484,7 @@
|
|||
|
||||
/*
|
||||
Called in response to clicking the edit note link
|
||||
|
||||
|
||||
Hides edit form and restores the original note text to the editor textarea.
|
||||
*/
|
||||
|
||||
|
@ -472,7 +506,7 @@
|
|||
|
||||
/*
|
||||
Called in response to deleting a note of any kind.
|
||||
|
||||
|
||||
Removes the actual note from view.
|
||||
Removes the whole discussion if the last note is being removed.
|
||||
*/
|
||||
|
@ -485,6 +519,15 @@
|
|||
var note, notes;
|
||||
note = $(el);
|
||||
notes = note.closest(".notes");
|
||||
|
||||
if (typeof DiffNotesApp !== "undefined" && DiffNotesApp !== null) {
|
||||
ref = DiffNotesApp.$refs[noteId];
|
||||
|
||||
if (ref) {
|
||||
ref.$destroy(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (notes.find(".note").length === 1) {
|
||||
notes.closest(".timeline-entry").remove();
|
||||
notes.closest("tr").remove();
|
||||
|
@ -498,7 +541,7 @@
|
|||
|
||||
/*
|
||||
Called in response to clicking the delete attachment link
|
||||
|
||||
|
||||
Removes the attachment wrapper view, including image tag if it exists
|
||||
Resets the note editing form
|
||||
*/
|
||||
|
@ -515,7 +558,7 @@
|
|||
|
||||
/*
|
||||
Called when clicking on the "reply" button for a diff line.
|
||||
|
||||
|
||||
Shows the note form below the notes.
|
||||
*/
|
||||
|
||||
|
@ -523,17 +566,19 @@
|
|||
var form, replyLink;
|
||||
form = this.formClone.clone();
|
||||
replyLink = $(e.target).closest(".js-discussion-reply-button");
|
||||
replyLink.hide();
|
||||
replyLink.after(form);
|
||||
replyLink
|
||||
.closest('.discussion-reply-holder')
|
||||
.hide()
|
||||
.after(form);
|
||||
return this.setupDiscussionNoteForm(replyLink, form);
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
Shows the diff or discussion form and does some setup on it.
|
||||
|
||||
|
||||
Sets some hidden fields in the form.
|
||||
|
||||
|
||||
Note: dataHolder must have the "discussionId", "lineCode", "noteableType"
|
||||
and "noteableId" data attributes set.
|
||||
*/
|
||||
|
@ -549,15 +594,29 @@
|
|||
form.find("#note_noteable_type").val(dataHolder.data("noteableType"));
|
||||
form.find("#note_noteable_id").val(dataHolder.data("noteableId"));
|
||||
form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text'));
|
||||
form.find('.js-note-target-close').remove();
|
||||
this.setupNoteForm(form);
|
||||
|
||||
if (typeof DiffNotesApp !== 'undefined') {
|
||||
var $commentBtn = form.find('comment-and-resolve-btn');
|
||||
$commentBtn
|
||||
.attr(':discussion-id', "'" + dataHolder.data('discussionId') + "'");
|
||||
DiffNotesApp.$compile($commentBtn.get(0));
|
||||
}
|
||||
|
||||
form.find(".js-note-text").focus();
|
||||
return form.removeClass('js-main-target-form').addClass("discussion-form js-discussion-note-form");
|
||||
form
|
||||
.find('.js-comment-resolve-button')
|
||||
.attr('data-discussion-id', dataHolder.data('discussionId'));
|
||||
form
|
||||
.removeClass('js-main-target-form')
|
||||
.addClass("discussion-form js-discussion-note-form");
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
Called when clicking on the "add a comment" button on the side of a diff line.
|
||||
|
||||
|
||||
Inserts a temporary row for the form below the line.
|
||||
Sets up the form and shows it.
|
||||
*/
|
||||
|
@ -570,16 +629,19 @@
|
|||
nextRow = row.next();
|
||||
hasNotes = nextRow.is(".notes_holder");
|
||||
addForm = false;
|
||||
targetContent = ".notes_content";
|
||||
rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"></td></tr>";
|
||||
notesContentSelector = ".notes_content";
|
||||
rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>";
|
||||
if (this.isParallelView()) {
|
||||
lineType = $link.data("lineType");
|
||||
targetContent += "." + lineType;
|
||||
rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\"></td><td class=\"notes_content parallel old\"></td><td class=\"notes_line\"></td><td class=\"notes_content parallel new\"></td></tr>";
|
||||
notesContentSelector += "." + lineType;
|
||||
rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line old\"></td><td class=\"notes_content parallel old\"><div class=\"content\"></div></td><td class=\"notes_line new\"></td><td class=\"notes_content parallel new\"><div class=\"content\"></div></td></tr>";
|
||||
}
|
||||
notesContentSelector += " .content";
|
||||
if (hasNotes) {
|
||||
notesContent = nextRow.find(targetContent);
|
||||
nextRow.show();
|
||||
notesContent = nextRow.find(notesContentSelector);
|
||||
if (notesContent.length) {
|
||||
notesContent.show();
|
||||
replyButton = notesContent.find(".js-discussion-reply-button:visible");
|
||||
if (replyButton.length) {
|
||||
e.target = replyButton[0];
|
||||
|
@ -593,11 +655,13 @@
|
|||
}
|
||||
} else {
|
||||
row.after(rowCssToAdd);
|
||||
nextRow = row.next();
|
||||
notesContent = nextRow.find(notesContentSelector);
|
||||
addForm = true;
|
||||
}
|
||||
if (addForm) {
|
||||
newForm = this.formClone.clone();
|
||||
newForm.appendTo(row.next().find(targetContent));
|
||||
newForm.appendTo(notesContent);
|
||||
return this.setupDiscussionNoteForm($link, newForm);
|
||||
}
|
||||
};
|
||||
|
@ -605,7 +669,7 @@
|
|||
|
||||
/*
|
||||
Called in response to "cancel" on a diff note form.
|
||||
|
||||
|
||||
Shows the reply button again.
|
||||
Removes the form and if necessary it's temporary row.
|
||||
*/
|
||||
|
@ -616,7 +680,9 @@
|
|||
glForm = form.data('gl-form');
|
||||
glForm.destroy();
|
||||
form.find(".js-note-text").data("autosave").reset();
|
||||
form.prev(".js-discussion-reply-button").show();
|
||||
form
|
||||
.prev('.discussion-reply-holder')
|
||||
.show();
|
||||
if (row.is(".js-temp-notes-holder")) {
|
||||
return row.remove();
|
||||
} else {
|
||||
|
@ -634,7 +700,7 @@
|
|||
|
||||
/*
|
||||
Called after an attachment file has been selected.
|
||||
|
||||
|
||||
Updates the file name for the selected attachment.
|
||||
*/
|
||||
|
||||
|
@ -725,6 +791,18 @@
|
|||
return this.notesCountBadge.text(parseInt(this.notesCountBadge.text()) + updateCount);
|
||||
};
|
||||
|
||||
Notes.prototype.resolveDiscussion = function () {
|
||||
var $this = $(this),
|
||||
discussionId = $this.attr('data-discussion-id');
|
||||
|
||||
$this
|
||||
.closest('form')
|
||||
.attr('data-discussion-id', discussionId)
|
||||
.attr('data-resolve-all', 'true')
|
||||
.attr('data-namespace-path', $this.attr('data-namespace-path'))
|
||||
.attr('data-project-path', $this.attr('data-project-path'));
|
||||
};
|
||||
|
||||
return Notes;
|
||||
|
||||
})();
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
(function() {
|
||||
function toggleGraph() {
|
||||
const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
|
||||
const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
|
||||
const $btnText = $(this).find('.toggle-btn-text');
|
||||
|
||||
$($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
|
||||
|
||||
const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
|
||||
|
||||
graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide')
|
||||
}
|
||||
|
||||
$(document).on('click', '.toggle-pipeline-btn', toggleGraph);
|
||||
})();
|
|
@ -44,8 +44,8 @@
|
|||
|
||||
// Enable submit button
|
||||
const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]');
|
||||
const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_level_attributes][access_level]"]');
|
||||
const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_level_attributes][access_level]"]');
|
||||
const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
|
||||
const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
|
||||
|
||||
if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){
|
||||
this.$form.find('input[type="submit"]').removeAttr('disabled');
|
||||
|
|
|
@ -39,12 +39,14 @@
|
|||
_method: 'PATCH',
|
||||
id: this.$wrap.data('banchId'),
|
||||
protected_branch: {
|
||||
merge_access_level_attributes: {
|
||||
merge_access_levels_attributes: [{
|
||||
id: this.$allowedToMergeDropdown.data('access-level-id'),
|
||||
access_level: $allowedToMergeInput.val()
|
||||
},
|
||||
push_access_level_attributes: {
|
||||
}],
|
||||
push_access_levels_attributes: [{
|
||||
id: this.$allowedToPushDropdown.data('access-level-id'),
|
||||
access_level: $allowedToPushInput.val()
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
success: () => {
|
||||
|
|
|
@ -35,10 +35,16 @@
|
|||
this.isOpen = !this.isOpen;
|
||||
if (!this.isOpen && !this.hasError) {
|
||||
this.content.hide();
|
||||
return this.collapsedContent.show();
|
||||
this.collapsedContent.show();
|
||||
if (typeof DiffNotesApp !== 'undefined') {
|
||||
DiffNotesApp.compileComponents();
|
||||
}
|
||||
} else if (this.content) {
|
||||
this.collapsedContent.hide();
|
||||
return this.content.show();
|
||||
this.content.show();
|
||||
if (typeof DiffNotesApp !== 'undefined') {
|
||||
DiffNotesApp.compileComponents();
|
||||
}
|
||||
} else {
|
||||
return this.getContentHTML();
|
||||
}
|
||||
|
@ -57,7 +63,11 @@
|
|||
_this.hasError = true;
|
||||
_this.content = $(ERROR_HTML);
|
||||
}
|
||||
return _this.collapsedContent.after(_this.content);
|
||||
_this.collapsedContent.after(_this.content);
|
||||
|
||||
if (typeof DiffNotesApp !== 'undefined') {
|
||||
DiffNotesApp.compileComponents();
|
||||
}
|
||||
};
|
||||
})(this));
|
||||
};
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*= require ../blob/template_selector */
|
||||
|
||||
((global) => {
|
||||
class IssuableTemplateSelector extends TemplateSelector {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.projectPath = this.dropdown.data('project-path');
|
||||
this.namespacePath = this.dropdown.data('namespace-path');
|
||||
this.issuableType = this.wrapper.data('issuable-type');
|
||||
this.titleInput = $(`#${this.issuableType}_title`);
|
||||
|
||||
let initialQuery = {
|
||||
name: this.dropdown.data('selected')
|
||||
};
|
||||
|
||||
if (initialQuery.name) this.requestFile(initialQuery);
|
||||
|
||||
$('.reset-template', this.dropdown.parent()).on('click', () => {
|
||||
if (this.currentTemplate) this.setInputValueToTemplateContent();
|
||||
});
|
||||
}
|
||||
|
||||
requestFile(query) {
|
||||
this.startLoadingSpinner();
|
||||
Api.issueTemplate(this.namespacePath, this.projectPath, query.name, this.issuableType, (err, currentTemplate) => {
|
||||
this.currentTemplate = currentTemplate;
|
||||
if (err) return; // Error handled by global AJAX error handler
|
||||
this.stopLoadingSpinner();
|
||||
this.setInputValueToTemplateContent();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setInputValueToTemplateContent() {
|
||||
// `this.requestFileSuccess` sets the value of the description input field
|
||||
// to the content of the template selected.
|
||||
if (this.titleInput.val() === '') {
|
||||
// If the title has not yet been set, focus the title input and
|
||||
// skip focusing the description input by setting `true` as the 2nd
|
||||
// argument to `requestFileSuccess`.
|
||||
this.requestFileSuccess(this.currentTemplate, true);
|
||||
this.titleInput.focus();
|
||||
} else {
|
||||
this.requestFileSuccess(this.currentTemplate);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
global.IssuableTemplateSelector = IssuableTemplateSelector;
|
||||
})(window);
|
|
@ -0,0 +1,29 @@
|
|||
((global) => {
|
||||
class IssuableTemplateSelectors {
|
||||
constructor(opts = {}) {
|
||||
this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector');
|
||||
this.editor = opts.editor || this.initEditor();
|
||||
|
||||
this.$dropdowns.each((i, dropdown) => {
|
||||
let $dropdown = $(dropdown);
|
||||
new IssuableTemplateSelector({
|
||||
pattern: /(\.md)/,
|
||||
data: $dropdown.data('data'),
|
||||
wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
|
||||
dropdown: $dropdown,
|
||||
editor: this.editor
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initEditor() {
|
||||
let editor = $('.markdown-area');
|
||||
// Proxy ace-editor's .setValue to jQuery's .val
|
||||
editor.setValue = editor.val;
|
||||
editor.getValue = editor.val;
|
||||
return editor;
|
||||
}
|
||||
}
|
||||
|
||||
global.IssuableTemplateSelectors = IssuableTemplateSelectors;
|
||||
})(window);
|
|
@ -141,7 +141,7 @@
|
|||
$selectbox.hide();
|
||||
return $value.css('display', '');
|
||||
},
|
||||
clicked: function(user) {
|
||||
clicked: function(user, $el, e) {
|
||||
var isIssueIndex, isMRIndex, page, selected;
|
||||
page = $('body').data('page');
|
||||
isIssueIndex = page === 'projects:issues:index';
|
||||
|
@ -149,7 +149,12 @@
|
|||
if ($dropdown.hasClass('js-filter-bulk-update')) {
|
||||
return;
|
||||
}
|
||||
if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
|
||||
if (page === 'projects:boards:show') {
|
||||
selectedId = user.id;
|
||||
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
|
||||
gl.issueBoards.BoardsStore.updateFiltersUrl();
|
||||
e.preventDefault();
|
||||
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
|
||||
selectedId = user.id;
|
||||
return Issuable.filterResults($dropdown.closest('form'));
|
||||
} else if ($dropdown.hasClass('js-filter-submit')) {
|
||||
|
|
|
@ -20,3 +20,8 @@
|
|||
.turn-off { display: block; }
|
||||
}
|
||||
}
|
||||
|
||||
// Hide element if Vue is still working on rendering it fully.
|
||||
[v-cloak="true"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
|
@ -164,6 +164,10 @@
|
|||
@include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light);
|
||||
}
|
||||
|
||||
&.btn-spam {
|
||||
@include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light);
|
||||
}
|
||||
|
||||
&.btn-danger,
|
||||
&.btn-remove,
|
||||
&.btn-red {
|
||||
|
@ -200,6 +204,10 @@
|
|||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
svg, .fa {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
|
|
|
@ -62,9 +62,13 @@
|
|||
position: absolute;
|
||||
top: 50%;
|
||||
right: 6px;
|
||||
margin-top: -4px;
|
||||
margin-top: -6px;
|
||||
color: $dropdown-toggle-icon-color;
|
||||
font-size: 10px;
|
||||
&.fa-spinner {
|
||||
font-size: 16px;
|
||||
margin-top: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover, {
|
||||
|
@ -412,6 +416,7 @@
|
|||
font-size: 14px;
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
table-layout: fixed;
|
||||
|
||||
pre {
|
||||
padding: 10px;
|
||||
padding: 10px 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
font-family: $monospace_font;
|
||||
font-size: $code_font_size !important;
|
||||
font-size: $code_font_size;
|
||||
line-height: $code_line_height !important;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
|
@ -20,13 +20,20 @@
|
|||
border-left: 1px solid;
|
||||
|
||||
code {
|
||||
display: inline-block;
|
||||
min-width: 100%;
|
||||
font-family: $monospace_font;
|
||||
white-space: pre;
|
||||
white-space: normal;
|
||||
word-wrap: normal;
|
||||
padding: 0;
|
||||
|
||||
.line {
|
||||
display: inline-block;
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 19px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,3 +147,8 @@
|
|||
color: $gl-link-color;
|
||||
}
|
||||
}
|
||||
|
||||
.atwho-view small.description {
|
||||
float: right;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
|
|
@ -123,4 +123,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin dark-diff-match-line {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
|
|
@ -222,3 +222,7 @@ header.header-pinned-nav {
|
|||
padding-right: $sidebar_collapsed_width;
|
||||
}
|
||||
}
|
||||
|
||||
.right-sidebar {
|
||||
border-left: 1px solid $border-color;
|
||||
}
|
||||
|
|
|
@ -276,3 +276,5 @@ $personal-access-tokens-disabled-label-color: #bbb;
|
|||
|
||||
$ci-output-bg: #1d1f21;
|
||||
$ci-text-color: #c5c8c6;
|
||||
|
||||
$issue-boards-font-size: 15px;
|
||||
|
|
|
@ -21,6 +21,10 @@
|
|||
|
||||
// Diff line
|
||||
.line_holder {
|
||||
&.match .line_content {
|
||||
@include dark-diff-match-line;
|
||||
}
|
||||
|
||||
td.diff-line-num.hll:not(.empty-cell),
|
||||
td.line_content.hll:not(.empty-cell) {
|
||||
background-color: #557;
|
||||
|
@ -36,8 +40,7 @@
|
|||
}
|
||||
|
||||
.line_content.match {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
@include dark-diff-match-line;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,10 @@
|
|||
|
||||
// Diff line
|
||||
.line_holder {
|
||||
&.match .line_content {
|
||||
@include dark-diff-match-line;
|
||||
}
|
||||
|
||||
td.diff-line-num.hll:not(.empty-cell),
|
||||
td.line_content.hll:not(.empty-cell) {
|
||||
background-color: #49483e;
|
||||
|
@ -36,8 +40,7 @@
|
|||
}
|
||||
|
||||
.line_content.match {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
@include dark-diff-match-line;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,10 @@
|
|||
|
||||
// Diff line
|
||||
.line_holder {
|
||||
&.match .line_content {
|
||||
@include dark-diff-match-line;
|
||||
}
|
||||
|
||||
td.diff-line-num.hll:not(.empty-cell),
|
||||
td.line_content.hll:not(.empty-cell) {
|
||||
background-color: #174652;
|
||||
|
@ -36,8 +40,7 @@
|
|||
}
|
||||
|
||||
.line_content.match {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
@include dark-diff-match-line;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
/* https://gist.github.com/qguv/7936275 */
|
||||
|
||||
@mixin matchLine {
|
||||
color: $black-transparent;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.code.solarized-light {
|
||||
// Line numbers
|
||||
.line-numbers, .diff-line-num {
|
||||
|
@ -21,6 +27,10 @@
|
|||
|
||||
// Diff line
|
||||
.line_holder {
|
||||
&.match .line_content {
|
||||
@include matchLine;
|
||||
}
|
||||
|
||||
td.diff-line-num.hll:not(.empty-cell),
|
||||
td.line_content.hll:not(.empty-cell) {
|
||||
background-color: #ddd8c5;
|
||||
|
@ -36,8 +46,7 @@
|
|||
}
|
||||
|
||||
.line_content.match {
|
||||
color: $black-transparent;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
@include matchLine;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
/* https://github.com/aahan/pygments-github-style */
|
||||
|
||||
@mixin matchLine {
|
||||
color: $black-transparent;
|
||||
background-color: $match-line;
|
||||
}
|
||||
|
||||
.code.white {
|
||||
// Line numbers
|
||||
.line-numbers, .diff-line-num {
|
||||
|
@ -22,6 +28,10 @@
|
|||
// Diff line
|
||||
.line_holder {
|
||||
|
||||
&.match .line_content {
|
||||
@include matchLine;
|
||||
}
|
||||
|
||||
.diff-line-num {
|
||||
&.old {
|
||||
background-color: $line-number-old;
|
||||
|
@ -57,8 +67,7 @@
|
|||
}
|
||||
|
||||
&.match {
|
||||
color: $black-transparent;
|
||||
background-color: $match-line;
|
||||
@include matchLine;
|
||||
}
|
||||
|
||||
&.hll:not(.empty-cell) {
|
||||
|
|
|
@ -45,7 +45,6 @@
|
|||
.line_content {
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
white-space: pre;
|
||||
|
||||
&.old {
|
||||
background-color: $line-removed;
|
||||
|
@ -71,6 +70,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
span.highlight_word {
|
||||
background-color: #fafe3d !important;
|
||||
}
|
||||
|
|
|
@ -72,7 +72,6 @@
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
// Users List
|
||||
|
||||
.users-list {
|
||||
|
@ -98,3 +97,44 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.abuse-reports {
|
||||
.table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
.subheading {
|
||||
padding-bottom: $gl-padding;
|
||||
}
|
||||
.message {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.btn {
|
||||
white-space: normal;
|
||||
padding: $gl-btn-padding;
|
||||
}
|
||||
th {
|
||||
width: 15%;
|
||||
&.wide {
|
||||
width: 55%;
|
||||
}
|
||||
}
|
||||
@media (max-width: $screen-sm-max) {
|
||||
th {
|
||||
width: 100%;
|
||||
}
|
||||
td {
|
||||
width: 100%;
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.no-reports {
|
||||
.emoji-icon {
|
||||
margin-left: $btn-side-margin;
|
||||
margin-top: 3px;
|
||||
}
|
||||
span {
|
||||
font-size: 19px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,329 @@
|
|||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-can-drag {
|
||||
cursor: -webkit-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.is-dragging {
|
||||
* {
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-issues-board-new {
|
||||
width: 320px;
|
||||
|
||||
.dropdown-content {
|
||||
max-height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.issue-board-dropdown-content {
|
||||
margin: 0 8px 10px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid $dropdown-divider-color;
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #9c9c9c;
|
||||
}
|
||||
}
|
||||
|
||||
.issue-boards-page {
|
||||
.content-wrapper {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sub-nav,
|
||||
.issues-filters {
|
||||
-webkit-flex: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.page-with-sidebar {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
max-height: 100vh;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.issue-boards-content {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
|
||||
.content {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.boards-app-loading {
|
||||
width: 100%;
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
.boards-list {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
-webkit-flex-basis: 0;
|
||||
flex-basis: 0;
|
||||
min-height: calc(100vh - 152px);
|
||||
max-height: calc(100vh - 152px);
|
||||
padding-top: 25px;
|
||||
padding-right: ($gl-padding / 2);
|
||||
padding-left: ($gl-padding / 2);
|
||||
overflow-x: scroll;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
min-height: 475px;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
.board {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
min-width: calc(100vw - 15px);
|
||||
max-width: calc(100vw - 15px);
|
||||
margin-bottom: 25px;
|
||||
padding-right: ($gl-padding / 2);
|
||||
padding-left: ($gl-padding / 2);
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
min-width: 400px;
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.board-inner {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
font-size: $issue-boards-font-size;
|
||||
background: $background-color;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: $border-radius-default;
|
||||
}
|
||||
|
||||
.board-header {
|
||||
border-top-left-radius: $border-radius-default;
|
||||
border-top-right-radius: $border-radius-default;
|
||||
|
||||
&.has-border {
|
||||
border-top: 3px solid;
|
||||
|
||||
.board-title {
|
||||
padding-top: ($gl-padding - 3px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.board-header-loading-spinner {
|
||||
margin-right: 10px;
|
||||
color: $gray-darkest;
|
||||
}
|
||||
|
||||
.board-inner-container {
|
||||
border-bottom: 1px solid $border-color;
|
||||
padding: $gl-padding;
|
||||
}
|
||||
|
||||
.board-title {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: $gl-padding;
|
||||
font-size: 1em;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
.board-mobile-handle {
|
||||
position: relative;
|
||||
left: 0;
|
||||
top: 1px;
|
||||
margin-top: 0;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.board-search-container {
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
|
||||
.form-control {
|
||||
padding-right: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.board-search-icon,
|
||||
.board-search-clear-btn {
|
||||
position: absolute;
|
||||
right: $gl-padding + 10px;
|
||||
top: 50%;
|
||||
margin-top: -7px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.board-search-icon {
|
||||
color: $gl-placeholder-color;
|
||||
}
|
||||
|
||||
.board-search-clear-btn {
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
|
||||
&:hover {
|
||||
color: $gl-link-color;
|
||||
}
|
||||
}
|
||||
|
||||
.board-delete {
|
||||
margin-right: 10px;
|
||||
padding: 0;
|
||||
color: $gray-darkest;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
|
||||
&:hover {
|
||||
color: $gl-link-color;
|
||||
}
|
||||
}
|
||||
|
||||
.board-blank-state {
|
||||
height: 100%;
|
||||
padding: $gl-padding;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.board-blank-state-list {
|
||||
list-style: none;
|
||||
|
||||
> li:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.label-color {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 3px;
|
||||
border-radius: $border-radius-default;
|
||||
}
|
||||
}
|
||||
|
||||
.board-list {
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
height: 400px;
|
||||
margin-bottom: 0;
|
||||
padding: 5px;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.board-list-loading {
|
||||
margin-top: 10px;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.is-ghost {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.is-dragging {
|
||||
// Important because plugin sets inline CSS
|
||||
opacity: 1!important;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 10px $gl-padding;
|
||||
background: #fff;
|
||||
border-radius: $border-radius-default;
|
||||
box-shadow: 0 1px 2px rgba(186, 186, 186, 0.5);
|
||||
list-style: none;
|
||||
|
||||
&.user-can-drag {
|
||||
padding-left: ($gl-padding * 2);
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
padding-left: $gl-padding;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.label {
|
||||
border: 0;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.confidential-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.board-mobile-handle {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
margin-top: (-15px / 2);
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
margin-top: 5px;
|
||||
|
||||
.label {
|
||||
margin-right: 4px;
|
||||
font-size: (14px / $issue-boards-font-size) * 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.card-number {
|
||||
margin-right: 8px;
|
||||
font-weight: 500;
|
||||
}
|
|
@ -53,14 +53,6 @@
|
|||
left: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
svg {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.build-header {
|
||||
|
@ -108,24 +100,98 @@
|
|||
}
|
||||
|
||||
.right-sidebar.build-sidebar {
|
||||
padding-top: $gl-padding;
|
||||
padding-bottom: $gl-padding;
|
||||
padding: $gl-padding 0;
|
||||
|
||||
&.right-sidebar-collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.blocks-container {
|
||||
padding: $gl-padding;
|
||||
}
|
||||
|
||||
.block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.build-sidebar-header {
|
||||
padding-top: 0;
|
||||
padding: 0 $gl-padding $gl-padding;
|
||||
|
||||
.gutter-toggle {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.stage-item {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $gl-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.build-dropdown {
|
||||
padding: 0 $gl-padding;
|
||||
|
||||
.dropdown-menu-toggle {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
right: $gl-padding;
|
||||
left: $gl-padding;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.builds-container {
|
||||
margin-top: $gl-padding;
|
||||
background-color: $white-light;
|
||||
border-top: 1px solid $border-color;
|
||||
border-bottom: 1px solid $border-color;
|
||||
max-height: 300px;
|
||||
overflow: scroll;
|
||||
|
||||
svg {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin-right: 3px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: $gl-padding 10px $gl-padding 40px;
|
||||
width: 270px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover {
|
||||
background-color: $row-hover;
|
||||
color: $gl-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.build-job {
|
||||
position: relative;
|
||||
|
||||
.fa {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
|
||||
.fa {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.build-detail-row {
|
||||
|
|
|
@ -395,3 +395,12 @@
|
|||
display: inline-block;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.js-issuable-selector-wrap {
|
||||
.js-issuable-selector {
|
||||
width: 100%;
|
||||
}
|
||||
@media (max-width: $screen-sm-max) {
|
||||
margin-bottom: $gl-padding;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,238 @@
|
|||
$colors: (
|
||||
white_header_head_neutral : #e1fad7,
|
||||
white_line_head_neutral : #effdec,
|
||||
white_button_head_neutral : #9adb84,
|
||||
|
||||
white_header_head_chosen : #baf0a8,
|
||||
white_line_head_chosen : #e1fad7,
|
||||
white_button_head_chosen : #52c22d,
|
||||
|
||||
white_header_origin_neutral : #e0f0ff,
|
||||
white_line_origin_neutral : #f2f9ff,
|
||||
white_button_origin_neutral : #87c2fa,
|
||||
|
||||
white_header_origin_chosen : #add8ff,
|
||||
white_line_origin_chosen : #e0f0ff,
|
||||
white_button_origin_chosen : #268ced,
|
||||
|
||||
white_header_not_chosen : #f0f0f0,
|
||||
white_line_not_chosen : #f9f9f9,
|
||||
|
||||
|
||||
dark_header_head_neutral : rgba(#3f3, .2),
|
||||
dark_line_head_neutral : rgba(#3f3, .1),
|
||||
dark_button_head_neutral : #40874f,
|
||||
|
||||
dark_header_head_chosen : rgba(#3f3, .33),
|
||||
dark_line_head_chosen : rgba(#3f3, .2),
|
||||
dark_button_head_chosen : #258537,
|
||||
|
||||
dark_header_origin_neutral : rgba(#2878c9, .4),
|
||||
dark_line_origin_neutral : rgba(#2878c9, .3),
|
||||
dark_button_origin_neutral : #2a5c8c,
|
||||
|
||||
dark_header_origin_chosen : rgba(#2878c9, .6),
|
||||
dark_line_origin_chosen : rgba(#2878c9, .4),
|
||||
dark_button_origin_chosen : #1d6cbf,
|
||||
|
||||
dark_header_not_chosen : rgba(#fff, .25),
|
||||
dark_line_not_chosen : rgba(#fff, .1),
|
||||
|
||||
|
||||
monokai_header_head_neutral : rgba(#a6e22e, .25),
|
||||
monokai_line_head_neutral : rgba(#a6e22e, .1),
|
||||
monokai_button_head_neutral : #376b20,
|
||||
|
||||
monokai_header_head_chosen : rgba(#a6e22e, .4),
|
||||
monokai_line_head_chosen : rgba(#a6e22e, .25),
|
||||
monokai_button_head_chosen : #39800d,
|
||||
|
||||
monokai_header_origin_neutral : rgba(#60d9f1, .35),
|
||||
monokai_line_origin_neutral : rgba(#60d9f1, .15),
|
||||
monokai_button_origin_neutral : #38848c,
|
||||
|
||||
monokai_header_origin_chosen : rgba(#60d9f1, .5),
|
||||
monokai_line_origin_chosen : rgba(#60d9f1, .35),
|
||||
monokai_button_origin_chosen : #3ea4b2,
|
||||
|
||||
monokai_header_not_chosen : rgba(#76715d, .24),
|
||||
monokai_line_not_chosen : rgba(#76715d, .1),
|
||||
|
||||
|
||||
solarized_light_header_head_neutral : rgba(#859900, .37),
|
||||
solarized_light_line_head_neutral : rgba(#859900, .2),
|
||||
solarized_light_button_head_neutral : #afb262,
|
||||
|
||||
solarized_light_header_head_chosen : rgba(#859900, .5),
|
||||
solarized_light_line_head_chosen : rgba(#859900, .37),
|
||||
solarized_light_button_head_chosen : #94993d,
|
||||
|
||||
solarized_light_header_origin_neutral : rgba(#2878c9, .37),
|
||||
solarized_light_line_origin_neutral : rgba(#2878c9, .15),
|
||||
solarized_light_button_origin_neutral : #60a1bf,
|
||||
|
||||
solarized_light_header_origin_chosen : rgba(#2878c9, .6),
|
||||
solarized_light_line_origin_chosen : rgba(#2878c9, .37),
|
||||
solarized_light_button_origin_chosen : #2482b2,
|
||||
|
||||
solarized_light_header_not_chosen : rgba(#839496, .37),
|
||||
solarized_light_line_not_chosen : rgba(#839496, .2),
|
||||
|
||||
|
||||
solarized_dark_header_head_neutral : rgba(#859900, .35),
|
||||
solarized_dark_line_head_neutral : rgba(#859900, .15),
|
||||
solarized_dark_button_head_neutral : #376b20,
|
||||
|
||||
solarized_dark_header_head_chosen : rgba(#859900, .5),
|
||||
solarized_dark_line_head_chosen : rgba(#859900, .35),
|
||||
solarized_dark_button_head_chosen : #39800d,
|
||||
|
||||
solarized_dark_header_origin_neutral : rgba(#2878c9, .35),
|
||||
solarized_dark_line_origin_neutral : rgba(#2878c9, .15),
|
||||
solarized_dark_button_origin_neutral : #086799,
|
||||
|
||||
solarized_dark_header_origin_chosen : rgba(#2878c9, .6),
|
||||
solarized_dark_line_origin_chosen : rgba(#2878c9, .35),
|
||||
solarized_dark_button_origin_chosen : #0082cc,
|
||||
|
||||
solarized_dark_header_not_chosen : rgba(#839496, .25),
|
||||
solarized_dark_line_not_chosen : rgba(#839496, .15)
|
||||
);
|
||||
|
||||
|
||||
@mixin color-scheme($color) {
|
||||
.header.line_content, .diff-line-num {
|
||||
&.origin {
|
||||
background-color: map-get($colors, #{$color}_header_origin_neutral);
|
||||
border-color: map-get($colors, #{$color}_header_origin_neutral);
|
||||
|
||||
button {
|
||||
background-color: map-get($colors, #{$color}_button_origin_neutral);
|
||||
border-color: darken(map-get($colors, #{$color}_button_origin_neutral), 15);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: map-get($colors, #{$color}_header_origin_chosen);
|
||||
border-color: map-get($colors, #{$color}_header_origin_chosen);
|
||||
|
||||
button {
|
||||
background-color: map-get($colors, #{$color}_button_origin_chosen);
|
||||
border-color: darken(map-get($colors, #{$color}_button_origin_chosen), 15);
|
||||
}
|
||||
}
|
||||
|
||||
&.unselected {
|
||||
background-color: map-get($colors, #{$color}_header_not_chosen);
|
||||
border-color: map-get($colors, #{$color}_header_not_chosen);
|
||||
|
||||
button {
|
||||
background-color: lighten(map-get($colors, #{$color}_button_origin_neutral), 15);
|
||||
border-color: map-get($colors, #{$color}_button_origin_neutral);
|
||||
}
|
||||
}
|
||||
}
|
||||
&.head {
|
||||
background-color: map-get($colors, #{$color}_header_head_neutral);
|
||||
border-color: map-get($colors, #{$color}_header_head_neutral);
|
||||
|
||||
button {
|
||||
background-color: map-get($colors, #{$color}_button_head_neutral);
|
||||
border-color: darken(map-get($colors, #{$color}_button_head_neutral), 15);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: map-get($colors, #{$color}_header_head_chosen);
|
||||
border-color: map-get($colors, #{$color}_header_head_chosen);
|
||||
|
||||
button {
|
||||
background-color: map-get($colors, #{$color}_button_head_chosen);
|
||||
border-color: darken(map-get($colors, #{$color}_button_head_chosen), 15);
|
||||
}
|
||||
}
|
||||
|
||||
&.unselected {
|
||||
background-color: map-get($colors, #{$color}_header_not_chosen);
|
||||
border-color: map-get($colors, #{$color}_header_not_chosen);
|
||||
|
||||
button {
|
||||
background-color: lighten(map-get($colors, #{$color}_button_head_neutral), 15);
|
||||
border-color: map-get($colors, #{$color}_button_head_neutral);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.line_content {
|
||||
&.origin {
|
||||
background-color: map-get($colors, #{$color}_line_origin_neutral);
|
||||
|
||||
&.selected {
|
||||
background-color: map-get($colors, #{$color}_line_origin_chosen);
|
||||
}
|
||||
|
||||
&.unselected {
|
||||
background-color: map-get($colors, #{$color}_line_not_chosen);
|
||||
}
|
||||
}
|
||||
&.head {
|
||||
background-color: map-get($colors, #{$color}_line_head_neutral);
|
||||
|
||||
&.selected {
|
||||
background-color: map-get($colors, #{$color}_line_head_chosen);
|
||||
}
|
||||
|
||||
&.unselected {
|
||||
background-color: map-get($colors, #{$color}_line_not_chosen);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#conflicts {
|
||||
|
||||
.white {
|
||||
@include color-scheme('white')
|
||||
}
|
||||
|
||||
.dark {
|
||||
@include color-scheme('dark')
|
||||
}
|
||||
|
||||
.monokai {
|
||||
@include color-scheme('monokai')
|
||||
}
|
||||
|
||||
.solarized-light {
|
||||
@include color-scheme('solarized_light')
|
||||
}
|
||||
|
||||
.solarized-dark {
|
||||
@include color-scheme('solarized_dark')
|
||||
}
|
||||
|
||||
.diff-wrap-lines .line_content {
|
||||
white-space: normal;
|
||||
min-height: 19px;
|
||||
}
|
||||
|
||||
.line_content.header {
|
||||
position: relative;
|
||||
|
||||
button {
|
||||
border-radius: 2px;
|
||||
font-size: 10px;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
color: #fff;
|
||||
width: 75px; // static width to make 2 buttons have same width
|
||||
height: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-success .fa-spinner {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
|
@ -69,6 +69,10 @@
|
|||
|
||||
&.ci-success {
|
||||
color: $gl-success;
|
||||
|
||||
a.environment {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&.ci-success_with_warnings {
|
||||
|
@ -126,7 +130,6 @@
|
|||
&.has-conflicts .fa-exclamation-triangle {
|
||||
color: $gl-warning;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
|
|
|
@ -159,6 +159,32 @@
|
|||
}
|
||||
}
|
||||
|
||||
.discussion-with-resolve-btn {
|
||||
display: table;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
table-layout: auto;
|
||||
|
||||
.btn-group {
|
||||
display: table-cell;
|
||||
float: none;
|
||||
width: 1%;
|
||||
|
||||
&:first-child {
|
||||
width: 100%;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.discussion-notes-count {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
|
|
@ -383,3 +383,80 @@ ul.notes {
|
|||
color: $gl-link-color;
|
||||
}
|
||||
}
|
||||
|
||||
.line-resolve-all-container {
|
||||
.btn-group {
|
||||
margin-top: -1px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.discussion-next-btn {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.line-resolve-all {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
background-color: $background-color;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: $border-radius-default;
|
||||
|
||||
&.has-next-btn {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.line-resolve-btn {
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.line-resolve-text {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.line-resolve-btn {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
outline: 0;
|
||||
|
||||
&.is-disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&:not(.is-disabled):hover,
|
||||
&:not(.is-disabled):focus,
|
||||
&.is-active {
|
||||
color: $gl-text-green;
|
||||
|
||||
svg path {
|
||||
fill: $gl-text-green;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
position: relative;
|
||||
color: $notes-action-color;
|
||||
|
||||
path {
|
||||
fill: $notes-action-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.discussion-next-btn {
|
||||
svg {
|
||||
margin: 0;
|
||||
|
||||
path {
|
||||
fill: $gray-darkest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -229,3 +229,196 @@
|
|||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Pipeline visualization
|
||||
|
||||
.toggle-pipeline-btn {
|
||||
background-color: $gray-dark;
|
||||
|
||||
.caret {
|
||||
border-top: none;
|
||||
border-bottom: 4px solid;
|
||||
}
|
||||
|
||||
&.graph-collapsed {
|
||||
background-color: $white-light;
|
||||
|
||||
.caret {
|
||||
border-bottom: none;
|
||||
border-top: 4px solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pipeline-graph {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
max-height: 500px;
|
||||
transition: max-height 0.3s, padding 0.3s;
|
||||
|
||||
&.graph-collapsed {
|
||||
max-height: 0;
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.pipeline-visualization {
|
||||
position: relative;
|
||||
min-width: 1220px;
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.stage-column {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-right: 50px;
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.stage-name {
|
||||
margin-bottom: 15px;
|
||||
font-weight: bold;
|
||||
width: 150px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.build {
|
||||
border: 1px solid $border-color;
|
||||
position: relative;
|
||||
padding: 6px 10px;
|
||||
border-radius: 30px;
|
||||
width: 150px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.playable {
|
||||
background-color: $gray-light;
|
||||
}
|
||||
|
||||
.build-content {
|
||||
width: 130px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
a {
|
||||
color: $layout-link-gray;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.fa {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
// Connect first build in each stage with right horizontal line
|
||||
&:first-child {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -54px;
|
||||
border-top: 2px solid $border-color;
|
||||
width: 54px;
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
// Connect each build (except for first) with curved lines
|
||||
&:not(:first-child) {
|
||||
&::after, &::before {
|
||||
content: '';
|
||||
top: -47px;
|
||||
position: absolute;
|
||||
border-bottom: 2px solid $border-color;
|
||||
width: 20px;
|
||||
height: 65px;
|
||||
}
|
||||
|
||||
// Right connecting curves
|
||||
&::after {
|
||||
right: -20px;
|
||||
border-right: 2px solid $border-color;
|
||||
border-radius: 0 0 50px;
|
||||
}
|
||||
|
||||
// Left connecting curves
|
||||
&::before {
|
||||
left: -20px;
|
||||
border-left: 2px solid $border-color;
|
||||
border-radius: 0 0 0 50px;
|
||||
}
|
||||
}
|
||||
|
||||
// Connect second build to first build with smaller curved line
|
||||
&:nth-child(2) {
|
||||
&::after, &::before {
|
||||
height: 45px;
|
||||
top: -26px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
.build {
|
||||
// Remove right connecting horizontal line from first build in last stage
|
||||
&:first-child {
|
||||
&::after, &::before {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
// Remove right curved connectors from all builds in last stage
|
||||
&:not(:first-child) {
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
.build {
|
||||
// Remove left curved connectors from all builds in first stage
|
||||
&:not(:first-child) {
|
||||
&::before {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pipeline-actions {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.toggle-pipeline-btn {
|
||||
|
||||
.fa {
|
||||
color: $dropdown-header-color;
|
||||
}
|
||||
}
|
||||
|
||||
.pipelines.tab-pane {
|
||||
|
||||
.content-list.pipelines {
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.stage {
|
||||
max-width: 60px;
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -228,3 +228,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.u2f-registrations {
|
||||
th:not(:last-child), td:not(:last-child) {
|
||||
border-right: solid 1px transparent;
|
||||
}
|
||||
}
|
|
@ -99,7 +99,7 @@
|
|||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 15px;
|
||||
max-width: 480px;
|
||||
max-width: 700px;
|
||||
|
||||
> p {
|
||||
margin-bottom: 0;
|
||||
|
|
|
@ -20,10 +20,43 @@
|
|||
}
|
||||
}
|
||||
|
||||
.todo {
|
||||
.todos-list > .todo {
|
||||
// workaround because we cannot use border-colapse
|
||||
border-top: 1px solid transparent;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
|
||||
&:hover {
|
||||
background-color: $row-hover;
|
||||
border-color: $row-hover-border;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// overwrite border style of .content-list
|
||||
&:last-child {
|
||||
border-bottom: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: $row-hover-border;
|
||||
}
|
||||
}
|
||||
|
||||
.todo-actions {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
-webkit-flex: auto;
|
||||
flex: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
|
@ -43,8 +76,6 @@
|
|||
}
|
||||
|
||||
.todo-body {
|
||||
margin-right: 174px;
|
||||
|
||||
.todo-note {
|
||||
word-wrap: break-word;
|
||||
|
||||
|
@ -90,6 +121,12 @@
|
|||
}
|
||||
|
||||
@media (max-width: $screen-xs-max) {
|
||||
.todo {
|
||||
.avatar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.todo-item {
|
||||
.todo-title {
|
||||
white-space: normal;
|
||||
|
@ -98,10 +135,6 @@
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-body {
|
||||
margin: 0;
|
||||
border-left: 2px solid #ddd;
|
||||
|
|
|
@ -14,4 +14,14 @@ class Admin::SpamLogsController < Admin::ApplicationController
|
|||
head :ok
|
||||
end
|
||||
end
|
||||
|
||||
def mark_as_ham
|
||||
spam_log = SpamLog.find(params[:id])
|
||||
|
||||
if HamService.new(spam_log).mark_as_ham!
|
||||
redirect_to admin_spam_logs_path, notice: 'Spam log successfully submitted as ham.'
|
||||
else
|
||||
redirect_to admin_spam_logs_path, alert: 'Error with Akismet. Please check the logs for more info.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
class AutocompleteController < ApplicationController
|
||||
skip_before_action :authenticate_user!, only: [:users]
|
||||
before_action :load_project, only: [:users]
|
||||
before_action :find_users, only: [:users]
|
||||
|
||||
def users
|
||||
|
@ -34,19 +35,13 @@ class AutocompleteController < ApplicationController
|
|||
|
||||
def projects
|
||||
project = Project.find_by_id(params[:project_id])
|
||||
|
||||
projects = current_user.authorized_projects
|
||||
projects = projects.search(params[:search]) if params[:search].present?
|
||||
projects = projects.select do |project|
|
||||
current_user.can?(:admin_issue, project)
|
||||
end
|
||||
projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_id])
|
||||
|
||||
no_project = {
|
||||
id: 0,
|
||||
name_with_namespace: 'No project',
|
||||
}
|
||||
projects.unshift(no_project)
|
||||
projects.delete(project)
|
||||
projects.unshift(no_project) unless params[:offset_id].present?
|
||||
|
||||
render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace)
|
||||
end
|
||||
|
@ -55,11 +50,8 @@ class AutocompleteController < ApplicationController
|
|||
|
||||
def find_users
|
||||
@users =
|
||||
if params[:project_id].present?
|
||||
project = Project.find(params[:project_id])
|
||||
return render_404 unless can?(current_user, :read_project, project)
|
||||
|
||||
project.team.users
|
||||
if @project
|
||||
@project.team.users
|
||||
elsif params[:group_id].present?
|
||||
group = Group.find(params[:group_id])
|
||||
return render_404 unless can?(current_user, :read_group, group)
|
||||
|
@ -71,4 +63,18 @@ class AutocompleteController < ApplicationController
|
|||
User.none
|
||||
end
|
||||
end
|
||||
|
||||
def load_project
|
||||
@project ||= begin
|
||||
if params[:project_id].present?
|
||||
project = Project.find(params[:project_id])
|
||||
return render_404 unless can?(current_user, :read_project, project)
|
||||
project
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def projects_finder
|
||||
MoveToProjectFinder.new(current_user)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -66,6 +66,11 @@ module IssuableCollections
|
|||
key = 'issuable_sort'
|
||||
|
||||
cookies[key] = params[:sort] if params[:sort].present?
|
||||
|
||||
# id_desc and id_asc are old values for these two.
|
||||
cookies[key] = sort_value_recently_created if cookies[key] == 'id_desc'
|
||||
cookies[key] = sort_value_oldest_created if cookies[key] == 'id_asc'
|
||||
|
||||
params[:sort] = cookies[key]
|
||||
end
|
||||
|
||||
|
|
|
@ -7,11 +7,16 @@ module ServiceParams
|
|||
:build_key, :server, :teamcity_url, :drone_url, :build_type,
|
||||
:description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
|
||||
:colorize_messages, :channels,
|
||||
:push_events, :issues_events, :merge_requests_events, :tag_push_events,
|
||||
:note_events, :build_events, :wiki_page_events,
|
||||
:notify_only_broken_builds, :add_pusher,
|
||||
:send_from_committer_email, :disable_diffs, :external_wiki_url,
|
||||
:notify, :color,
|
||||
# We're using `issues_events` and `merge_requests_events`
|
||||
# in the view so we still need to explicitly state them
|
||||
# here. `Service#event_names` would only give
|
||||
# `issue_events` and `merge_request_events` (singular!)
|
||||
# See app/helpers/services_helper.rb for how we
|
||||
# make those event names plural as special case.
|
||||
:issues_events, :merge_requests_events,
|
||||
:notify_only_broken_builds, :notify_only_broken_pipelines,
|
||||
:add_pusher, :send_from_committer_email, :disable_diffs,
|
||||
:external_wiki_url, :notify, :color,
|
||||
:server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
|
||||
:jira_issue_transition_id]
|
||||
|
||||
|
@ -19,9 +24,7 @@ module ServiceParams
|
|||
FILTER_BLANK_PARAMS = [:password]
|
||||
|
||||
def service_params
|
||||
dynamic_params = []
|
||||
dynamic_params.concat(@service.event_channel_names)
|
||||
|
||||
dynamic_params = @service.event_channel_names + @service.event_names
|
||||
service_params = params.permit(:id, service: ALLOWED_PARAMS + dynamic_params)
|
||||
|
||||
if service_params[:service].is_a?(Hash)
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
module SpammableActions
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :authorize_submit_spammable!, only: :mark_as_spam
|
||||
end
|
||||
|
||||
def mark_as_spam
|
||||
if SpamService.new(spammable).mark_as_spam!
|
||||
redirect_to spammable, notice: "#{spammable.class.to_s} was submitted to Akismet successfully."
|
||||
else
|
||||
redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def spammable
|
||||
raise NotImplementedError, "#{self.class} does not implement #{__method__}"
|
||||
end
|
||||
|
||||
def authorize_submit_spammable!
|
||||
access_denied! unless current_user.admin?
|
||||
end
|
||||
end
|
|
@ -6,7 +6,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
|
|||
end
|
||||
|
||||
def destroy
|
||||
TodoService.new.mark_todos_as_done([todo], current_user)
|
||||
TodoService.new.mark_todos_as_done_by_ids([params[:id]], current_user)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
|
||||
|
@ -27,10 +27,6 @@ class Dashboard::TodosController < Dashboard::ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def todo
|
||||
@todo ||= find_todos.find(params[:id])
|
||||
end
|
||||
|
||||
def find_todos
|
||||
@todos ||= TodosFinder.new(current_user, params).execute
|
||||
end
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
class Import::GitlabProjectsController < Import::BaseController
|
||||
before_action :verify_gitlab_project_import_enabled
|
||||
before_action :authenticate_admin!
|
||||
|
||||
def new
|
||||
@namespace_id = project_params[:namespace_id]
|
||||
|
@ -47,4 +48,8 @@ class Import::GitlabProjectsController < Import::BaseController
|
|||
:path, :namespace_id, :file
|
||||
)
|
||||
end
|
||||
|
||||
def authenticate_admin!
|
||||
render_404 unless current_user.is_admin?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -43,11 +43,11 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
|||
# A U2F (universal 2nd factor) device's information is stored after successful
|
||||
# registration, which is then used while 2FA authentication is taking place.
|
||||
def create_u2f
|
||||
@u2f_registration = U2fRegistration.register(current_user, u2f_app_id, params[:device_response], session[:challenges])
|
||||
@u2f_registration = U2fRegistration.register(current_user, u2f_app_id, u2f_registration_params, session[:challenges])
|
||||
|
||||
if @u2f_registration.persisted?
|
||||
session.delete(:challenges)
|
||||
redirect_to profile_account_path, notice: "Your U2F device was registered!"
|
||||
redirect_to profile_two_factor_auth_path, notice: "Your U2F device was registered!"
|
||||
else
|
||||
@qr_code = build_qr_code
|
||||
setup_u2f_registration
|
||||
|
@ -91,15 +91,19 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
|||
# Actual communication is performed using a Javascript API
|
||||
def setup_u2f_registration
|
||||
@u2f_registration ||= U2fRegistration.new
|
||||
@registration_key_handles = current_user.u2f_registrations.pluck(:key_handle)
|
||||
@u2f_registrations = current_user.u2f_registrations
|
||||
u2f = U2F::U2F.new(u2f_app_id)
|
||||
|
||||
registration_requests = u2f.registration_requests
|
||||
sign_requests = u2f.authentication_requests(@registration_key_handles)
|
||||
sign_requests = u2f.authentication_requests(@u2f_registrations.map(&:key_handle))
|
||||
session[:challenges] = registration_requests.map(&:challenge)
|
||||
|
||||
gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
|
||||
register_requests: registration_requests,
|
||||
sign_requests: sign_requests })
|
||||
end
|
||||
|
||||
def u2f_registration_params
|
||||
params.require(:u2f_registration).permit(:device_response, :name)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
class Profiles::U2fRegistrationsController < Profiles::ApplicationController
|
||||
def destroy
|
||||
u2f_registration = current_user.u2f_registrations.find(params[:id])
|
||||
u2f_registration.destroy
|
||||
redirect_to profile_two_factor_auth_path, notice: "Successfully deleted U2F device."
|
||||
end
|
||||
end
|
|
@ -83,6 +83,7 @@ class Projects::ApplicationController < ApplicationController
|
|||
end
|
||||
|
||||
def apply_diff_view_cookie!
|
||||
@show_changes_tab = params[:view].present?
|
||||
cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present?
|
||||
end
|
||||
|
||||
|
|
|
@ -4,11 +4,24 @@ class Projects::BadgesController < Projects::ApplicationController
|
|||
before_action :no_cache_headers, except: [:index]
|
||||
|
||||
def build
|
||||
badge = Gitlab::Badge::Build.new(project, params[:ref])
|
||||
build_status = Gitlab::Badge::Build::Status
|
||||
.new(project, params[:ref])
|
||||
|
||||
render_badge build_status
|
||||
end
|
||||
|
||||
def coverage
|
||||
coverage_report = Gitlab::Badge::Coverage::Report
|
||||
.new(project, params[:ref], params[:job])
|
||||
|
||||
render_badge coverage_report
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_badge(badge)
|
||||
respond_to do |format|
|
||||
format.html { render_404 }
|
||||
|
||||
format.svg do
|
||||
render 'badge', locals: { badge: badge.template }
|
||||
end
|
||||
|
|
|
@ -17,6 +17,7 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
before_action :require_branch_head, only: [:edit, :update]
|
||||
before_action :editor_variables, except: [:show, :preview, :diff]
|
||||
before_action :validate_diff_params, only: :diff
|
||||
before_action :set_last_commit_sha, only: [:edit, :update]
|
||||
|
||||
def new
|
||||
commit unless @repository.empty?
|
||||
|
@ -33,7 +34,6 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def edit
|
||||
@last_commit = Gitlab::Git::Commit.last_for_path(@repository, @ref, @path).sha
|
||||
blob.load_all_data!(@repository)
|
||||
end
|
||||
|
||||
|
@ -55,6 +55,10 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
create_commit(Files::UpdateService, success_path: after_edit_path,
|
||||
failure_view: :edit,
|
||||
failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
|
||||
|
||||
rescue Files::UpdateService::FileChangedError
|
||||
@conflict = true
|
||||
render :edit
|
||||
end
|
||||
|
||||
def preview
|
||||
|
@ -152,7 +156,8 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
file_path: @file_path,
|
||||
commit_message: params[:commit_message],
|
||||
file_content: params[:content],
|
||||
file_content_encoding: params[:encoding]
|
||||
file_content_encoding: params[:encoding],
|
||||
last_commit_sha: params[:last_commit_sha]
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -161,4 +166,9 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
render nothing: true
|
||||
end
|
||||
end
|
||||
|
||||
def set_last_commit_sha
|
||||
@last_commit_sha = Gitlab::Git::Commit.
|
||||
last_for_path(@repository, @ref, @path).sha
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
class Projects::BoardListsController < Projects::ApplicationController
|
||||
respond_to :json
|
||||
|
||||
before_action :authorize_admin_list!
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
|
||||
|
||||
def create
|
||||
list = Boards::Lists::CreateService.new(project, current_user, list_params).execute
|
||||
|
||||
if list.valid?
|
||||
render json: list.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } })
|
||||
else
|
||||
render json: list.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
service = Boards::Lists::MoveService.new(project, current_user, move_params)
|
||||
|
||||
if service.execute
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
service = Boards::Lists::DestroyService.new(project, current_user, params)
|
||||
|
||||
if service.execute
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def generate
|
||||
service = Boards::Lists::GenerateService.new(project, current_user)
|
||||
|
||||
if service.execute
|
||||
render json: project.board.lists.label.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } })
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize_admin_list!
|
||||
return render_403 unless can?(current_user, :admin_list, project)
|
||||
end
|
||||
|
||||
def list_params
|
||||
params.require(:list).permit(:label_id)
|
||||
end
|
||||
|
||||
def move_params
|
||||
params.require(:list).permit(:position).merge(id: params[:id])
|
||||
end
|
||||
|
||||
def record_not_found(exception)
|
||||
render json: { error: exception.message }, status: :not_found
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
module Projects
|
||||
module Boards
|
||||
class ApplicationController < Projects::ApplicationController
|
||||
respond_to :json
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
|
||||
|
||||
private
|
||||
|
||||
def record_not_found(exception)
|
||||
render json: { error: exception.message }, status: :not_found
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,56 @@
|
|||
module Projects
|
||||
module Boards
|
||||
class IssuesController < Boards::ApplicationController
|
||||
before_action :authorize_read_issue!, only: [:index]
|
||||
before_action :authorize_update_issue!, only: [:update]
|
||||
|
||||
def index
|
||||
issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
|
||||
issues = issues.page(params[:page])
|
||||
|
||||
render json: issues.as_json(
|
||||
only: [:iid, :title, :confidential],
|
||||
include: {
|
||||
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
|
||||
labels: { only: [:id, :title, :description, :color, :priority] }
|
||||
})
|
||||
end
|
||||
|
||||
def update
|
||||
service = ::Boards::Issues::MoveService.new(project, current_user, move_params)
|
||||
|
||||
if service.execute(issue)
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def issue
|
||||
@issue ||=
|
||||
IssuesFinder.new(current_user, project_id: project.id, state: 'all')
|
||||
.execute
|
||||
.where(iid: params[:id])
|
||||
.first!
|
||||
end
|
||||
|
||||
def authorize_read_issue!
|
||||
return render_403 unless can?(current_user, :read_issue, project)
|
||||
end
|
||||
|
||||
def authorize_update_issue!
|
||||
return render_403 unless can?(current_user, :update_issue, issue)
|
||||
end
|
||||
|
||||
def filter_params
|
||||
params.merge(id: params[:list_id])
|
||||
end
|
||||
|
||||
def move_params
|
||||
params.permit(:id, :from_list_id, :to_list_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,81 @@
|
|||
module Projects
|
||||
module Boards
|
||||
class ListsController < Boards::ApplicationController
|
||||
before_action :authorize_admin_list!, only: [:create, :update, :destroy, :generate]
|
||||
before_action :authorize_read_list!, only: [:index]
|
||||
|
||||
def index
|
||||
render json: serialize_as_json(project.board.lists)
|
||||
end
|
||||
|
||||
def create
|
||||
list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute
|
||||
|
||||
if list.valid?
|
||||
render json: serialize_as_json(list)
|
||||
else
|
||||
render json: list.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
list = project.board.lists.movable.find(params[:id])
|
||||
service = ::Boards::Lists::MoveService.new(project, current_user, move_params)
|
||||
|
||||
if service.execute(list)
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
list = project.board.lists.destroyable.find(params[:id])
|
||||
service = ::Boards::Lists::DestroyService.new(project, current_user, params)
|
||||
|
||||
if service.execute(list)
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def generate
|
||||
service = ::Boards::Lists::GenerateService.new(project, current_user)
|
||||
|
||||
if service.execute
|
||||
render json: serialize_as_json(project.board.lists.movable)
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize_admin_list!
|
||||
return render_403 unless can?(current_user, :admin_list, project)
|
||||
end
|
||||
|
||||
def authorize_read_list!
|
||||
return render_403 unless can?(current_user, :read_list, project)
|
||||
end
|
||||
|
||||
def list_params
|
||||
params.require(:list).permit(:label_id)
|
||||
end
|
||||
|
||||
def move_params
|
||||
params.require(:list).permit(:position)
|
||||
end
|
||||
|
||||
def serialize_as_json(resource)
|
||||
resource.as_json(
|
||||
only: [:id, :list_type, :position],
|
||||
methods: [:title],
|
||||
include: {
|
||||
label: { only: [:id, :title, :description, :color, :priority] }
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue