Merge branch 'master' into feature/gb/variables-expressions-in-only-except
* master: (199 commits) Conflicts: lib/gitlab/ci/pipeline/seed/stage.rb
This commit is contained in:
commit
dbe9e0dff8
26
.babelrc
26
.babelrc
|
@ -1,20 +1,20 @@
|
|||
{
|
||||
"presets": [
|
||||
["latest", { "es2015": { "modules": false } }],
|
||||
"stage-2"
|
||||
],
|
||||
"presets": [["latest", { "es2015": { "modules": false } }], "stage-2"],
|
||||
"env": {
|
||||
"coverage": {
|
||||
"plugins": [
|
||||
["istanbul", {
|
||||
"exclude": [
|
||||
"spec/javascripts/**/*",
|
||||
"app/assets/javascripts/locale/**/app.js"
|
||||
]
|
||||
}],
|
||||
["transform-define", {
|
||||
"process.env.BABEL_ENV": "coverage"
|
||||
}]
|
||||
[
|
||||
"istanbul",
|
||||
{
|
||||
"exclude": ["spec/javascripts/**/*", "app/assets/javascripts/locale/**/app.js"]
|
||||
}
|
||||
],
|
||||
[
|
||||
"transform-define",
|
||||
{
|
||||
"process.env.BABEL_ENV": "coverage"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
/app/assets/javascripts/locale/**/app.js
|
||||
/config/
|
||||
/builds/
|
||||
/coverage/
|
||||
/coverage-javascript/
|
||||
/node_modules/
|
||||
/public/
|
||||
/scripts/
|
||||
/tmp/
|
||||
/vendor/
|
||||
karma.config.js
|
||||
webpack.config.js
|
||||
svg.config.js
|
||||
/app/assets/javascripts/locale/**/app.js
|
||||
|
|
|
@ -544,7 +544,7 @@ migration:path-mysql:
|
|||
.db-rollback: &db-rollback
|
||||
<<: *dedicated-no-docs-pull-cache-job
|
||||
script:
|
||||
- bundle exec rake db:rollback STEP=119
|
||||
- bundle exec rake db:migrate VERSION=20170523121229
|
||||
- bundle exec rake db:migrate
|
||||
|
||||
db:rollback-pg:
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
/app/assets/javascripts/locale/**/app.js
|
||||
/node_modules/
|
||||
/public/
|
||||
/vendor/
|
||||
/tmp/
|
11
.prettierrc
11
.prettierrc
|
@ -1,4 +1,13 @@
|
|||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
"trailingComma": "es5",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/app/**/*", "**/spec/**/*"],
|
||||
"options": {
|
||||
"trailingComma": "all"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -118,6 +118,9 @@ Gitlab/ModuleWithInstanceVariables:
|
|||
- spec/support/**/*.rb
|
||||
- features/steps/**/*.rb
|
||||
|
||||
Gitlab/HTTParty:
|
||||
Enabled: true
|
||||
|
||||
GitlabSecurity/PublicSend:
|
||||
Enabled: true
|
||||
Exclude:
|
||||
|
|
213
CHANGELOG.md
213
CHANGELOG.md
|
@ -2,6 +2,202 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 10.6.0 (2018-03-22)
|
||||
|
||||
### Security (4 changes)
|
||||
|
||||
- Fixed some SSRF vulnerabilities in services, hooks and integrations. !2337
|
||||
- Ensure that OTP backup codes are always invalidated.
|
||||
- Add verification for GitLab Pages custom domains.
|
||||
- Fix GitLab Auth0 integration signing in the wrong user.
|
||||
|
||||
### Fixed (75 changes, 17 of them are from the community)
|
||||
|
||||
- Ensure users cannot create environments with leading or trailing slashes (Fixes #39885). !15273
|
||||
- Fix new project path input overlapping. !16755 (George Tsiolis)
|
||||
- Respect description and visibility when creating project from template. !16820 (George Tsiolis)
|
||||
- Remove user notification settings for groups and projects when user leaves. !16906 (Jacopo Beschi @jacopo-beschi)
|
||||
- Fix Teleporting Emoji. !16963 (Jared Deckard <jared.deckard@gmail.com>)
|
||||
- Fix duplicate system notes when merging a merge request. !17035
|
||||
- Fix breadcrumb on labels page for groups. !17045 (Onuwa Nnachi Isaac)
|
||||
- Fix user avatar's vertical align on the issues and merge requests pages. !17072 (Laszlo Karpati)
|
||||
- Fix settings panels not expanding when fragment hash linked. !17074
|
||||
- Fix 404 when listing archived projects in a group where all projects have been archived. !17077 (Ashley Dumaine)
|
||||
- Allow to call PUT /projects/:id API with only ci_config_path specified. !17105 (Laszlo Karpati)
|
||||
- Fix long list of recipients on group request membership email. !17121 (Jacopo Beschi @jacopo-beschi)
|
||||
- Remove duplicated error message on duplicate variable validation. !17135
|
||||
- Keep "Import project" tab/form active when validation fails trying to import "Repo by URL". !17136
|
||||
- Fixed bug with unauthenticated requests through git ssh. !17149
|
||||
- Allows project rename after validation error. !17150
|
||||
- Fix "Remove source branch" button in Merge request widget during merge when pipeline succeeds state. !17192
|
||||
- Add missing pagination on the commit diff endpoint. !17203 (Maxime Roussin-Bélanger)
|
||||
- Fix get a single pages domain when project path contains a period. !17206 (Travis Miller)
|
||||
- remove avater underline. !17219 (Ken Ding)
|
||||
- Allows the usage of /milestone quick action for group milestones. !17239 (Jacopo Beschi @jacopo-beschi)
|
||||
- Encode branch name as binary before creating a RPC request to copy attributes. !17291
|
||||
- Restart Unicorn and Sidekiq when GRPC throws 14:Endpoint read failed. !17293
|
||||
- Do not persist Google Project verification flash errors after a page reload. !17299
|
||||
- Ensure group issues and merge requests pages show results from subgroups when there are no results from the current group. !17312
|
||||
- Prevent trace artifact migration to incur data loss. !17313
|
||||
- Fixes gpg popover layout. !17323
|
||||
- Return a 404 instead of 403 if the repository does not exist on disk. !17341
|
||||
- Fix Slack/Mattermost notifications not respecting `notify_only_default_branch` setting for pushes. !17345
|
||||
- Fix Group labels load failure when there are duplicate labels present. !17353
|
||||
- Allow Prometheus application to be installed from Cluster applications. !17372
|
||||
- Fixes Prometheus admin configuration page. !17377
|
||||
- Enable filtering MR list based on clicked label in MR sidebar. !17390
|
||||
- Fix code and wiki search results pages when non-ASCII text is displayed. !17413
|
||||
- Count comments on diffs and discussions as contributions for the contributions calendar. !17418 (Riccardo Padovani)
|
||||
- Add Assignees vue component missing data container. !17426 (George Tsiolis)
|
||||
- Update tooltip on pipeline cancel to Stop (#42946). !17444
|
||||
- Removing the two factor check when the user sets a new password. !17457
|
||||
- Fix quick actions for users who cannot update issues and merge requests. !17482
|
||||
- Stop loading spinner on error of milestone update on issue. !17507 (Takuya Noguchi)
|
||||
- Set margins around dropdown dividers to 4px. !17517
|
||||
- Fix pages flaky failure by reloading stale object. !17522
|
||||
- Remove extra breadcrumb on tags. !17562 (Takuya Noguchi)
|
||||
- Fix missing uploads after group transfer. !17658
|
||||
- Fix markdown table showing extra column. !17669
|
||||
- Ensure the API returns https links when https is configured. !17681
|
||||
- Sanitize extra blank spaces used when uploading a SSH key. !40552
|
||||
- Render htmlentities correctly for links not supported by Rinku.
|
||||
- Keep link when redacting unauthorized object links.
|
||||
- Handle empty state in Pipelines page.
|
||||
- Revert Project.public_or_visible_to_user changes and only apply to snippets.
|
||||
- Release libgit2 cache and open file descriptors after `git gc` run.
|
||||
- Fix project dashboard showing the wrong timestamps.
|
||||
- Fix "Can't modify frozen hash" error when project is destroyed.
|
||||
- Fix Error 500 when viewing a commit with a GPG signature in Geo.
|
||||
- Don't error out in system hook if user has `nil` datetime columns.
|
||||
- Remove double caching of Repository#empty?.
|
||||
- Don't delete todos or unassign issues and MRs when a user leaves a project.
|
||||
- Don't cache a nil repository root ref to prevent caching issues.
|
||||
- Escape HTML entities in commit messages.
|
||||
- Verify project import status again before marking as failed.
|
||||
- [GitHub Import] Create an empty wiki if wiki import failed.
|
||||
- Create empty wiki when import from GitLab and wiki is not there.
|
||||
- Make sure wiki exists when it's enabled.
|
||||
- Fix broken loading state for close issue button.
|
||||
- Fix code and wiki search results when filename is non-ASCII.
|
||||
- Fix file upload on project show page.
|
||||
- Fix squashing when a file is renamed.
|
||||
- Show loading button inline in refresh button in MR widget.
|
||||
- Fix close button on issues not working on mobile.
|
||||
- Adds tooltip in environment names to increase readability.
|
||||
- Fixed issue edit shortcut not opening edit form.
|
||||
- Fix 500 error being shown when diff has context marker with invalid encoding.
|
||||
- Render modified icon for moved file in changes dropdown.
|
||||
- Remember assignee when moving an issue.
|
||||
|
||||
### Changed (16 changes, 9 of them are from the community)
|
||||
|
||||
- Allow including custom attributes in API responses. !16526 (Markus Koller)
|
||||
- Apply new default and inline label design. !16956 (George Tsiolis)
|
||||
- Remove whitespace from the username/email sign in form field. !17020 (Peter lauck)
|
||||
- CI charts now include the current day. !17032 (Dakkaron)
|
||||
- Hide CI secret variable values after saving. !17044
|
||||
- Add new modal Vue component. !17108
|
||||
- Asciidoc now support inter-document cross references between files in repository. !17125 (Turo Soisenniemi)
|
||||
- Update issue closing pattern to allow variations in punctuation. !17198 (Vicky Chijwani)
|
||||
- Add a button to deploy a runner to a Kubernetes cluster in the settings page. !17278
|
||||
- Pages custom domain: allow update of key/certificate. !17376 (rfwatson)
|
||||
- Clear the Labels dropdown search filter after a selection is made. !17393 (Andrew Torres)
|
||||
- Hook data for pipelines includes detailed_status. !17607
|
||||
- Avoid showing unnecessary Trigger checkboxes for project Integrations with only one event. !17607
|
||||
- Display a link to external issue tracker when enabled.
|
||||
- Allow token authentication on go-get request.
|
||||
- Update SSH key link to include existing keys. (Brendan O'Leary)
|
||||
|
||||
### Performance (24 changes, 5 of them are from the community)
|
||||
|
||||
- Add catch-up background migration to migrate pipeline stages. !15741
|
||||
- Move BoardNewIssue vue component. !16947 (George Tsiolis)
|
||||
- Move IssuableTimeTracker vue component. !16948 (George Tsiolis)
|
||||
- Move RecentSearchesDropdownContent vue component. !16951 (George Tsiolis)
|
||||
- Move Assignees vue component. !16952 (George Tsiolis)
|
||||
- Improve performance of pipeline page by reducing DB queries. !17168
|
||||
- Store sha256 checksum to job artifacts. !17354
|
||||
- Move SidebarAssignees vue component. !17398 (George Tsiolis)
|
||||
- Improve database response time for user activity listing. !17454
|
||||
- Use persisted/memoized value for MRs shas instead of doing git lookups. !17555
|
||||
- Cache MergeRequests can_be_resolved_in_ui? git operations. !17589
|
||||
- Prevent the graphs page from generating unnecessary Gitaly requests. !37602
|
||||
- Use a user object in ApplicationHelper#avatar_icon where possible to avoid N+1 queries. !42800
|
||||
- Submit a single batch blob RPC to Gitaly per HTTP request when viewing diffs.
|
||||
- Avoid re-fetching merge-base SHA from Gitaly unnecessarily.
|
||||
- Don't use ProjectsFinder in TodosFinder.
|
||||
- Adding missing indexes on taggings table.
|
||||
- Add index on section_name_id on ci_build_trace_sections table.
|
||||
- Cache column_exists? for application settings.
|
||||
- Cache table_exists?('application_settings') to reduce repeated schema reloads.
|
||||
- Make --prune a configurable parameter in fetching a git remote.
|
||||
- Fix timeouts loading /admin/projects page.
|
||||
- Add partial indexes on todos to handle users with many todos.
|
||||
- Optimize search queries on the search page by setting a limit for matching records in project scope.
|
||||
|
||||
### Added (30 changes, 9 of them are from the community)
|
||||
|
||||
- Add CommonMark markdown engine (experimental). !14835 (blackst0ne)
|
||||
- API: Get references a commit is pushed to. !15026 (Robert Schilling)
|
||||
- Add overview of branches and a filter for active/stale branches. !15402 (Takuya Noguchi)
|
||||
- Add project export API. !15860 (Travis Miller)
|
||||
- expose more metrics in merge requests api. !16589 (haseebeqx)
|
||||
- #28481: Display time tracking totals on milestone page. !16753 (Riccardo Padovani)
|
||||
- Add a button on the project page to set up a Kubernetes cluster and enable Auto DevOps. !16900
|
||||
- Include cycle time in usage ping data. !16973
|
||||
- Add ability to use external plugins as an alternative to system hooks. !17003
|
||||
- Add search param to Branches API. !17005 (bunufi)
|
||||
- API endpoint for importing a project export. !17025
|
||||
- Display ingress IP address in the Kubernetes page. !17052
|
||||
- Implemented badge API endpoints. !17082
|
||||
- Allow installation of GitLab Runner with a single click. !17134
|
||||
- Allow commits endpoint to work over all commits of a repository. !17182
|
||||
- Display Runner IP Address. !17286
|
||||
- Add archive feature to trace. !17314
|
||||
- Allow maintainers to push to forks of their projects when a merge request is open. !17395
|
||||
- Foreground verification of uploads and LFS objects. !17402
|
||||
- Adds updated_at filter to issues and merge_requests API. !17417 (Jacopo Beschi @jacopo-beschi)
|
||||
- Port /wip quick action command to Merge Request creation (on description). !17463 (Adam Pahlevi)
|
||||
- Add a paragraph about security implications on Cluster's page. !17486
|
||||
- Add plugins list to the system hooks page. !17518
|
||||
- Enable privileged mode for GitLab Runner. !17528
|
||||
- Expose GITLAB_FEATURES as CI/CD variable (fixes #40994).
|
||||
- Upgrade GitLab Workhorse to 4.0.0.
|
||||
- Allow CI/CD Jobs being grouped on version strings.
|
||||
- Add discussions API for Issues and Snippets.
|
||||
- Add one group board to Libre.
|
||||
- Add support for filtering by source and target branch to merge requests API.
|
||||
|
||||
### Other (18 changes, 7 of them are from the community)
|
||||
|
||||
- Group MRs on issue page by project and namespace. !8494 (Jeff Stubler)
|
||||
- Make oauth provider login generic. !8809 (Horatiu Eugen Vlad)
|
||||
- Add email button to new issue by email. !10942 (Islam Wazery)
|
||||
- Update vue component naming guidelines. !17018 (George Tsiolis)
|
||||
- Added new design for promotion modals. !17197
|
||||
- Update to github-linguist 5.3.x. !17241 (Ken Ding)
|
||||
- update toml-rb to 1.0.0. !17259 (Ken Ding)
|
||||
- Keep track of projects a user interacted with. !17327
|
||||
- Moved o_auth/saml/ldap modules under gitlab/auth. !17359 (Horatiu Eugen Vlad)
|
||||
- Enables eslint in codeclimate job. !17392
|
||||
- Port Labels Select dropdown to Vue. !17411
|
||||
- Add NOT NULL constraint to projects.namespace_id. !17448
|
||||
- Ensure foreign keys on clusters applications. !17488
|
||||
- Started translation into Turkish, Indonesian and Filipino. !17526
|
||||
- Add documentation for displayed K8s Ingress IP address (#44330). !17836
|
||||
- Move Ruby endpoints to OPT_OUT.
|
||||
- Upgrade Workhorse to version 3.8.0 to support structured logging.
|
||||
- Use host URL to build JIRA remote link icon.
|
||||
|
||||
|
||||
## 10.5.6 (2018-03-16)
|
||||
|
||||
### Security (2 changes)
|
||||
|
||||
- Fixed some SSRF vulnerabilities in services, hooks and integrations. !2337
|
||||
- Fix GitLab Auth0 integration signing in the wrong user.
|
||||
|
||||
|
||||
## 10.5.5 (2018-03-15)
|
||||
|
||||
### Fixed (3 changes)
|
||||
|
@ -261,6 +457,14 @@ entry.
|
|||
- Adds empty state illustration for pending job.
|
||||
|
||||
|
||||
## 10.4.6 (2018-03-16)
|
||||
|
||||
### Security (2 changes)
|
||||
|
||||
- Fixed some SSRF vulnerabilities in services, hooks and integrations. !2337
|
||||
- Fix GitLab Auth0 integration signing in the wrong user.
|
||||
|
||||
|
||||
## 10.4.5 (2018-03-01)
|
||||
|
||||
### Security (1 change)
|
||||
|
@ -492,6 +696,15 @@ entry.
|
|||
- Use a background migration for issues.closed_at.
|
||||
|
||||
|
||||
## 10.3.9 (2018-03-16)
|
||||
|
||||
### Security (3 changes)
|
||||
|
||||
- Fixed some SSRF vulnerabilities in services, hooks and integrations. !2337
|
||||
- Update nokogiri to 1.8.2. !16807
|
||||
- Fix GitLab Auth0 integration signing in the wrong user.
|
||||
|
||||
|
||||
## 10.3.8 (2018-03-01)
|
||||
|
||||
### Security (1 change)
|
||||
|
|
|
@ -1 +1 @@
|
|||
6.0.4
|
||||
7.1.1
|
||||
|
|
49
Gemfile
49
Gemfile
|
@ -1,6 +1,19 @@
|
|||
# --- Special code for migrating to Rails 5.0 ---
|
||||
def rails5?
|
||||
%w[1 true].include?(ENV["RAILS5"])
|
||||
end
|
||||
|
||||
gem_versions = {}
|
||||
gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2'
|
||||
gem_versions['default_value_for'] = rails5? ? '~> 3.0.5' : '~> 3.0.0'
|
||||
gem_versions['html-pipeline'] = rails5? ? '~> 2.6.0' : '~> 1.11.0'
|
||||
gem_versions['rails'] = rails5? ? '5.0.6' : '4.2.10'
|
||||
gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9'
|
||||
# --- The end of special code for migrating to Rails 5.0 ---
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'rails', '4.2.10'
|
||||
gem 'rails', gem_versions['rails']
|
||||
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
|
||||
|
||||
# Responders respond_to and respond_with
|
||||
|
@ -9,7 +22,7 @@ gem 'responders', '~> 2.0'
|
|||
gem 'sprockets', '~> 3.7.0'
|
||||
|
||||
# Default values for AR models
|
||||
gem 'default_value_for', '~> 3.0.0'
|
||||
gem 'default_value_for', gem_versions['default_value_for']
|
||||
|
||||
# Supported DBs
|
||||
gem 'mysql2', '~> 0.4.10', group: :mysql
|
||||
|
@ -22,10 +35,10 @@ gem 'faraday', '~> 0.12'
|
|||
|
||||
# Authentication libraries
|
||||
gem 'devise', '~> 4.2'
|
||||
gem 'doorkeeper', '~> 4.2.0'
|
||||
gem 'doorkeeper-openid_connect', '~> 1.2.0'
|
||||
gem 'omniauth', '~> 1.4.2'
|
||||
gem 'omniauth-auth0', '~> 1.4.1'
|
||||
gem 'doorkeeper', '~> 4.3'
|
||||
gem 'doorkeeper-openid_connect', '~> 1.3'
|
||||
gem 'omniauth', '~> 1.8'
|
||||
gem 'omniauth-auth0', '~> 2.0.0'
|
||||
gem 'omniauth-azure-oauth2', '~> 0.0.9'
|
||||
gem 'omniauth-cas3', '~> 1.1.4'
|
||||
gem 'omniauth-facebook', '~> 4.0.0'
|
||||
|
@ -34,9 +47,9 @@ gem 'omniauth-gitlab', '~> 1.0.2'
|
|||
gem 'omniauth-google-oauth2', '~> 0.5.2'
|
||||
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
|
||||
gem 'omniauth-oauth2-generic', '~> 0.2.2'
|
||||
gem 'omniauth-saml', '~> 1.10.0'
|
||||
gem 'omniauth-saml', '~> 1.10'
|
||||
gem 'omniauth-shibboleth', '~> 1.2.0'
|
||||
gem 'omniauth-twitter', '~> 1.2.0'
|
||||
gem 'omniauth-twitter', '~> 1.4'
|
||||
gem 'omniauth_crowd', '~> 2.2.0'
|
||||
gem 'omniauth-authentiq', '~> 0.3.1'
|
||||
gem 'rack-oauth2', '~> 1.2.1'
|
||||
|
@ -113,7 +126,7 @@ gem 'fog-rackspace', '~> 0.1.1'
|
|||
gem 'fog-aliyun', '~> 0.2.0'
|
||||
|
||||
# for Google storage
|
||||
gem 'google-api-client', '~> 0.13.6'
|
||||
gem 'google-api-client', '~> 0.19.8'
|
||||
|
||||
# for aws storage
|
||||
gem 'unf', '~> 0.1.4'
|
||||
|
@ -122,7 +135,7 @@ gem 'unf', '~> 0.1.4'
|
|||
gem 'seed-fu', '~> 2.3.7'
|
||||
|
||||
# Markdown and HTML processing
|
||||
gem 'html-pipeline', '~> 1.11.0'
|
||||
gem 'html-pipeline', gem_versions['html-pipeline']
|
||||
gem 'deckar01-task_list', '2.0.0'
|
||||
gem 'gitlab-markup', '~> 1.6.2'
|
||||
gem 'redcarpet', '~> 3.4'
|
||||
|
@ -149,7 +162,7 @@ group :unicorn do
|
|||
end
|
||||
|
||||
# State machine
|
||||
gem 'state_machines-activerecord', '~> 0.4.0'
|
||||
gem 'state_machines-activerecord', '~> 0.5.1'
|
||||
|
||||
# Issue tags
|
||||
gem 'acts-as-taggable-on', '~> 5.0'
|
||||
|
@ -218,10 +231,10 @@ gem 'sanitize', '~> 2.0'
|
|||
gem 'babosa', '~> 1.0.2'
|
||||
|
||||
# Sanitizes SVG input
|
||||
gem 'loofah', '~> 2.0.3'
|
||||
gem 'loofah', '~> 2.2'
|
||||
|
||||
# Working with license
|
||||
gem 'licensee', '~> 8.7.0'
|
||||
gem 'licensee', '~> 8.9'
|
||||
|
||||
# Protect against bruteforcing
|
||||
gem 'rack-attack', '~> 4.4.1'
|
||||
|
@ -235,9 +248,6 @@ gem 'mousetrap-rails', '~> 1.4.6'
|
|||
# Detect and convert string character encoding
|
||||
gem 'charlock_holmes', '~> 0.7.5'
|
||||
|
||||
# Faster JSON
|
||||
gem 'oj', '~> 2.17.4'
|
||||
|
||||
# Faster blank
|
||||
gem 'fast_blank'
|
||||
|
||||
|
@ -269,9 +279,9 @@ gem 'premailer-rails', '~> 1.9.7'
|
|||
|
||||
# I18n
|
||||
gem 'ruby_parser', '~> 3.8', require: false
|
||||
gem 'rails-i18n', '~> 4.0.9'
|
||||
gem 'rails-i18n', gem_versions['rails-i18n']
|
||||
gem 'gettext_i18n_rails', '~> 1.8.0'
|
||||
gem 'gettext_i18n_rails_js', '~> 1.2.0'
|
||||
gem 'gettext_i18n_rails_js', '~> 1.3'
|
||||
gem 'gettext', '~> 3.2.2', require: false, group: :development
|
||||
|
||||
gem 'batch-loader', '~> 1.2.1'
|
||||
|
@ -279,7 +289,6 @@ gem 'batch-loader', '~> 1.2.1'
|
|||
# Perf bar
|
||||
gem 'peek', '~> 1.0.1'
|
||||
gem 'peek-gc', '~> 0.0.2'
|
||||
gem 'peek-host', '~> 1.0.0'
|
||||
gem 'peek-mysql2', '~> 1.1.0', group: :mysql
|
||||
gem 'peek-performance_bar', '~> 1.3.0'
|
||||
gem 'peek-pg', '~> 1.3.0', group: :postgres
|
||||
|
@ -361,7 +370,7 @@ group :development, :test do
|
|||
gem 'license_finder', '~> 3.1', require: false
|
||||
gem 'knapsack', '~> 1.16'
|
||||
|
||||
gem 'activerecord_sane_schema_dumper', '0.2'
|
||||
gem 'activerecord_sane_schema_dumper', gem_versions['activerecord_sane_schema_dumper']
|
||||
|
||||
gem 'stackprof', '~> 0.2.10', require: false
|
||||
|
||||
|
|
102
Gemfile.lock
102
Gemfile.lock
|
@ -47,6 +47,7 @@ GEM
|
|||
memoizable (~> 0.4.0)
|
||||
addressable (2.5.2)
|
||||
public_suffix (>= 2.0.2, < 4.0)
|
||||
aes_key_wrap (1.0.1)
|
||||
akismet (2.0.0)
|
||||
allocations (1.0.5)
|
||||
arel (6.0.4)
|
||||
|
@ -86,7 +87,7 @@ GEM
|
|||
coderay (>= 1.0.0)
|
||||
erubis (>= 2.6.6)
|
||||
rack (>= 0.9.0)
|
||||
bindata (2.4.1)
|
||||
bindata (2.4.3)
|
||||
binding_of_caller (0.7.2)
|
||||
debug_inspector (>= 0.0.1)
|
||||
blankslate (2.1.2.4)
|
||||
|
@ -142,6 +143,7 @@ GEM
|
|||
connection_pool (2.2.1)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.3)
|
||||
creole (0.5.0)
|
||||
css_parser (1.5.0)
|
||||
addressable
|
||||
|
@ -176,10 +178,10 @@ GEM
|
|||
docile (1.1.5)
|
||||
domain_name (0.5.20170404)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (4.2.6)
|
||||
doorkeeper (4.3.1)
|
||||
railties (>= 4.2)
|
||||
doorkeeper-openid_connect (1.2.0)
|
||||
doorkeeper (~> 4.0)
|
||||
doorkeeper-openid_connect (1.3.0)
|
||||
doorkeeper (~> 4.3)
|
||||
json-jwt (~> 1.6)
|
||||
dropzonejs-rails (0.7.2)
|
||||
rails (> 3.1)
|
||||
|
@ -210,7 +212,7 @@ GEM
|
|||
faraday_middleware
|
||||
multi_json
|
||||
fast_blank (1.0.0)
|
||||
fast_gettext (1.4.0)
|
||||
fast_gettext (1.6.0)
|
||||
ffaker (2.4.0)
|
||||
ffi (1.9.18)
|
||||
flay (2.10.0)
|
||||
|
@ -276,12 +278,12 @@ GEM
|
|||
gemojione (3.3.0)
|
||||
json
|
||||
get_process_mem (0.2.0)
|
||||
gettext (3.2.2)
|
||||
gettext (3.2.9)
|
||||
locale (>= 2.0.5)
|
||||
text (>= 1.3.0)
|
||||
gettext_i18n_rails (1.8.0)
|
||||
fast_gettext (>= 0.9.0)
|
||||
gettext_i18n_rails_js (1.2.0)
|
||||
gettext_i18n_rails_js (1.3.0)
|
||||
gettext (>= 3.0.2)
|
||||
gettext_i18n_rails (>= 0.7.1)
|
||||
po_to_json (>= 1.0.0)
|
||||
|
@ -335,9 +337,9 @@ GEM
|
|||
json
|
||||
multi_json
|
||||
request_store (>= 1.0)
|
||||
google-api-client (0.13.6)
|
||||
google-api-client (0.19.8)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 0.5)
|
||||
googleauth (>= 0.5, < 0.7.0)
|
||||
httpclient (>= 2.8.1, < 3.0)
|
||||
mime-types (~> 3.0)
|
||||
representable (~> 3.0)
|
||||
|
@ -355,7 +357,7 @@ GEM
|
|||
signet (~> 0.7)
|
||||
gpgme (2.0.13)
|
||||
mini_portile2 (~> 2.1)
|
||||
grape (1.0.0)
|
||||
grape (1.0.2)
|
||||
activesupport
|
||||
builder
|
||||
mustermann-grape (~> 1.0.0)
|
||||
|
@ -414,7 +416,7 @@ GEM
|
|||
httparty (0.13.7)
|
||||
json (~> 1.8)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.8.2)
|
||||
httpclient (2.8.3)
|
||||
i18n (0.9.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
ice_nine (0.11.2)
|
||||
|
@ -428,10 +430,10 @@ GEM
|
|||
oauth (~> 0.5, >= 0.5.0)
|
||||
jquery-atwho-rails (1.3.2)
|
||||
json (1.8.6)
|
||||
json-jwt (1.7.2)
|
||||
json-jwt (1.9.2)
|
||||
activesupport
|
||||
aes_key_wrap
|
||||
bindata
|
||||
multi_json (>= 1.3)
|
||||
securecompare
|
||||
url_safe_base64
|
||||
json-schema (2.8.0)
|
||||
|
@ -473,7 +475,7 @@ GEM
|
|||
toml (= 0.1.2)
|
||||
with_env (> 1.0)
|
||||
xml-simple
|
||||
licensee (8.7.0)
|
||||
licensee (8.9.2)
|
||||
rugged (~> 0.24)
|
||||
little-plugger (1.1.4)
|
||||
locale (2.1.2)
|
||||
|
@ -484,7 +486,8 @@ GEM
|
|||
actionpack (>= 4, < 5.2)
|
||||
activesupport (>= 4, < 5.2)
|
||||
railties (>= 4, < 5.2)
|
||||
loofah (2.0.3)
|
||||
loofah (2.2.2)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.0)
|
||||
mini_mime (>= 0.1.1)
|
||||
|
@ -497,14 +500,14 @@ GEM
|
|||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2016.0521)
|
||||
mimemagic (0.3.0)
|
||||
mini_mime (0.1.4)
|
||||
mini_mime (1.0.0)
|
||||
mini_portile2 (2.3.0)
|
||||
minitest (5.7.0)
|
||||
mousetrap-rails (1.4.6)
|
||||
multi_json (1.13.1)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
mustermann (1.0.0)
|
||||
mustermann (1.0.2)
|
||||
mustermann-grape (1.0.0)
|
||||
mustermann (~> 1.0.0)
|
||||
mysql2 (0.4.10)
|
||||
|
@ -514,7 +517,7 @@ GEM
|
|||
nokogiri (1.8.2)
|
||||
mini_portile2 (~> 2.3.0)
|
||||
numerizer (0.1.1)
|
||||
oauth (0.5.1)
|
||||
oauth (0.5.4)
|
||||
oauth2 (1.4.0)
|
||||
faraday (>= 0.8, < 0.13)
|
||||
jwt (~> 1.0)
|
||||
|
@ -523,12 +526,11 @@ GEM
|
|||
rack (>= 1.2, < 3)
|
||||
octokit (4.8.0)
|
||||
sawyer (~> 0.8.0, >= 0.5.3)
|
||||
oj (2.17.5)
|
||||
omniauth (1.4.3)
|
||||
hashie (>= 1.2, < 4)
|
||||
omniauth (1.8.1)
|
||||
hashie (>= 3.4.6, < 3.6.0)
|
||||
rack (>= 1.6.2, < 3)
|
||||
omniauth-auth0 (1.4.1)
|
||||
omniauth-oauth2 (~> 1.1)
|
||||
omniauth-auth0 (2.0.0)
|
||||
omniauth-oauth2 (~> 1.4)
|
||||
omniauth-authentiq (0.3.1)
|
||||
omniauth-oauth2 (~> 1.3, >= 1.3.1)
|
||||
omniauth-azure-oauth2 (0.0.9)
|
||||
|
@ -570,9 +572,9 @@ GEM
|
|||
ruby-saml (~> 1.7)
|
||||
omniauth-shibboleth (1.2.1)
|
||||
omniauth (>= 1.0.0)
|
||||
omniauth-twitter (1.2.1)
|
||||
json (~> 1.3)
|
||||
omniauth-twitter (1.4.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
omniauth_crowd (2.2.3)
|
||||
activesupport
|
||||
nokogiri (>= 1.4.4)
|
||||
|
@ -593,8 +595,6 @@ GEM
|
|||
railties (>= 4.0.0)
|
||||
peek-gc (0.0.2)
|
||||
peek
|
||||
peek-host (1.0.0)
|
||||
peek
|
||||
peek-mysql2 (1.1.0)
|
||||
atomic (>= 1.0.0)
|
||||
mysql2
|
||||
|
@ -658,7 +658,7 @@ GEM
|
|||
httpclient (>= 2.4)
|
||||
multi_json (>= 1.3.6)
|
||||
rack (>= 1.1)
|
||||
rack-protection (1.5.3)
|
||||
rack-protection (2.0.1)
|
||||
rack
|
||||
rack-proxy (0.6.0)
|
||||
rack
|
||||
|
@ -677,12 +677,12 @@ GEM
|
|||
sprockets-rails
|
||||
rails-deprecated_sanitizer (1.0.3)
|
||||
activesupport (>= 4.2.0.alpha)
|
||||
rails-dom-testing (1.0.8)
|
||||
activesupport (>= 4.2.0.beta, < 5.0)
|
||||
rails-dom-testing (1.0.9)
|
||||
activesupport (>= 4.2.0, < 5.0)
|
||||
nokogiri (~> 1.6)
|
||||
rails-deprecated_sanitizer (>= 1.0.1)
|
||||
rails-html-sanitizer (1.0.3)
|
||||
loofah (~> 2.0)
|
||||
rails-html-sanitizer (1.0.4)
|
||||
loofah (~> 2.2, >= 2.2.2)
|
||||
rails-i18n (4.0.9)
|
||||
i18n (~> 0.7)
|
||||
railties (~> 4.0)
|
||||
|
@ -897,13 +897,13 @@ GEM
|
|||
sqlite3 (1.3.13)
|
||||
sshkey (1.9.0)
|
||||
stackprof (0.2.10)
|
||||
state_machines (0.4.0)
|
||||
state_machines-activemodel (0.4.0)
|
||||
activemodel (>= 4.1, < 5.1)
|
||||
state_machines (>= 0.4.0)
|
||||
state_machines-activerecord (0.4.0)
|
||||
activerecord (>= 4.1, < 5.1)
|
||||
state_machines-activemodel (>= 0.3.0)
|
||||
state_machines (0.5.0)
|
||||
state_machines-activemodel (0.5.1)
|
||||
activemodel (>= 4.1, < 6.0)
|
||||
state_machines (>= 0.5.0)
|
||||
state_machines-activerecord (0.5.1)
|
||||
activerecord (>= 4.1, < 6.0)
|
||||
state_machines-activemodel (>= 0.5.0)
|
||||
stringex (2.7.1)
|
||||
sys-filesystem (1.1.6)
|
||||
ffi
|
||||
|
@ -1030,8 +1030,8 @@ DEPENDENCIES
|
|||
devise (~> 4.2)
|
||||
devise-two-factor (~> 3.0.0)
|
||||
diffy (~> 3.1.0)
|
||||
doorkeeper (~> 4.2.0)
|
||||
doorkeeper-openid_connect (~> 1.2.0)
|
||||
doorkeeper (~> 4.3)
|
||||
doorkeeper-openid_connect (~> 1.3)
|
||||
dropzonejs-rails (~> 0.7.1)
|
||||
email_reply_trimmer (~> 0.1)
|
||||
email_spec (~> 1.6.0)
|
||||
|
@ -1057,7 +1057,7 @@ DEPENDENCIES
|
|||
gemojione (~> 3.3)
|
||||
gettext (~> 3.2.2)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.2.0)
|
||||
gettext_i18n_rails_js (~> 1.3)
|
||||
gitaly-proto (~> 0.88.0)
|
||||
github-linguist (~> 5.3.3)
|
||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||
|
@ -1067,7 +1067,7 @@ DEPENDENCIES
|
|||
gollum-lib (~> 4.2)
|
||||
gollum-rugged_adapter (~> 0.4.4)
|
||||
gon (~> 6.1.0)
|
||||
google-api-client (~> 0.13.6)
|
||||
google-api-client (~> 0.19.8)
|
||||
google-protobuf (= 3.5.1)
|
||||
gpgme
|
||||
grape (~> 1.0)
|
||||
|
@ -1093,9 +1093,9 @@ DEPENDENCIES
|
|||
kubeclient (~> 3.0)
|
||||
letter_opener_web (~> 1.3.0)
|
||||
license_finder (~> 3.1)
|
||||
licensee (~> 8.7.0)
|
||||
licensee (~> 8.9)
|
||||
lograge (~> 0.5)
|
||||
loofah (~> 2.0.3)
|
||||
loofah (~> 2.2)
|
||||
mail_room (~> 0.9.1)
|
||||
method_source (~> 0.8)
|
||||
minitest (~> 5.7.0)
|
||||
|
@ -1106,9 +1106,8 @@ DEPENDENCIES
|
|||
nokogiri (~> 1.8.2)
|
||||
oauth2 (~> 1.4)
|
||||
octokit (~> 4.8)
|
||||
oj (~> 2.17.4)
|
||||
omniauth (~> 1.4.2)
|
||||
omniauth-auth0 (~> 1.4.1)
|
||||
omniauth (~> 1.8)
|
||||
omniauth-auth0 (~> 2.0.0)
|
||||
omniauth-authentiq (~> 0.3.1)
|
||||
omniauth-azure-oauth2 (~> 0.0.9)
|
||||
omniauth-cas3 (~> 1.1.4)
|
||||
|
@ -1118,14 +1117,13 @@ DEPENDENCIES
|
|||
omniauth-google-oauth2 (~> 0.5.2)
|
||||
omniauth-kerberos (~> 0.3.0)
|
||||
omniauth-oauth2-generic (~> 0.2.2)
|
||||
omniauth-saml (~> 1.10.0)
|
||||
omniauth-saml (~> 1.10)
|
||||
omniauth-shibboleth (~> 1.2.0)
|
||||
omniauth-twitter (~> 1.2.0)
|
||||
omniauth-twitter (~> 1.4)
|
||||
omniauth_crowd (~> 2.2.0)
|
||||
org-ruby (~> 0.9.12)
|
||||
peek (~> 1.0.1)
|
||||
peek-gc (~> 0.0.2)
|
||||
peek-host (~> 1.0.0)
|
||||
peek-mysql2 (~> 1.1.0)
|
||||
peek-performance_bar (~> 1.3.0)
|
||||
peek-pg (~> 1.3.0)
|
||||
|
@ -1196,7 +1194,7 @@ DEPENDENCIES
|
|||
sprockets (~> 3.7.0)
|
||||
sshkey (~> 1.9.0)
|
||||
stackprof (~> 0.2.10)
|
||||
state_machines-activerecord (~> 0.4.0)
|
||||
state_machines-activerecord (~> 0.5.1)
|
||||
sys-filesystem (~> 1.1.6)
|
||||
test-prof (~> 0.2.5)
|
||||
test_after_commit (~> 1.1)
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# BUNDLE_GEMFILE=Gemfile.rails5 bundle install
|
||||
|
||||
ENV["RAILS5"] = "true"
|
||||
|
||||
gemfile = File.expand_path("../Gemfile", __FILE__)
|
||||
|
||||
eval(File.read(gemfile), nil, gemfile)
|
File diff suppressed because it is too large
Load Diff
1
Procfile
1
Procfile
|
@ -4,4 +4,3 @@
|
|||
#
|
||||
web: RAILS_ENV=development bin/web start_foreground
|
||||
worker: RAILS_ENV=development bin/background_jobs start_foreground
|
||||
# mail_room: bundle exec mail_room -q -c config/mail_room.yml
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import './autosize';
|
||||
import './bind_in_out';
|
||||
import initCopyAsGFM from './copy_as_gfm';
|
||||
import './markdown/render_gfm';
|
||||
import initCopyAsGFM from './markdown/copy_as_gfm';
|
||||
import initCopyToClipboard from './copy_to_clipboard';
|
||||
import './details_behavior';
|
||||
import installGlEmojiElement from './gl_emoji';
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
import $ from 'jquery';
|
||||
import _ from 'underscore';
|
||||
import { insertText, getSelectedFragment, nodeMatchesSelector } from '../lib/utils/common_utils';
|
||||
import { placeholderImage } from '../lazy_loader';
|
||||
import { insertText, getSelectedFragment, nodeMatchesSelector } from '~/lib/utils/common_utils';
|
||||
import { placeholderImage } from '~/lazy_loader';
|
||||
|
||||
const gfmRules = {
|
||||
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
|
|
@ -1,7 +1,7 @@
|
|||
import $ from 'jquery';
|
||||
import syntaxHighlight from '~/syntax_highlight';
|
||||
import renderMath from './render_math';
|
||||
import renderMermaid from './render_mermaid';
|
||||
import syntaxHighlight from './syntax_highlight';
|
||||
|
||||
// Render Gitlab flavoured Markdown
|
||||
//
|
|
@ -1,6 +1,6 @@
|
|||
import $ from 'jquery';
|
||||
import { __ } from './locale';
|
||||
import flash from './flash';
|
||||
import { __ } from '~/locale';
|
||||
import flash from '~/flash';
|
||||
|
||||
// Renders math using KaTeX in any element with the
|
||||
// `js-render-math` class
|
|
@ -1,3 +1,5 @@
|
|||
import flash from '~/flash';
|
||||
|
||||
// Renders diagrams and flowcharts from text using Mermaid in any element with the
|
||||
// `js-render-mermaid` class.
|
||||
//
|
||||
|
@ -12,8 +14,6 @@
|
|||
// </pre>
|
||||
//
|
||||
|
||||
import Flash from './flash';
|
||||
|
||||
export default function renderMermaid($els) {
|
||||
if (!$els.length) return;
|
||||
|
||||
|
@ -52,6 +52,6 @@ export default function renderMermaid($els) {
|
|||
});
|
||||
});
|
||||
}).catch((err) => {
|
||||
Flash(`Can't load mermaid module: ${err}`);
|
||||
flash(`Can't load mermaid module: ${err}`);
|
||||
});
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import eventHub from '../eventhub';
|
||||
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
@ -45,7 +45,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
|
|||
};
|
||||
},
|
||||
components: {
|
||||
userAvatarLink,
|
||||
UserAvatarLink,
|
||||
},
|
||||
computed: {
|
||||
numberOverLimit() {
|
||||
|
|
|
@ -33,7 +33,7 @@ export default class VariableList {
|
|||
selector: '.js-ci-variable-input-key',
|
||||
default: '',
|
||||
},
|
||||
value: {
|
||||
secret_value: {
|
||||
selector: '.js-ci-variable-input-value',
|
||||
default: '',
|
||||
},
|
||||
|
@ -105,7 +105,7 @@ export default class VariableList {
|
|||
setupToggleButtons($row[0]);
|
||||
|
||||
// Reset the resizable textarea
|
||||
$row.find(this.inputMap.value.selector).css('height', '');
|
||||
$row.find(this.inputMap.secret_value.selector).css('height', '');
|
||||
|
||||
const $environmentSelect = $row.find('.js-variable-environment-toggle');
|
||||
if ($environmentSelect.length) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// ECMAScript polyfills
|
||||
import 'core-js/fn/array/fill';
|
||||
import 'core-js/fn/array/find';
|
||||
import 'core-js/fn/array/find-index';
|
||||
import 'core-js/fn/array/from';
|
||||
|
|
|
@ -1,33 +1,32 @@
|
|||
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, max-len */
|
||||
|
||||
import $ from 'jquery';
|
||||
import { rstrip } from './lib/utils/common_utils';
|
||||
|
||||
window.ConfirmDangerModal = (function() {
|
||||
function ConfirmDangerModal(form, text) {
|
||||
var project_path, submit;
|
||||
this.form = form;
|
||||
$('.js-confirm-text').text(text || '');
|
||||
$('.js-confirm-danger-input').val('');
|
||||
$('#modal-confirm-danger').modal('show');
|
||||
project_path = $('.js-confirm-danger-match').text();
|
||||
submit = $('.js-confirm-danger-submit');
|
||||
submit.disable();
|
||||
$('.js-confirm-danger-input').off('input');
|
||||
$('.js-confirm-danger-input').on('input', function() {
|
||||
if (rstrip($(this).val()) === project_path) {
|
||||
return submit.enable();
|
||||
} else {
|
||||
return submit.disable();
|
||||
}
|
||||
});
|
||||
$('.js-confirm-danger-submit').off('click');
|
||||
$('.js-confirm-danger-submit').on('click', (function(_this) {
|
||||
return function() {
|
||||
return _this.form.submit();
|
||||
};
|
||||
})(this));
|
||||
}
|
||||
function openConfirmDangerModal($form, text) {
|
||||
$('.js-confirm-text').text(text || '');
|
||||
$('.js-confirm-danger-input').val('');
|
||||
$('#modal-confirm-danger').modal('show');
|
||||
|
||||
return ConfirmDangerModal;
|
||||
})();
|
||||
const confirmTextMatch = $('.js-confirm-danger-match').text();
|
||||
const $submit = $('.js-confirm-danger-submit');
|
||||
$submit.disable();
|
||||
|
||||
$('.js-confirm-danger-input').off('input').on('input', function handleInput() {
|
||||
const confirmText = rstrip($(this).val());
|
||||
if (confirmText === confirmTextMatch) {
|
||||
$submit.enable();
|
||||
} else {
|
||||
$submit.disable();
|
||||
}
|
||||
});
|
||||
$('.js-confirm-danger-submit').off('click').on('click', () => $form.submit());
|
||||
}
|
||||
|
||||
export default function initConfirmDangerModal() {
|
||||
$(document).on('click', '.js-confirm-danger', (e) => {
|
||||
e.preventDefault();
|
||||
const $btn = $(e.target);
|
||||
const $form = $btn.closest('form');
|
||||
const text = $btn.data('confirmDangerMessage');
|
||||
openConfirmDangerModal($form, text);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -53,8 +53,12 @@ function initPageShortcuts(page) {
|
|||
|
||||
function initGFMInput() {
|
||||
$('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
|
||||
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
|
||||
const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete);
|
||||
const gfm = new GfmAutoComplete(
|
||||
gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources,
|
||||
);
|
||||
const enableGFM = convertPermissionToBoolean(
|
||||
el.dataset.supportsAutocomplete,
|
||||
);
|
||||
gfm.setup($(el), {
|
||||
emojis: true,
|
||||
members: enableGFM,
|
||||
|
@ -67,9 +71,9 @@ function initGFMInput() {
|
|||
}
|
||||
|
||||
function initPerformanceBar() {
|
||||
if (document.querySelector('#peek')) {
|
||||
if (document.querySelector('#js-peek')) {
|
||||
import('./performance_bar')
|
||||
.then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap
|
||||
.then(m => new m.default({ container: '#js-peek' })) // eslint-disable-line new-cap
|
||||
.catch(() => Flash('Error loading performance bar module'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import $ from 'jquery';
|
|||
import autosize from 'autosize';
|
||||
import GfmAutoComplete from './gfm_auto_complete';
|
||||
import dropzoneInput from './dropzone_input';
|
||||
import textUtils from './lib/utils/text_markdown';
|
||||
import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown';
|
||||
|
||||
export default class GLForm {
|
||||
constructor(form, enableGFM = false) {
|
||||
|
@ -47,7 +47,7 @@ export default class GLForm {
|
|||
}
|
||||
// form and textarea event listeners
|
||||
this.addEventListeners();
|
||||
textUtils.init(this.form);
|
||||
addMarkdownListeners(this.form);
|
||||
// hide discard button
|
||||
this.form.find('.js-note-discard').hide();
|
||||
this.form.show();
|
||||
|
@ -86,7 +86,7 @@ export default class GLForm {
|
|||
clearEventListeners() {
|
||||
this.textarea.off('focus');
|
||||
this.textarea.off('blur');
|
||||
textUtils.removeListeners(this.form);
|
||||
removeMarkdownListeners(this.form);
|
||||
}
|
||||
|
||||
addEventListeners() {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import $ from 'jquery';
|
||||
import { s__ } from '~/locale';
|
||||
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||
import modal from '~/vue_shared/components/modal.vue';
|
||||
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
|
||||
import { getParameterByName } from '~/lib/utils/common_utils';
|
||||
import { mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
|
||||
|
@ -15,7 +15,7 @@ import groupsComponent from './groups.vue';
|
|||
export default {
|
||||
components: {
|
||||
loadingIcon,
|
||||
modal,
|
||||
DeprecatedModal,
|
||||
groupsComponent,
|
||||
},
|
||||
props: {
|
||||
|
@ -52,8 +52,9 @@ export default {
|
|||
},
|
||||
},
|
||||
created() {
|
||||
this.searchEmptyMessage = this.hideProjects ?
|
||||
COMMON_STR.GROUP_SEARCH_EMPTY : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
|
||||
this.searchEmptyMessage = this.hideProjects
|
||||
? COMMON_STR.GROUP_SEARCH_EMPTY
|
||||
: COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
|
||||
|
||||
eventHub.$on('fetchPage', this.fetchPage);
|
||||
eventHub.$on('toggleChildren', this.toggleChildren);
|
||||
|
@ -72,22 +73,30 @@ export default {
|
|||
eventHub.$off('updateGroups', this.updateGroups);
|
||||
},
|
||||
methods: {
|
||||
fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) {
|
||||
return this.service.getGroups(parentId, page, filterGroupsBy, sortBy, archived)
|
||||
.then((res) => {
|
||||
if (updatePagination) {
|
||||
this.updatePagination(res.headers);
|
||||
}
|
||||
fetchGroups({
|
||||
parentId,
|
||||
page,
|
||||
filterGroupsBy,
|
||||
sortBy,
|
||||
archived,
|
||||
updatePagination,
|
||||
}) {
|
||||
return this.service
|
||||
.getGroups(parentId, page, filterGroupsBy, sortBy, archived)
|
||||
.then(res => {
|
||||
if (updatePagination) {
|
||||
this.updatePagination(res.headers);
|
||||
}
|
||||
|
||||
return res;
|
||||
})
|
||||
.then(res => res.json())
|
||||
.catch(() => {
|
||||
this.isLoading = false;
|
||||
$.scrollTo(0);
|
||||
return res;
|
||||
})
|
||||
.then(res => res.json())
|
||||
.catch(() => {
|
||||
this.isLoading = false;
|
||||
$.scrollTo(0);
|
||||
|
||||
Flash(COMMON_STR.FAILURE);
|
||||
});
|
||||
Flash(COMMON_STR.FAILURE);
|
||||
});
|
||||
},
|
||||
fetchAllGroups() {
|
||||
const page = getParameterByName('page') || null;
|
||||
|
@ -103,7 +112,7 @@ export default {
|
|||
sortBy,
|
||||
archived,
|
||||
updatePagination: true,
|
||||
}).then((res) => {
|
||||
}).then(res => {
|
||||
this.isLoading = false;
|
||||
this.updateGroups(res, Boolean(filterGroupsBy));
|
||||
});
|
||||
|
@ -118,14 +127,18 @@ export default {
|
|||
sortBy,
|
||||
archived,
|
||||
updatePagination: true,
|
||||
}).then((res) => {
|
||||
}).then(res => {
|
||||
this.isLoading = false;
|
||||
$.scrollTo(0);
|
||||
|
||||
const currentPath = mergeUrlParams({ page }, window.location.href);
|
||||
window.history.replaceState({
|
||||
page: currentPath,
|
||||
}, document.title, currentPath);
|
||||
window.history.replaceState(
|
||||
{
|
||||
page: currentPath,
|
||||
},
|
||||
document.title,
|
||||
currentPath,
|
||||
);
|
||||
|
||||
this.updateGroups(res);
|
||||
});
|
||||
|
@ -138,11 +151,13 @@ export default {
|
|||
// eslint-disable-next-line promise/catch-or-return
|
||||
this.fetchGroups({
|
||||
parentId: parentGroup.id,
|
||||
}).then((res) => {
|
||||
this.store.setGroupChildren(parentGroup, res);
|
||||
}).catch(() => {
|
||||
parentGroup.isChildrenLoading = false;
|
||||
});
|
||||
})
|
||||
.then(res => {
|
||||
this.store.setGroupChildren(parentGroup, res);
|
||||
})
|
||||
.catch(() => {
|
||||
parentGroup.isChildrenLoading = false;
|
||||
});
|
||||
} else {
|
||||
parentGroup.isOpen = true;
|
||||
}
|
||||
|
@ -154,7 +169,11 @@ export default {
|
|||
this.targetGroup = group;
|
||||
this.targetParentGroup = parentGroup;
|
||||
this.showModal = true;
|
||||
this.groupLeaveConfirmationMessage = s__(`GroupsTree|Are you sure you want to leave the "${group.fullName}" group?`);
|
||||
this.groupLeaveConfirmationMessage = s__(
|
||||
`GroupsTree|Are you sure you want to leave the "${
|
||||
group.fullName
|
||||
}" group?`,
|
||||
);
|
||||
},
|
||||
hideLeaveGroupModal() {
|
||||
this.showModal = false;
|
||||
|
@ -162,14 +181,15 @@ export default {
|
|||
leaveGroup() {
|
||||
this.showModal = false;
|
||||
this.targetGroup.isBeingRemoved = true;
|
||||
this.service.leaveGroup(this.targetGroup.leavePath)
|
||||
this.service
|
||||
.leaveGroup(this.targetGroup.leavePath)
|
||||
.then(res => res.json())
|
||||
.then((res) => {
|
||||
.then(res => {
|
||||
$.scrollTo(0);
|
||||
this.store.removeGroup(this.targetGroup, this.targetParentGroup);
|
||||
Flash(res.notice, 'notice');
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(err => {
|
||||
let message = COMMON_STR.FAILURE;
|
||||
if (err.status === 403) {
|
||||
message = COMMON_STR.LEAVE_FORBIDDEN;
|
||||
|
@ -208,8 +228,8 @@ export default {
|
|||
:search-empty-message="searchEmptyMessage"
|
||||
:page-info="pageInfo"
|
||||
/>
|
||||
<modal
|
||||
v-if="showModal"
|
||||
<deprecated-modal
|
||||
v-show="showModal"
|
||||
kind="warning"
|
||||
:primary-button-label="__('Leave')"
|
||||
:title="__('Are you sure?')"
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<script>
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
changedIcon() {
|
||||
return this.file.tempFile ? 'file-addition' : 'file-modified';
|
||||
},
|
||||
changedIconClass() {
|
||||
return `multi-${this.changedIcon}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<icon
|
||||
:name="changedIcon"
|
||||
:size="12"
|
||||
:css-classes="`ide-file-changed-icon ${changedIconClass}`"
|
||||
/>
|
||||
</template>
|
|
@ -0,0 +1,65 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
import * as consts from '../../stores/modules/commit/constants';
|
||||
import RadioGroup from './radio_group.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RadioGroup,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'currentBranchId',
|
||||
]),
|
||||
newMergeRequestHelpText() {
|
||||
return sprintf(
|
||||
__('Creates a new branch from %{branchName} and re-directs to create a new merge request'),
|
||||
{ branchName: this.currentBranchId },
|
||||
);
|
||||
},
|
||||
commitToCurrentBranchText() {
|
||||
return sprintf(
|
||||
__('Commit to %{branchName} branch'),
|
||||
{ branchName: `<strong>${this.currentBranchId}</strong>` },
|
||||
false,
|
||||
);
|
||||
},
|
||||
commitToNewBranchText() {
|
||||
return sprintf(
|
||||
__('Creates a new branch from %{branchName}'),
|
||||
{ branchName: this.currentBranchId },
|
||||
);
|
||||
},
|
||||
},
|
||||
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
|
||||
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
|
||||
commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="append-bottom-15 ide-commit-radios">
|
||||
<radio-group
|
||||
:value="$options.commitToCurrentBranch"
|
||||
:checked="true"
|
||||
>
|
||||
<span
|
||||
v-html="commitToCurrentBranchText"
|
||||
>
|
||||
</span>
|
||||
</radio-group>
|
||||
<radio-group
|
||||
:value="$options.commitToNewBranch"
|
||||
:label="__('Create a new branch')"
|
||||
:show-input="true"
|
||||
:help-text="commitToNewBranchText"
|
||||
/>
|
||||
<radio-group
|
||||
:value="$options.commitToNewBranchMR"
|
||||
:label="__('Create a new branch and merge request')"
|
||||
:show-input="true"
|
||||
:help-text="newMergeRequestHelpText"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,66 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import listItem from './list_item.vue';
|
||||
import listCollapsed from './list_collapsed.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
listItem,
|
||||
listCollapsed,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fileList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'currentProjectId',
|
||||
'currentBranchId',
|
||||
'rightPanelCollapsed',
|
||||
]),
|
||||
isCommitInfoShown() {
|
||||
return this.rightPanelCollapsed || this.fileList.length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleCollapsed() {
|
||||
this.$emit('toggleCollapsed');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'multi-file-commit-list': isCommitInfoShown
|
||||
}"
|
||||
>
|
||||
<list-collapsed
|
||||
v-if="rightPanelCollapsed"
|
||||
/>
|
||||
<template v-else>
|
||||
<ul
|
||||
v-if="fileList.length"
|
||||
class="list-unstyled append-bottom-0"
|
||||
>
|
||||
<li
|
||||
v-for="file in fileList"
|
||||
:key="file.key"
|
||||
>
|
||||
<list-item
|
||||
:file="file"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,35 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'addedFiles',
|
||||
'modifiedFiles',
|
||||
]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="multi-file-commit-list-collapsed text-center"
|
||||
>
|
||||
<icon
|
||||
name="file-addition"
|
||||
:size="18"
|
||||
css-classes="multi-file-addition append-bottom-10"
|
||||
/>
|
||||
{{ addedFiles.length }}
|
||||
<icon
|
||||
name="file-modified"
|
||||
:size="18"
|
||||
css-classes="multi-file-modified prepend-top-10 append-bottom-10"
|
||||
/>
|
||||
{{ modifiedFiles.length }}
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,60 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import router from '../../ide_router';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconName() {
|
||||
return this.file.tempFile ? 'file-addition' : 'file-modified';
|
||||
},
|
||||
iconClass() {
|
||||
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'discardFileChanges',
|
||||
'updateViewer',
|
||||
]),
|
||||
openFileInEditor(file) {
|
||||
this.updateViewer('diff');
|
||||
|
||||
router.push(`/project${file.url}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multi-file-commit-list-item">
|
||||
<button
|
||||
type="button"
|
||||
class="multi-file-commit-list-path"
|
||||
@click="openFileInEditor(file)">
|
||||
<span class="multi-file-commit-list-file-path">
|
||||
<icon
|
||||
:name="iconName"
|
||||
:size="16"
|
||||
:css-classes="iconClass"
|
||||
/>{{ file.path }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-blank multi-file-discard-btn"
|
||||
@click="discardFileChanges(file.path)"
|
||||
>
|
||||
Discard
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,94 @@
|
|||
<script>
|
||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
checked: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
showInput: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
helpText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState('commit', [
|
||||
'commitAction',
|
||||
]),
|
||||
...mapGetters('commit', [
|
||||
'newBranchName',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions('commit', [
|
||||
'updateCommitAction',
|
||||
'updateBranchName',
|
||||
]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<fieldset>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="commit-action"
|
||||
:value="value"
|
||||
@change="updateCommitAction($event.target.value)"
|
||||
:checked="checked"
|
||||
v-once
|
||||
/>
|
||||
<span class="prepend-left-10">
|
||||
<template v-if="label">
|
||||
{{ label }}
|
||||
</template>
|
||||
<slot v-else></slot>
|
||||
<span
|
||||
v-if="helpText"
|
||||
v-tooltip
|
||||
class="help-block inline"
|
||||
:title="helpText"
|
||||
>
|
||||
<i
|
||||
class="fa fa-question-circle"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</i>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
v-if="commitAction === value && showInput"
|
||||
class="ide-commit-new-branch"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
:placeholder="newBranchName"
|
||||
@input="updateBranchName($event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</template>
|
|
@ -0,0 +1,91 @@
|
|||
<script>
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
hasChanges: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
viewer: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
showShadow: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changeMode(mode) {
|
||||
this.$emit('click', mode);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="dropdown"
|
||||
:class="{
|
||||
shadow: showShadow,
|
||||
}"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
:class="{
|
||||
'btn-inverted': hasChanges,
|
||||
}"
|
||||
data-toggle="dropdown"
|
||||
>
|
||||
<template v-if="viewer === 'editor'">
|
||||
{{ __('Editing') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ __('Reviewing') }}
|
||||
</template>
|
||||
<icon
|
||||
name="angle-down"
|
||||
:size="12"
|
||||
css-classes="caret-down"
|
||||
/>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="changeMode('editor')"
|
||||
:class="{
|
||||
'is-active': viewer === 'editor',
|
||||
}"
|
||||
>
|
||||
<strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong>
|
||||
<span class="dropdown-menu-inner-content">
|
||||
{{ __('View and edit lines') }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="changeMode('diff')"
|
||||
:class="{
|
||||
'is-active': viewer === 'diff',
|
||||
}"
|
||||
>
|
||||
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
|
||||
<span class="dropdown-menu-inner-content">
|
||||
{{ __('Compare changes with the last commit') }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,111 @@
|
|||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import ideSidebar from './ide_side_bar.vue';
|
||||
import ideContextbar from './ide_context_bar.vue';
|
||||
import repoTabs from './repo_tabs.vue';
|
||||
import repoFileButtons from './repo_file_buttons.vue';
|
||||
import ideStatusBar from './ide_status_bar.vue';
|
||||
import repoEditor from './repo_editor.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ideSidebar,
|
||||
ideContextbar,
|
||||
repoTabs,
|
||||
repoFileButtons,
|
||||
ideStatusBar,
|
||||
repoEditor,
|
||||
},
|
||||
props: {
|
||||
emptyStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
noChangesStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
committedStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['changedFiles', 'openFiles', 'viewer']),
|
||||
...mapGetters(['activeFile', 'hasChanges']),
|
||||
},
|
||||
mounted() {
|
||||
const returnValue = 'Are you sure you want to lose unsaved changes?';
|
||||
window.onbeforeunload = e => {
|
||||
if (!this.changedFiles.length) return undefined;
|
||||
|
||||
Object.assign(e, {
|
||||
returnValue,
|
||||
});
|
||||
return returnValue;
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ide-view"
|
||||
>
|
||||
<ide-sidebar />
|
||||
<div
|
||||
class="multi-file-edit-pane"
|
||||
>
|
||||
<template
|
||||
v-if="activeFile"
|
||||
>
|
||||
<repo-tabs
|
||||
:files="openFiles"
|
||||
:viewer="viewer"
|
||||
:has-changes="hasChanges"
|
||||
/>
|
||||
<repo-editor
|
||||
class="multi-file-edit-pane-content"
|
||||
:file="activeFile"
|
||||
/>
|
||||
<repo-file-buttons
|
||||
:file="activeFile"
|
||||
/>
|
||||
<ide-status-bar
|
||||
:file="activeFile"
|
||||
/>
|
||||
</template>
|
||||
<template
|
||||
v-else
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="ide-empty-state"
|
||||
>
|
||||
<div class="row js-empty-state">
|
||||
<div class="col-xs-12">
|
||||
<div class="svg-content svg-250">
|
||||
<img :src="emptyStateSvgPath" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
<div class="text-content text-center">
|
||||
<h4>
|
||||
Welcome to the GitLab IDE
|
||||
</h4>
|
||||
<p>
|
||||
You can select a file in the left sidebar to begin
|
||||
editing and use the right sidebar to commit your changes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<ide-contextbar
|
||||
:no-changes-state-svg-path="noChangesStateSvgPath"
|
||||
:committed-state-svg-path="committedStateSvgPath"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,84 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import repoCommitSection from './repo_commit_section.vue';
|
||||
import ResizablePanel from './resizable_panel.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
repoCommitSection,
|
||||
icon,
|
||||
panelResizer,
|
||||
ResizablePanel,
|
||||
},
|
||||
props: {
|
||||
noChangesStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
committedStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['changedFiles', 'rightPanelCollapsed']),
|
||||
...mapGetters(['currentIcon']),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setPanelCollapsedStatus']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<resizable-panel
|
||||
:collapsible="true"
|
||||
:initial-width="340"
|
||||
side="right"
|
||||
>
|
||||
<div
|
||||
class="multi-file-commit-panel-section"
|
||||
>
|
||||
<header
|
||||
class="multi-file-commit-panel-header"
|
||||
:class="{
|
||||
'is-collapsed': rightPanelCollapsed,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="multi-file-commit-panel-header-title"
|
||||
v-if="!rightPanelCollapsed"
|
||||
>
|
||||
<div
|
||||
v-if="changedFiles.length"
|
||||
>
|
||||
<icon
|
||||
name="list-bulleted"
|
||||
:size="18"
|
||||
/>
|
||||
Staged
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
|
||||
@click.stop="setPanelCollapsedStatus({
|
||||
side: 'right',
|
||||
collapsed: !rightPanelCollapsed,
|
||||
})"
|
||||
>
|
||||
<icon
|
||||
:name="currentIcon"
|
||||
:size="18"
|
||||
/>
|
||||
</button>
|
||||
</header>
|
||||
<repo-commit-section
|
||||
:no-changes-state-svg-path="noChangesStateSvgPath"
|
||||
:committed-state-svg-path="committedStateSvgPath"
|
||||
/>
|
||||
</div>
|
||||
</resizable-panel>
|
||||
</template>
|
|
@ -0,0 +1,43 @@
|
|||
<script>
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
props: {
|
||||
projectUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
goBackUrl() {
|
||||
return document.referrer || this.projectUrl;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav
|
||||
class="ide-external-links"
|
||||
v-once
|
||||
>
|
||||
<p>
|
||||
<a
|
||||
:href="goBackUrl"
|
||||
class="ide-sidebar-link"
|
||||
>
|
||||
<icon
|
||||
:size="16"
|
||||
class="append-right-8"
|
||||
name="go-back"
|
||||
/>
|
||||
<span class="ide-external-links-text">
|
||||
{{ s__('Go back') }}
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
</nav>
|
||||
</template>
|
|
@ -0,0 +1,47 @@
|
|||
<script>
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import repoTree from './ide_repo_tree.vue';
|
||||
import newDropdown from './new_dropdown/index.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
repoTree,
|
||||
icon,
|
||||
newDropdown,
|
||||
},
|
||||
props: {
|
||||
projectId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
branch: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="branch-container">
|
||||
<div class="branch-header">
|
||||
<div class="branch-header-title str-truncated ref-name">
|
||||
<icon
|
||||
name="branch"
|
||||
:size="12"
|
||||
/>
|
||||
{{ branch.name }}
|
||||
</div>
|
||||
<div class="branch-header-btns">
|
||||
<new-dropdown
|
||||
:project-id="projectId"
|
||||
:branch="branch.name"
|
||||
path=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<repo-tree
|
||||
:tree="branch.tree"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,65 @@
|
|||
<script>
|
||||
import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
|
||||
import Identicon from '../../vue_shared/components/identicon.vue';
|
||||
import BranchesTree from './ide_project_branches_tree.vue';
|
||||
import ExternalLinks from './ide_external_links.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BranchesTree,
|
||||
ExternalLinks,
|
||||
ProjectAvatarImage,
|
||||
Identicon,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="projects-sidebar">
|
||||
<div class="context-header">
|
||||
<a
|
||||
:title="project.name"
|
||||
:href="project.web_url"
|
||||
>
|
||||
<div
|
||||
v-if="project.avatar_url"
|
||||
class="avatar-container s40 project-avatar"
|
||||
>
|
||||
<project-avatar-image
|
||||
class="avatar-container project-avatar"
|
||||
:link-href="project.path"
|
||||
:img-src="project.avatar_url"
|
||||
:img-alt="project.name"
|
||||
:img-size="40"
|
||||
/>
|
||||
</div>
|
||||
<identicon
|
||||
v-else
|
||||
size-class="s40"
|
||||
:entity-id="project.id"
|
||||
:entity-name="project.name"
|
||||
/>
|
||||
<div class="sidebar-context-title">
|
||||
{{ project.name }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<external-links
|
||||
:project-url="project.web_url"
|
||||
/>
|
||||
<div class="multi-file-commit-panel-inner-scroll">
|
||||
<branches-tree
|
||||
v-for="branch in project.branches"
|
||||
:key="branch.name"
|
||||
:project-id="project.path_with_namespace"
|
||||
:branch="branch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,41 @@
|
|||
<script>
|
||||
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import RepoFile from './repo_file.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RepoFile,
|
||||
SkeletonLoadingContainer,
|
||||
},
|
||||
props: {
|
||||
tree: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ide-file-list"
|
||||
>
|
||||
<template v-if="tree.loading">
|
||||
<div
|
||||
class="multi-file-loading-container"
|
||||
v-for="n in 3"
|
||||
:key="n"
|
||||
>
|
||||
<skeleton-loading-container />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<repo-file
|
||||
v-for="file in tree.tree"
|
||||
:key="file.key"
|
||||
:file="file"
|
||||
:level="0"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,51 @@
|
|||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import projectTree from './ide_project_tree.vue';
|
||||
import ResizablePanel from './resizable_panel.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
projectTree,
|
||||
icon,
|
||||
panelResizer,
|
||||
skeletonLoadingContainer,
|
||||
ResizablePanel,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'loading',
|
||||
]),
|
||||
...mapGetters([
|
||||
'projectsWithTrees',
|
||||
]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<resizable-panel
|
||||
:collapsible="false"
|
||||
:initial-width="290"
|
||||
side="left"
|
||||
>
|
||||
<div class="multi-file-commit-panel-inner">
|
||||
<template v-if="loading">
|
||||
<div
|
||||
class="multi-file-loading-container"
|
||||
v-for="n in 3"
|
||||
:key="n"
|
||||
>
|
||||
<skeleton-loading-container />
|
||||
</div>
|
||||
</template>
|
||||
<project-tree
|
||||
v-for="project in projectsWithTrees"
|
||||
:key="project.id"
|
||||
:project="project"
|
||||
/>
|
||||
</div>
|
||||
</resizable-panel>
|
||||
</template>
|
|
@ -0,0 +1,60 @@
|
|||
<script>
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import timeAgoMixin from '~/vue_shared/mixins/timeago';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
mixins: [
|
||||
timeAgoMixin,
|
||||
],
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-status-bar">
|
||||
<div class="ref-name">
|
||||
<icon
|
||||
name="branch"
|
||||
:size="12"
|
||||
/>
|
||||
{{ file.branchId }}
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="file.lastCommit && file.lastCommit.id">
|
||||
Last commit:
|
||||
<a
|
||||
v-tooltip
|
||||
:title="file.lastCommit.message"
|
||||
:href="file.lastCommit.url"
|
||||
>
|
||||
{{ timeFormated(file.lastCommit.updatedAt) }} by
|
||||
{{ file.lastCommit.author }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{{ file.name }}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{{ file.eol }}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{{ file.editorRow }}:{{ file.editorColumn }}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{{ file.fileLanguage }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,111 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import newModal from './modal.vue';
|
||||
import upload from './upload.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
newModal,
|
||||
upload,
|
||||
},
|
||||
props: {
|
||||
branch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
openModal: false,
|
||||
modalType: '',
|
||||
dropdownOpen: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'createTempEntry',
|
||||
]),
|
||||
createNewItem(type) {
|
||||
this.modalType = type;
|
||||
this.openModal = true;
|
||||
this.dropdownOpen = false;
|
||||
},
|
||||
hideModal() {
|
||||
this.openModal = false;
|
||||
},
|
||||
openDropdown() {
|
||||
this.dropdownOpen = !this.dropdownOpen;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-new-btn">
|
||||
<div
|
||||
class="dropdown"
|
||||
:class="{
|
||||
open: dropdownOpen,
|
||||
}"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-default dropdown-toggle add-to-tree"
|
||||
aria-label="Create new file or directory"
|
||||
@click.stop="openDropdown()"
|
||||
>
|
||||
<icon
|
||||
name="plus"
|
||||
:size="12"
|
||||
css-classes="pull-left"
|
||||
/>
|
||||
<icon
|
||||
name="arrow-down"
|
||||
:size="12"
|
||||
css-classes="pull-left"
|
||||
/>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.stop.prevent="createNewItem('blob')"
|
||||
>
|
||||
{{ __('New file') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<upload
|
||||
:branch-id="branch"
|
||||
:path="path"
|
||||
@create="createTempEntry"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.stop.prevent="createNewItem('tree')"
|
||||
>
|
||||
{{ __('New directory') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<new-modal
|
||||
v-if="openModal"
|
||||
:type="modalType"
|
||||
:branch-id="branch"
|
||||
:path="path"
|
||||
@hide="hideModal"
|
||||
@create="createTempEntry"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,99 @@
|
|||
<script>
|
||||
import { __ } from '~/locale';
|
||||
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DeprecatedModal,
|
||||
},
|
||||
props: {
|
||||
branchId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
entryName: this.path !== '' ? `${this.path}/` : '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
modalTitle() {
|
||||
if (this.type === 'tree') {
|
||||
return __('Create new directory');
|
||||
}
|
||||
|
||||
return __('Create new file');
|
||||
},
|
||||
buttonLabel() {
|
||||
if (this.type === 'tree') {
|
||||
return __('Create directory');
|
||||
}
|
||||
|
||||
return __('Create file');
|
||||
},
|
||||
formLabelName() {
|
||||
if (this.type === 'tree') {
|
||||
return __('Directory name');
|
||||
}
|
||||
|
||||
return __('File name');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.fieldName.focus();
|
||||
},
|
||||
methods: {
|
||||
createEntryInStore() {
|
||||
this.$emit('create', {
|
||||
branchId: this.branchId,
|
||||
name: this.entryName,
|
||||
type: this.type,
|
||||
});
|
||||
|
||||
this.hideModal();
|
||||
},
|
||||
hideModal() {
|
||||
this.$emit('hide');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<deprecated-modal
|
||||
:title="modalTitle"
|
||||
:primary-button-label="buttonLabel"
|
||||
kind="success"
|
||||
@cancel="hideModal"
|
||||
@submit="createEntryInStore"
|
||||
>
|
||||
<form
|
||||
class="form-horizontal"
|
||||
slot="body"
|
||||
@submit.prevent="createEntryInStore"
|
||||
>
|
||||
<fieldset class="form-group append-bottom-0">
|
||||
<label class="label-light col-sm-3">
|
||||
{{ formLabelName }}
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
v-model="entryName"
|
||||
ref="fieldName"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</deprecated-modal>
|
||||
</template>
|
|
@ -0,0 +1,75 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
branchId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.fileUpload.addEventListener('change', this.openFile);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$refs.fileUpload.removeEventListener('change', this.openFile);
|
||||
},
|
||||
methods: {
|
||||
createFile(target, file, isText) {
|
||||
const { name } = file;
|
||||
let { result } = target;
|
||||
|
||||
if (!isText) {
|
||||
result = result.split('base64,')[1];
|
||||
}
|
||||
|
||||
this.$emit('create', {
|
||||
name: `${(this.path ? `${this.path}/` : '')}${name}`,
|
||||
branchId: this.branchId,
|
||||
type: 'blob',
|
||||
content: result,
|
||||
base64: !isText,
|
||||
});
|
||||
},
|
||||
readFile(file) {
|
||||
const reader = new FileReader();
|
||||
const isText = file.type.match(/text.*/) !== null;
|
||||
|
||||
reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true });
|
||||
|
||||
if (isText) {
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
},
|
||||
openFile() {
|
||||
Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
|
||||
},
|
||||
startFileUpload() {
|
||||
this.$refs.fileUpload.click();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
@click.stop.prevent="startFileUpload"
|
||||
>
|
||||
{{ __('Upload file') }}
|
||||
</a>
|
||||
<input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
class="hidden"
|
||||
ref="fileUpload"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,172 @@
|
|||
<script>
|
||||
import { mapState, mapActions, mapGetters } from 'vuex';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
|
||||
import LoadingButton from '~/vue_shared/components/loading_button.vue';
|
||||
import commitFilesList from './commit_sidebar/list.vue';
|
||||
import * as consts from '../stores/modules/commit/constants';
|
||||
import Actions from './commit_sidebar/actions.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DeprecatedModal,
|
||||
icon,
|
||||
commitFilesList,
|
||||
Actions,
|
||||
LoadingButton,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
noChangesStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
committedStateSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'currentProjectId',
|
||||
'currentBranchId',
|
||||
'rightPanelCollapsed',
|
||||
'lastCommitMsg',
|
||||
'changedFiles',
|
||||
]),
|
||||
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
|
||||
...mapGetters('commit', [
|
||||
'commitButtonDisabled',
|
||||
'discardDraftButtonDisabled',
|
||||
'branchName',
|
||||
]),
|
||||
statusSvg() {
|
||||
return this.lastCommitMsg
|
||||
? this.committedStateSvgPath
|
||||
: this.noChangesStateSvgPath;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setPanelCollapsedStatus']),
|
||||
...mapActions('commit', [
|
||||
'updateCommitMessage',
|
||||
'discardDraft',
|
||||
'commitChanges',
|
||||
'updateCommitAction',
|
||||
]),
|
||||
toggleCollapsed() {
|
||||
this.setPanelCollapsedStatus({
|
||||
side: 'right',
|
||||
collapsed: !this.rightPanelCollapsed,
|
||||
});
|
||||
},
|
||||
forceCreateNewBranch() {
|
||||
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() =>
|
||||
this.commitChanges(),
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="multi-file-commit-panel-section"
|
||||
:class="{
|
||||
'multi-file-commit-empty-state-container': !changedFiles.length
|
||||
}"
|
||||
>
|
||||
<deprecated-modal
|
||||
id="ide-create-branch-modal"
|
||||
:primary-button-label="__('Create new branch')"
|
||||
kind="success"
|
||||
:title="__('Branch has changed')"
|
||||
@submit="forceCreateNewBranch"
|
||||
>
|
||||
<template slot="body">
|
||||
{{ __(`This branch has changed since you started editing.
|
||||
Would you like to create a new branch?`) }}
|
||||
</template>
|
||||
</deprecated-modal>
|
||||
<commit-files-list
|
||||
title="Staged"
|
||||
:file-list="changedFiles"
|
||||
:collapsed="rightPanelCollapsed"
|
||||
@toggleCollapsed="toggleCollapsed"
|
||||
/>
|
||||
<template
|
||||
v-if="changedFiles.length"
|
||||
>
|
||||
<form
|
||||
class="form-horizontal multi-file-commit-form"
|
||||
@submit.prevent.stop="commitChanges"
|
||||
v-if="!rightPanelCollapsed"
|
||||
>
|
||||
<div class="multi-file-commit-fieldset">
|
||||
<textarea
|
||||
class="form-control multi-file-commit-message"
|
||||
name="commit-message"
|
||||
:value="commitMessage"
|
||||
:placeholder="__('Write a commit message...')"
|
||||
@input="updateCommitMessage($event.target.value)"
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="clearfix prepend-top-15">
|
||||
<actions />
|
||||
<loading-button
|
||||
:loading="submitCommitLoading"
|
||||
:disabled="commitButtonDisabled"
|
||||
container-class="btn btn-success btn-sm pull-left"
|
||||
:label="__('Commit')"
|
||||
@click="commitChanges"
|
||||
/>
|
||||
<button
|
||||
v-if="!discardDraftButtonDisabled"
|
||||
type="button"
|
||||
class="btn btn-default btn-sm pull-right"
|
||||
@click="discardDraft"
|
||||
>
|
||||
{{ __('Discard draft') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
<div
|
||||
v-else-if="!rightPanelCollapsed"
|
||||
class="row js-empty-state"
|
||||
>
|
||||
<div class="col-xs-10 col-xs-offset-1">
|
||||
<div class="svg-content svg-80">
|
||||
<img :src="statusSvg" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-10 col-xs-offset-1">
|
||||
<div
|
||||
class="text-content text-center"
|
||||
v-if="!lastCommitMsg"
|
||||
>
|
||||
<h4>
|
||||
{{ __('No changes') }}
|
||||
</h4>
|
||||
<p>
|
||||
{{ __('Edit files in the editor and commit changes here') }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="text-content text-center"
|
||||
v-else
|
||||
>
|
||||
<h4>
|
||||
{{ __('All changes are committed') }}
|
||||
</h4>
|
||||
<p v-html="lastCommitMsg">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,161 @@
|
|||
<script>
|
||||
/* global monaco */
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import flash from '~/flash';
|
||||
import monacoLoader from '../monaco_loader';
|
||||
import Editor from '../lib/editor';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'leftPanelCollapsed',
|
||||
'rightPanelCollapsed',
|
||||
'viewer',
|
||||
'delayViewerUpdated',
|
||||
]),
|
||||
shouldHideEditor() {
|
||||
return this.file && this.file.binary && !this.file.raw;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
file(oldVal, newVal) {
|
||||
if (newVal.path !== this.file.path) {
|
||||
this.initMonaco();
|
||||
}
|
||||
},
|
||||
leftPanelCollapsed() {
|
||||
this.editor.updateDimensions();
|
||||
},
|
||||
rightPanelCollapsed() {
|
||||
this.editor.updateDimensions();
|
||||
},
|
||||
viewer() {
|
||||
this.createEditorInstance();
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.editor.dispose();
|
||||
},
|
||||
mounted() {
|
||||
if (this.editor && monaco) {
|
||||
this.initMonaco();
|
||||
} else {
|
||||
monacoLoader(['vs/editor/editor.main'], () => {
|
||||
this.editor = Editor.create(monaco);
|
||||
|
||||
this.initMonaco();
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'getRawFileData',
|
||||
'changeFileContent',
|
||||
'setFileLanguage',
|
||||
'setEditorPosition',
|
||||
'setFileEOL',
|
||||
'updateViewer',
|
||||
'updateDelayViewerUpdated',
|
||||
]),
|
||||
initMonaco() {
|
||||
if (this.shouldHideEditor) return;
|
||||
|
||||
this.editor.clearEditor();
|
||||
|
||||
this.getRawFileData(this.file)
|
||||
.then(() => {
|
||||
const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve();
|
||||
|
||||
return viewerPromise;
|
||||
})
|
||||
.then(() => {
|
||||
this.updateDelayViewerUpdated(false);
|
||||
this.createEditorInstance();
|
||||
})
|
||||
.catch((err) => {
|
||||
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
createEditorInstance() {
|
||||
this.editor.dispose();
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.viewer === 'editor') {
|
||||
this.editor.createInstance(this.$refs.editor);
|
||||
} else {
|
||||
this.editor.createDiffInstance(this.$refs.editor);
|
||||
}
|
||||
|
||||
this.setupEditor();
|
||||
});
|
||||
},
|
||||
setupEditor() {
|
||||
if (!this.file || !this.editor.instance) return;
|
||||
|
||||
this.model = this.editor.createModel(this.file);
|
||||
|
||||
this.editor.attachModel(this.model);
|
||||
|
||||
this.model.onChange((model) => {
|
||||
const { file } = model;
|
||||
|
||||
if (file.active) {
|
||||
this.changeFileContent({
|
||||
path: file.path,
|
||||
content: model.getModel().getValue(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Cursor Position
|
||||
this.editor.onPositionChange((instance, e) => {
|
||||
this.setEditorPosition({
|
||||
editorRow: e.position.lineNumber,
|
||||
editorColumn: e.position.column,
|
||||
});
|
||||
});
|
||||
|
||||
this.editor.setPosition({
|
||||
lineNumber: this.file.editorRow,
|
||||
column: this.file.editorColumn,
|
||||
});
|
||||
|
||||
// Handle File Language
|
||||
this.setFileLanguage({
|
||||
fileLanguage: this.model.language,
|
||||
});
|
||||
|
||||
// Get File eol
|
||||
this.setFileEOL({
|
||||
eol: this.model.eol,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
id="ide"
|
||||
class="blob-viewer-container blob-editor-container"
|
||||
>
|
||||
<div
|
||||
v-if="shouldHideEditor"
|
||||
v-html="file.html"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-show="!shouldHideEditor"
|
||||
ref="editor"
|
||||
class="multi-file-editor-holder"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,128 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import fileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
import router from '../ide_router';
|
||||
import newDropdown from './new_dropdown/index.vue';
|
||||
import fileStatusIcon from './repo_file_status_icon.vue';
|
||||
import changedFileIcon from './changed_file_icon.vue';
|
||||
|
||||
export default {
|
||||
name: 'RepoFile',
|
||||
components: {
|
||||
skeletonLoadingContainer,
|
||||
newDropdown,
|
||||
fileStatusIcon,
|
||||
fileIcon,
|
||||
changedFileIcon,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isTree() {
|
||||
return this.file.type === 'tree';
|
||||
},
|
||||
isBlob() {
|
||||
return this.file.type === 'blob';
|
||||
},
|
||||
levelIndentation() {
|
||||
return {
|
||||
marginLeft: `${this.level * 16}px`,
|
||||
};
|
||||
},
|
||||
fileClass() {
|
||||
return {
|
||||
'file-open': this.isBlob && this.file.opened,
|
||||
'file-active': this.isBlob && this.file.active,
|
||||
folder: this.isTree,
|
||||
'is-open': this.file.opened,
|
||||
};
|
||||
},
|
||||
},
|
||||
updated() {
|
||||
if (this.file.type === 'blob' && this.file.active) {
|
||||
this.$el.scrollIntoView();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
|
||||
clickFile() {
|
||||
// Manual Action if a tree is selected/opened
|
||||
if (
|
||||
this.isTree &&
|
||||
this.$router.currentRoute.path === `/project${this.file.url}`
|
||||
) {
|
||||
this.toggleTreeOpen(this.file.path);
|
||||
}
|
||||
|
||||
const delayPromise = this.file.changed
|
||||
? Promise.resolve()
|
||||
: this.updateDelayViewerUpdated(true);
|
||||
|
||||
return delayPromise.then(() => {
|
||||
router.push(`/project${this.file.url}`);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="file"
|
||||
:class="fileClass"
|
||||
>
|
||||
<div
|
||||
class="file-name"
|
||||
@click="clickFile"
|
||||
role="button"
|
||||
>
|
||||
<span
|
||||
class="ide-file-name str-truncated"
|
||||
:style="levelIndentation"
|
||||
>
|
||||
<file-icon
|
||||
:file-name="file.name"
|
||||
:loading="file.loading"
|
||||
:folder="isTree"
|
||||
:opened="file.opened"
|
||||
:size="16"
|
||||
/>
|
||||
{{ file.name }}
|
||||
<file-status-icon
|
||||
:file="file"
|
||||
/>
|
||||
</span>
|
||||
<changed-file-icon
|
||||
:file="file"
|
||||
v-if="file.changed || file.tempFile"
|
||||
class="prepend-top-5 pull-right"
|
||||
/>
|
||||
<new-dropdown
|
||||
v-if="isTree"
|
||||
:project-id="file.projectId"
|
||||
:branch="file.branchId"
|
||||
:path="file.path"
|
||||
class="pull-right prepend-left-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="file.opened">
|
||||
<repo-file
|
||||
v-for="childFile in file.tree"
|
||||
:key="childFile.key"
|
||||
:file="childFile"
|
||||
:level="level + 1"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,61 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showButtons() {
|
||||
return this.file.rawPath ||
|
||||
this.file.blamePath ||
|
||||
this.file.commitsPath ||
|
||||
this.file.permalink;
|
||||
},
|
||||
rawDownloadButtonLabel() {
|
||||
return this.file.binary ? 'Download' : 'Raw';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="showButtons"
|
||||
class="multi-file-editor-btn-group"
|
||||
>
|
||||
<a
|
||||
:href="file.rawPath"
|
||||
target="_blank"
|
||||
class="btn btn-default btn-sm raw"
|
||||
rel="noopener noreferrer">
|
||||
{{ rawDownloadButtonLabel }}
|
||||
</a>
|
||||
|
||||
<div
|
||||
class="btn-group"
|
||||
role="group"
|
||||
aria-label="File actions"
|
||||
>
|
||||
<a
|
||||
:href="file.blamePath"
|
||||
class="btn btn-default btn-sm blame"
|
||||
>
|
||||
Blame
|
||||
</a>
|
||||
<a
|
||||
:href="file.commitsPath"
|
||||
class="btn btn-default btn-sm history"
|
||||
>
|
||||
History
|
||||
</a>
|
||||
<a
|
||||
:href="file.permalink"
|
||||
class="btn btn-default btn-sm permalink"
|
||||
>
|
||||
Permalink
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,39 @@
|
|||
<script>
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import '~/lib/utils/datetime_utility';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
lockTooltip() {
|
||||
return `Locked by ${this.file.file_lock.user.name}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
v-if="file.file_lock"
|
||||
v-tooltip
|
||||
:title="lockTooltip"
|
||||
data-container="body"
|
||||
>
|
||||
<icon
|
||||
name="lock"
|
||||
css-classes="file-status-icon"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
|
@ -0,0 +1,42 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
skeletonLoadingContainer,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'leftPanelCollapsed',
|
||||
]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr
|
||||
class="loading-file"
|
||||
aria-label="Loading files"
|
||||
>
|
||||
<td class="multi-file-table-col-name">
|
||||
<skeleton-loading-container
|
||||
:small="true"
|
||||
/>
|
||||
</td>
|
||||
<template v-if="!leftPanelCollapsed">
|
||||
<td class="hidden-sm hidden-xs">
|
||||
<skeleton-loading-container
|
||||
:small="true"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td class="hidden-xs">
|
||||
<skeleton-loading-container
|
||||
class="animation-container-right"
|
||||
:small="true"
|
||||
/>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</template>
|
|
@ -0,0 +1,98 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
|
||||
import fileIcon from '~/vue_shared/components/file_icon.vue';
|
||||
import icon from '~/vue_shared/components/icon.vue';
|
||||
import fileStatusIcon from './repo_file_status_icon.vue';
|
||||
import changedFileIcon from './changed_file_icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
fileStatusIcon,
|
||||
fileIcon,
|
||||
icon,
|
||||
changedFileIcon,
|
||||
},
|
||||
props: {
|
||||
tab: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabMouseOver: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
closeLabel() {
|
||||
if (this.tab.changed || this.tab.tempFile) {
|
||||
return `${this.tab.name} changed`;
|
||||
}
|
||||
return `Close ${this.tab.name}`;
|
||||
},
|
||||
showChangedIcon() {
|
||||
return this.tab.changed ? !this.tabMouseOver : false;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions([
|
||||
'closeFile',
|
||||
]),
|
||||
clickFile(tab) {
|
||||
this.$router.push(`/project${tab.url}`);
|
||||
},
|
||||
mouseOverTab() {
|
||||
if (this.tab.changed) {
|
||||
this.tabMouseOver = true;
|
||||
}
|
||||
},
|
||||
mouseOutTab() {
|
||||
if (this.tab.changed) {
|
||||
this.tabMouseOver = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
@click="clickFile(tab)"
|
||||
@mouseover="mouseOverTab"
|
||||
@mouseout="mouseOutTab"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="multi-file-tab-close"
|
||||
@click.stop.prevent="closeFile(tab.path)"
|
||||
:aria-label="closeLabel"
|
||||
>
|
||||
<icon
|
||||
v-if="!showChangedIcon"
|
||||
name="close"
|
||||
:size="12"
|
||||
/>
|
||||
<changed-file-icon
|
||||
v-else
|
||||
:file="tab"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="multi-file-tab"
|
||||
:class="{active : tab.active }"
|
||||
:title="tab.url"
|
||||
>
|
||||
<file-icon
|
||||
:file-name="tab.name"
|
||||
:size="16"
|
||||
/>
|
||||
{{ tab.name }}
|
||||
<file-status-icon
|
||||
:file="tab"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
|
@ -0,0 +1,61 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import RepoTab from './repo_tab.vue';
|
||||
import EditorMode from './editor_mode_dropdown.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RepoTab,
|
||||
EditorMode,
|
||||
},
|
||||
props: {
|
||||
files: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
viewer: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
hasChanges: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showShadow: false,
|
||||
};
|
||||
},
|
||||
updated() {
|
||||
if (!this.$refs.tabsScroller) return;
|
||||
|
||||
this.showShadow =
|
||||
this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['updateViewer']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multi-file-tabs">
|
||||
<ul
|
||||
class="list-unstyled append-bottom-0"
|
||||
ref="tabsScroller"
|
||||
>
|
||||
<repo-tab
|
||||
v-for="tab in files"
|
||||
:key="tab.key"
|
||||
:tab="tab"
|
||||
/>
|
||||
</ul>
|
||||
<editor-mode
|
||||
:viewer="viewer"
|
||||
:show-shadow="showShadow"
|
||||
:has-changes="hasChanges"
|
||||
@click="updateViewer"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,88 @@
|
|||
<script>
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PanelResizer,
|
||||
},
|
||||
props: {
|
||||
collapsible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
initialWidth: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
minSize: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 200,
|
||||
},
|
||||
side: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
width: this.initialWidth,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
collapsed(state) {
|
||||
return state[`${this.side}PanelCollapsed`];
|
||||
},
|
||||
}),
|
||||
panelStyle() {
|
||||
if (!this.collapsed) {
|
||||
return {
|
||||
width: `${this.width}px`,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
'setPanelCollapsedStatus',
|
||||
'setResizingStatus',
|
||||
]),
|
||||
toggleFullbarCollapsed() {
|
||||
if (this.collapsed && this.collapsible) {
|
||||
this.setPanelCollapsedStatus({
|
||||
side: this.side,
|
||||
collapsed: !this.collapsed,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
maxSize: (window.innerWidth / 2),
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="multi-file-commit-panel"
|
||||
:class="{
|
||||
'is-collapsed': collapsed && collapsible,
|
||||
}"
|
||||
:style="panelStyle"
|
||||
@click="toggleFullbarCollapsed"
|
||||
>
|
||||
<slot></slot>
|
||||
<panel-resizer
|
||||
:size.sync="width"
|
||||
:enabled="!collapsed"
|
||||
:start-size="initialWidth"
|
||||
:min-size="minSize"
|
||||
:max-size="$options.maxSize"
|
||||
@resize-start="setResizingStatus(true)"
|
||||
@resize-end="setResizingStatus(false)"
|
||||
:side="side === 'right' ? 'left' : 'right'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
export default new Vue();
|
|
@ -0,0 +1,117 @@
|
|||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import flash from '~/flash';
|
||||
import store from './stores';
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
/**
|
||||
* Routes below /-/ide/:
|
||||
|
||||
/project/h5bp/html5-boilerplate/blob/master
|
||||
/project/h5bp/html5-boilerplate/blob/master/app/js/test.js
|
||||
|
||||
/project/h5bp/html5-boilerplate/mr/123
|
||||
/project/h5bp/html5-boilerplate/mr/123/app/js/test.js
|
||||
|
||||
/workspace/123
|
||||
/workspace/project/h5bp/html5-boilerplate/blob/my-special-branch
|
||||
/workspace/project/h5bp/html5-boilerplate/mr/123
|
||||
|
||||
/ = /workspace
|
||||
|
||||
/settings
|
||||
*/
|
||||
|
||||
// Unfortunately Vue Router doesn't work without at least a fake component
|
||||
// If you do only data handling
|
||||
const EmptyRouterComponent = {
|
||||
render(createElement) {
|
||||
return createElement('div');
|
||||
},
|
||||
};
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: `${gon.relative_url_root}/-/ide/`,
|
||||
routes: [
|
||||
{
|
||||
path: '/project/:namespace/:project',
|
||||
component: EmptyRouterComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':targetmode/:branch/*',
|
||||
component: EmptyRouterComponent,
|
||||
},
|
||||
{
|
||||
path: 'mr/:mrid',
|
||||
component: EmptyRouterComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.params.namespace && to.params.project) {
|
||||
store
|
||||
.dispatch('getProjectData', {
|
||||
namespace: to.params.namespace,
|
||||
projectId: to.params.project,
|
||||
})
|
||||
.then(() => {
|
||||
const fullProjectId = `${to.params.namespace}/${to.params.project}`;
|
||||
|
||||
if (to.params.branch) {
|
||||
store.dispatch('getBranchData', {
|
||||
projectId: fullProjectId,
|
||||
branchId: to.params.branch,
|
||||
});
|
||||
|
||||
store
|
||||
.dispatch('getFiles', {
|
||||
projectId: fullProjectId,
|
||||
branchId: to.params.branch,
|
||||
})
|
||||
.then(() => {
|
||||
if (to.params[0]) {
|
||||
const path =
|
||||
to.params[0].slice(-1) === '/'
|
||||
? to.params[0].slice(0, -1)
|
||||
: to.params[0];
|
||||
const treeEntry = store.state.entries[path];
|
||||
if (treeEntry) {
|
||||
store.dispatch('handleTreeEntryAction', treeEntry);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
flash(
|
||||
'Error while loading the branch files. Please try again.',
|
||||
'alert',
|
||||
document,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
flash(
|
||||
'Error while loading the project data. Please try again.',
|
||||
'alert',
|
||||
document,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -0,0 +1,33 @@
|
|||
import Vue from 'vue';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import ide from './components/ide.vue';
|
||||
import store from './stores';
|
||||
import router from './ide_router';
|
||||
|
||||
function initIde(el) {
|
||||
if (!el) return null;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store,
|
||||
router,
|
||||
components: {
|
||||
ide,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('ide', {
|
||||
props: {
|
||||
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
|
||||
noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
|
||||
committedStateSvgPath: el.dataset.committedStateSvgPath,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const ideElement = document.getElementById('ide');
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
initIde(ideElement);
|
|
@ -0,0 +1,14 @@
|
|||
export default class Disposable {
|
||||
constructor() {
|
||||
this.disposers = new Set();
|
||||
}
|
||||
|
||||
add(...disposers) {
|
||||
disposers.forEach(disposer => this.disposers.add(disposer));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposers.forEach(disposer => disposer.dispose());
|
||||
this.disposers.clear();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/* global monaco */
|
||||
import Disposable from './disposable';
|
||||
import eventHub from '../../eventhub';
|
||||
|
||||
export default class Model {
|
||||
constructor(monaco, file) {
|
||||
this.monaco = monaco;
|
||||
this.disposable = new Disposable();
|
||||
this.file = file;
|
||||
this.content = file.content !== '' ? file.content : file.raw;
|
||||
|
||||
this.disposable.add(
|
||||
(this.originalModel = this.monaco.editor.createModel(
|
||||
this.file.raw,
|
||||
undefined,
|
||||
new this.monaco.Uri(null, null, `original/${this.file.path}`),
|
||||
)),
|
||||
(this.model = this.monaco.editor.createModel(
|
||||
this.content,
|
||||
undefined,
|
||||
new this.monaco.Uri(null, null, this.file.path),
|
||||
)),
|
||||
);
|
||||
|
||||
this.events = new Map();
|
||||
|
||||
this.updateContent = this.updateContent.bind(this);
|
||||
this.dispose = this.dispose.bind(this);
|
||||
|
||||
eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose);
|
||||
eventHub.$on(
|
||||
`editor.update.model.content.${this.file.path}`,
|
||||
this.updateContent,
|
||||
);
|
||||
}
|
||||
|
||||
get url() {
|
||||
return this.model.uri.toString();
|
||||
}
|
||||
|
||||
get language() {
|
||||
return this.model.getModeId();
|
||||
}
|
||||
|
||||
get eol() {
|
||||
return this.model.getEOL() === '\n' ? 'LF' : 'CRLF';
|
||||
}
|
||||
|
||||
get path() {
|
||||
return this.file.path;
|
||||
}
|
||||
|
||||
getModel() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
getOriginalModel() {
|
||||
return this.originalModel;
|
||||
}
|
||||
|
||||
setValue(value) {
|
||||
this.getModel().setValue(value);
|
||||
}
|
||||
|
||||
onChange(cb) {
|
||||
this.events.set(
|
||||
this.path,
|
||||
this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))),
|
||||
);
|
||||
}
|
||||
|
||||
updateContent(content) {
|
||||
this.getOriginalModel().setValue(content);
|
||||
this.getModel().setValue(content);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable.dispose();
|
||||
this.events.clear();
|
||||
|
||||
eventHub.$off(
|
||||
`editor.update.model.dispose.${this.file.path}`,
|
||||
this.dispose,
|
||||
);
|
||||
eventHub.$off(
|
||||
`editor.update.model.content.${this.file.path}`,
|
||||
this.updateContent,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import eventHub from '../../eventhub';
|
||||
import Disposable from './disposable';
|
||||
import Model from './model';
|
||||
|
||||
export default class ModelManager {
|
||||
constructor(monaco) {
|
||||
this.monaco = monaco;
|
||||
this.disposable = new Disposable();
|
||||
this.models = new Map();
|
||||
}
|
||||
|
||||
hasCachedModel(path) {
|
||||
return this.models.has(path);
|
||||
}
|
||||
|
||||
getModel(path) {
|
||||
return this.models.get(path);
|
||||
}
|
||||
|
||||
addModel(file) {
|
||||
if (this.hasCachedModel(file.path)) {
|
||||
return this.getModel(file.path);
|
||||
}
|
||||
|
||||
const model = new Model(this.monaco, file);
|
||||
this.models.set(model.path, model);
|
||||
this.disposable.add(model);
|
||||
|
||||
eventHub.$on(
|
||||
`editor.update.model.dispose.${file.path}`,
|
||||
this.removeCachedModel.bind(this, file),
|
||||
);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
removeCachedModel(file) {
|
||||
this.models.delete(file.path);
|
||||
|
||||
eventHub.$off(
|
||||
`editor.update.model.dispose.${file.path}`,
|
||||
this.removeCachedModel,
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// dispose of all the models
|
||||
this.disposable.dispose();
|
||||
this.models.clear();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
export default class DecorationsController {
|
||||
constructor(editor) {
|
||||
this.editor = editor;
|
||||
this.decorations = new Map();
|
||||
this.editorDecorations = new Map();
|
||||
}
|
||||
|
||||
getAllDecorationsForModel(model) {
|
||||
if (!this.decorations.has(model.url)) return [];
|
||||
|
||||
const modelDecorations = this.decorations.get(model.url);
|
||||
const decorations = [];
|
||||
|
||||
modelDecorations.forEach(val => decorations.push(...val));
|
||||
|
||||
return decorations;
|
||||
}
|
||||
|
||||
addDecorations(model, decorationsKey, decorations) {
|
||||
const decorationMap = this.decorations.get(model.url) || new Map();
|
||||
|
||||
decorationMap.set(decorationsKey, decorations);
|
||||
|
||||
this.decorations.set(model.url, decorationMap);
|
||||
|
||||
this.decorate(model);
|
||||
}
|
||||
|
||||
decorate(model) {
|
||||
if (!this.editor.instance) return;
|
||||
|
||||
const decorations = this.getAllDecorationsForModel(model);
|
||||
const oldDecorations = this.editorDecorations.get(model.url) || [];
|
||||
|
||||
this.editorDecorations.set(
|
||||
model.url,
|
||||
this.editor.instance.deltaDecorations(oldDecorations, decorations),
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.decorations.clear();
|
||||
this.editorDecorations.clear();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/* global monaco */
|
||||
import { throttle } from 'underscore';
|
||||
import DirtyDiffWorker from './diff_worker';
|
||||
import Disposable from '../common/disposable';
|
||||
|
||||
export const getDiffChangeType = (change) => {
|
||||
if (change.modified) {
|
||||
return 'modified';
|
||||
} else if (change.added) {
|
||||
return 'added';
|
||||
} else if (change.removed) {
|
||||
return 'removed';
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
export const getDecorator = change => ({
|
||||
range: new monaco.Range(
|
||||
change.lineNumber,
|
||||
1,
|
||||
change.endLineNumber,
|
||||
1,
|
||||
),
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
|
||||
},
|
||||
});
|
||||
|
||||
export default class DirtyDiffController {
|
||||
constructor(modelManager, decorationsController) {
|
||||
this.disposable = new Disposable();
|
||||
this.editorSimpleWorker = null;
|
||||
this.modelManager = modelManager;
|
||||
this.decorationsController = decorationsController;
|
||||
this.dirtyDiffWorker = new DirtyDiffWorker();
|
||||
this.throttledComputeDiff = throttle(this.computeDiff, 250);
|
||||
this.decorate = this.decorate.bind(this);
|
||||
|
||||
this.dirtyDiffWorker.addEventListener('message', this.decorate);
|
||||
}
|
||||
|
||||
attachModel(model) {
|
||||
model.onChange(() => this.throttledComputeDiff(model));
|
||||
}
|
||||
|
||||
computeDiff(model) {
|
||||
this.dirtyDiffWorker.postMessage({
|
||||
path: model.path,
|
||||
originalContent: model.getOriginalModel().getValue(),
|
||||
newContent: model.getModel().getValue(),
|
||||
});
|
||||
}
|
||||
|
||||
reDecorate(model) {
|
||||
this.decorationsController.decorate(model);
|
||||
}
|
||||
|
||||
decorate({ data }) {
|
||||
const decorations = data.changes.map(change => getDecorator(change));
|
||||
const model = this.modelManager.getModel(data.path);
|
||||
this.decorationsController.addDecorations(model, 'dirtyDiff', decorations);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable.dispose();
|
||||
|
||||
this.dirtyDiffWorker.removeEventListener('message', this.decorate);
|
||||
this.dirtyDiffWorker.terminate();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { diffLines } from 'diff';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const computeDiff = (originalContent, newContent) => {
|
||||
const changes = diffLines(originalContent, newContent);
|
||||
|
||||
let lineNumber = 1;
|
||||
return changes.reduce((acc, change) => {
|
||||
const findOnLine = acc.find(c => c.lineNumber === lineNumber);
|
||||
|
||||
if (findOnLine) {
|
||||
Object.assign(findOnLine, change, {
|
||||
modified: true,
|
||||
endLineNumber: (lineNumber + change.count) - 1,
|
||||
});
|
||||
} else if ('added' in change || 'removed' in change) {
|
||||
acc.push(Object.assign({}, change, {
|
||||
lineNumber,
|
||||
modified: undefined,
|
||||
endLineNumber: (lineNumber + change.count) - 1,
|
||||
}));
|
||||
}
|
||||
|
||||
if (!change.removed) {
|
||||
lineNumber += change.count;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
import { computeDiff } from './diff';
|
||||
|
||||
self.addEventListener('message', (e) => {
|
||||
const data = e.data;
|
||||
|
||||
self.postMessage({
|
||||
path: data.path,
|
||||
changes: computeDiff(data.originalContent, data.newContent),
|
||||
});
|
||||
});
|
|
@ -0,0 +1,168 @@
|
|||
import _ from 'underscore';
|
||||
import DecorationsController from './decorations/controller';
|
||||
import DirtyDiffController from './diff/controller';
|
||||
import Disposable from './common/disposable';
|
||||
import ModelManager from './common/model_manager';
|
||||
import editorOptions, { defaultEditorOptions } from './editor_options';
|
||||
import gitlabTheme from './themes/gl_theme';
|
||||
|
||||
export const clearDomElement = el => {
|
||||
if (!el || !el.firstChild) return;
|
||||
|
||||
while (el.firstChild) {
|
||||
el.removeChild(el.firstChild);
|
||||
}
|
||||
};
|
||||
|
||||
export default class Editor {
|
||||
static create(monaco) {
|
||||
if (this.editorInstance) return this.editorInstance;
|
||||
|
||||
this.editorInstance = new Editor(monaco);
|
||||
|
||||
return this.editorInstance;
|
||||
}
|
||||
|
||||
constructor(monaco) {
|
||||
this.monaco = monaco;
|
||||
this.currentModel = null;
|
||||
this.instance = null;
|
||||
this.dirtyDiffController = null;
|
||||
this.disposable = new Disposable();
|
||||
this.modelManager = new ModelManager(this.monaco);
|
||||
this.decorationsController = new DecorationsController(this);
|
||||
|
||||
this.setupMonacoTheme();
|
||||
|
||||
this.debouncedUpdate = _.debounce(() => {
|
||||
this.updateDimensions();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
createInstance(domElement) {
|
||||
if (!this.instance) {
|
||||
clearDomElement(domElement);
|
||||
|
||||
this.disposable.add(
|
||||
(this.instance = this.monaco.editor.create(domElement, {
|
||||
...defaultEditorOptions,
|
||||
})),
|
||||
(this.dirtyDiffController = new DirtyDiffController(
|
||||
this.modelManager,
|
||||
this.decorationsController,
|
||||
)),
|
||||
);
|
||||
|
||||
window.addEventListener('resize', this.debouncedUpdate, false);
|
||||
}
|
||||
}
|
||||
|
||||
createDiffInstance(domElement) {
|
||||
if (!this.instance) {
|
||||
clearDomElement(domElement);
|
||||
|
||||
this.disposable.add(
|
||||
(this.instance = this.monaco.editor.createDiffEditor(domElement, {
|
||||
...defaultEditorOptions,
|
||||
readOnly: true,
|
||||
quickSuggestions: false,
|
||||
occurrencesHighlight: false,
|
||||
renderLineHighlight: 'none',
|
||||
hideCursorInOverviewRuler: true,
|
||||
})),
|
||||
);
|
||||
|
||||
window.addEventListener('resize', this.debouncedUpdate, false);
|
||||
}
|
||||
}
|
||||
|
||||
createModel(file) {
|
||||
return this.modelManager.addModel(file);
|
||||
}
|
||||
|
||||
attachModel(model) {
|
||||
if (this.instance.getEditorType() === 'vs.editor.IDiffEditor') {
|
||||
this.instance.setModel({
|
||||
original: model.getOriginalModel(),
|
||||
modified: model.getModel(),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.instance.setModel(model.getModel());
|
||||
if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model);
|
||||
|
||||
this.currentModel = model;
|
||||
|
||||
this.instance.updateOptions(
|
||||
editorOptions.reduce((acc, obj) => {
|
||||
Object.keys(obj).forEach(key => {
|
||||
Object.assign(acc, {
|
||||
[key]: obj[key](model),
|
||||
});
|
||||
});
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
|
||||
}
|
||||
|
||||
setupMonacoTheme() {
|
||||
this.monaco.editor.defineTheme(
|
||||
gitlabTheme.themeName,
|
||||
gitlabTheme.monacoTheme,
|
||||
);
|
||||
|
||||
this.monaco.editor.setTheme('gitlab');
|
||||
}
|
||||
|
||||
clearEditor() {
|
||||
if (this.instance) {
|
||||
this.instance.setModel(null);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
window.removeEventListener('resize', this.debouncedUpdate);
|
||||
|
||||
// catch any potential errors with disposing the error
|
||||
// this is mainly for tests caused by elements not existing
|
||||
try {
|
||||
this.disposable.dispose();
|
||||
|
||||
this.instance = null;
|
||||
} catch (e) {
|
||||
this.instance = null;
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateDimensions() {
|
||||
this.instance.layout();
|
||||
}
|
||||
|
||||
setPosition({ lineNumber, column }) {
|
||||
this.instance.revealPositionInCenter({
|
||||
lineNumber,
|
||||
column,
|
||||
});
|
||||
this.instance.setPosition({
|
||||
lineNumber,
|
||||
column,
|
||||
});
|
||||
}
|
||||
|
||||
onPositionChange(cb) {
|
||||
if (!this.instance.onDidChangeCursorPosition) return;
|
||||
|
||||
this.disposable.add(
|
||||
this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
export const defaultEditorOptions = {
|
||||
model: null,
|
||||
readOnly: false,
|
||||
contextmenu: true,
|
||||
scrollBeyondLastLine: false,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default [
|
||||
{
|
||||
readOnly: model => !!model.file.file_lock,
|
||||
},
|
||||
];
|
|
@ -0,0 +1,14 @@
|
|||
export default {
|
||||
themeName: 'gitlab',
|
||||
monacoTheme: {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editorLineNumber.foreground': '#CCCCCC',
|
||||
'diffEditor.insertedTextBackground': '#ddfbe6',
|
||||
'diffEditor.removedTextBackground': '#f9d7dc',
|
||||
'editor.selectionBackground': '#aad6f8',
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
import monacoContext from 'monaco-editor/dev/vs/loader';
|
||||
|
||||
monacoContext.require.config({
|
||||
paths: {
|
||||
vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
|
||||
},
|
||||
});
|
||||
|
||||
// ignore CDN config and use local assets path for service worker which cannot be cross-domain
|
||||
const relativeRootPath = (gon && gon.relative_url_root) || '';
|
||||
const monacoPath = `${relativeRootPath}/assets/webpack/monaco-editor/vs`;
|
||||
window.MonacoEnvironment = { getWorkerUrl: () => `${monacoPath}/base/worker/workerMain.js` };
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
window.__monaco_context__ = monacoContext;
|
||||
export default monacoContext.require;
|
|
@ -0,0 +1,55 @@
|
|||
import Vue from 'vue';
|
||||
import VueResource from 'vue-resource';
|
||||
import Api from '~/api';
|
||||
|
||||
Vue.use(VueResource);
|
||||
|
||||
export default {
|
||||
getTreeData(endpoint) {
|
||||
return Vue.http.get(endpoint, { params: { format: 'json' } });
|
||||
},
|
||||
getFileData(endpoint) {
|
||||
return Vue.http.get(endpoint, { params: { format: 'json' } });
|
||||
},
|
||||
getRawFileData(file) {
|
||||
if (file.tempFile) {
|
||||
return Promise.resolve(file.content);
|
||||
}
|
||||
|
||||
if (file.raw) {
|
||||
return Promise.resolve(file.raw);
|
||||
}
|
||||
|
||||
return Vue.http.get(file.rawPath, { params: { format: 'json' } })
|
||||
.then(res => res.text());
|
||||
},
|
||||
getProjectData(namespace, project) {
|
||||
return Api.project(`${namespace}/${project}`);
|
||||
},
|
||||
getBranchData(projectId, currentBranchId) {
|
||||
return Api.branchSingle(projectId, currentBranchId);
|
||||
},
|
||||
createBranch(projectId, payload) {
|
||||
const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);
|
||||
|
||||
return Vue.http.post(url, payload);
|
||||
},
|
||||
commit(projectId, payload) {
|
||||
return Api.commitMultiple(projectId, payload);
|
||||
},
|
||||
getTreeLastCommit(endpoint) {
|
||||
return Vue.http.get(endpoint, {
|
||||
params: {
|
||||
format: 'json',
|
||||
},
|
||||
});
|
||||
},
|
||||
getFiles(projectUrl, branchId) {
|
||||
const url = `${projectUrl}/files/${branchId}`;
|
||||
return Vue.http.get(url, {
|
||||
params: {
|
||||
format: 'json',
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,121 @@
|
|||
import Vue from 'vue';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import flash from '~/flash';
|
||||
import * as types from './mutation_types';
|
||||
import FilesDecoratorWorker from './workers/files_decorator_worker';
|
||||
|
||||
export const redirectToUrl = (_, url) => visitUrl(url);
|
||||
|
||||
export const setInitialData = ({ commit }, data) =>
|
||||
commit(types.SET_INITIAL_DATA, data);
|
||||
|
||||
export const discardAllChanges = ({ state, commit, dispatch }) => {
|
||||
state.changedFiles.forEach(file => {
|
||||
commit(types.DISCARD_FILE_CHANGES, file.path);
|
||||
|
||||
if (file.tempFile) {
|
||||
dispatch('closeFile', file.path);
|
||||
}
|
||||
});
|
||||
|
||||
commit(types.REMOVE_ALL_CHANGES_FILES);
|
||||
};
|
||||
|
||||
export const closeAllFiles = ({ state, dispatch }) => {
|
||||
state.openFiles.forEach(file => dispatch('closeFile', file.path));
|
||||
};
|
||||
|
||||
export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
|
||||
if (side === 'left') {
|
||||
commit(types.SET_LEFT_PANEL_COLLAPSED, collapsed);
|
||||
} else {
|
||||
commit(types.SET_RIGHT_PANEL_COLLAPSED, collapsed);
|
||||
}
|
||||
};
|
||||
|
||||
export const setResizingStatus = ({ commit }, resizing) => {
|
||||
commit(types.SET_RESIZING_STATUS, resizing);
|
||||
};
|
||||
|
||||
export const createTempEntry = (
|
||||
{ state, commit, dispatch },
|
||||
{ branchId, name, type, content = '', base64 = false },
|
||||
) =>
|
||||
new Promise(resolve => {
|
||||
const worker = new FilesDecoratorWorker();
|
||||
const fullName =
|
||||
name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
|
||||
|
||||
if (state.entries[name]) {
|
||||
flash(
|
||||
`The name "${name
|
||||
.split('/')
|
||||
.pop()}" is already taken in this directory.`,
|
||||
'alert',
|
||||
document,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
||||
resolve();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
worker.addEventListener('message', ({ data }) => {
|
||||
const { file } = data;
|
||||
|
||||
worker.terminate();
|
||||
|
||||
commit(types.CREATE_TMP_ENTRY, {
|
||||
data,
|
||||
projectId: state.currentProjectId,
|
||||
branchId,
|
||||
});
|
||||
|
||||
if (type === 'blob') {
|
||||
commit(types.TOGGLE_FILE_OPEN, file.path);
|
||||
commit(types.ADD_FILE_TO_CHANGED, file.path);
|
||||
dispatch('setFileActive', file.path);
|
||||
}
|
||||
|
||||
resolve(file);
|
||||
});
|
||||
|
||||
worker.postMessage({
|
||||
data: [fullName],
|
||||
projectId: state.currentProjectId,
|
||||
branchId,
|
||||
type,
|
||||
tempFile: true,
|
||||
base64,
|
||||
content,
|
||||
});
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
export const scrollToTab = () => {
|
||||
Vue.nextTick(() => {
|
||||
const tabs = document.getElementById('tabs');
|
||||
|
||||
if (tabs) {
|
||||
const tabEl = tabs.querySelector('.active .repo-tab');
|
||||
|
||||
tabEl.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const updateViewer = ({ commit }, viewer) => {
|
||||
commit(types.UPDATE_VIEWER, viewer);
|
||||
};
|
||||
|
||||
export const updateDelayViewerUpdated = ({ commit }, delay) => {
|
||||
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
|
||||
};
|
||||
|
||||
export * from './actions/tree';
|
||||
export * from './actions/file';
|
||||
export * from './actions/project';
|
|
@ -0,0 +1,146 @@
|
|||
import { normalizeHeaders } from '~/lib/utils/common_utils';
|
||||
import flash from '~/flash';
|
||||
import eventHub from '../../eventhub';
|
||||
import service from '../../services';
|
||||
import * as types from '../mutation_types';
|
||||
import router from '../../ide_router';
|
||||
import { setPageTitle } from '../utils';
|
||||
|
||||
export const closeFile = ({ commit, state, getters, dispatch }, path) => {
|
||||
const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path);
|
||||
const file = state.entries[path];
|
||||
const fileWasActive = file.active;
|
||||
|
||||
commit(types.TOGGLE_FILE_OPEN, path);
|
||||
commit(types.SET_FILE_ACTIVE, { path, active: false });
|
||||
|
||||
if (state.openFiles.length > 0 && fileWasActive) {
|
||||
const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
|
||||
const nextFileToOpen = state.entries[state.openFiles[nextIndexToOpen].path];
|
||||
|
||||
router.push(`/project${nextFileToOpen.url}`);
|
||||
} else if (!state.openFiles.length) {
|
||||
router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
|
||||
}
|
||||
|
||||
eventHub.$emit(`editor.update.model.dispose.${file.path}`);
|
||||
};
|
||||
|
||||
export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
|
||||
const file = state.entries[path];
|
||||
const currentActiveFile = getters.activeFile;
|
||||
|
||||
if (file.active) return;
|
||||
|
||||
if (currentActiveFile) {
|
||||
commit(types.SET_FILE_ACTIVE, {
|
||||
path: currentActiveFile.path,
|
||||
active: false,
|
||||
});
|
||||
}
|
||||
|
||||
commit(types.SET_FILE_ACTIVE, { path, active: true });
|
||||
dispatch('scrollToTab');
|
||||
|
||||
commit(types.SET_CURRENT_PROJECT, file.projectId);
|
||||
commit(types.SET_CURRENT_BRANCH, file.branchId);
|
||||
};
|
||||
|
||||
export const getFileData = ({ state, commit, dispatch }, file) => {
|
||||
commit(types.TOGGLE_LOADING, { entry: file });
|
||||
|
||||
return service
|
||||
.getFileData(file.url)
|
||||
.then(res => {
|
||||
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
|
||||
|
||||
setPageTitle(pageTitle);
|
||||
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
commit(types.SET_FILE_DATA, { data, file });
|
||||
commit(types.TOGGLE_FILE_OPEN, file.path);
|
||||
dispatch('setFileActive', file.path);
|
||||
commit(types.TOGGLE_LOADING, { entry: file });
|
||||
})
|
||||
.catch(() => {
|
||||
commit(types.TOGGLE_LOADING, { entry: file });
|
||||
flash(
|
||||
'Error loading file data. Please try again.',
|
||||
'alert',
|
||||
document,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const getRawFileData = ({ commit, dispatch }, file) =>
|
||||
service
|
||||
.getRawFileData(file)
|
||||
.then(raw => {
|
||||
commit(types.SET_FILE_RAW_DATA, { file, raw });
|
||||
})
|
||||
.catch(() =>
|
||||
flash(
|
||||
'Error loading file content. Please try again.',
|
||||
'alert',
|
||||
document,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
),
|
||||
);
|
||||
|
||||
export const changeFileContent = ({ state, commit }, { path, content }) => {
|
||||
const file = state.entries[path];
|
||||
commit(types.UPDATE_FILE_CONTENT, { path, content });
|
||||
|
||||
const indexOfChangedFile = state.changedFiles.findIndex(f => f.path === path);
|
||||
|
||||
if (file.changed && indexOfChangedFile === -1) {
|
||||
commit(types.ADD_FILE_TO_CHANGED, path);
|
||||
} else if (!file.changed && indexOfChangedFile !== -1) {
|
||||
commit(types.REMOVE_FILE_FROM_CHANGED, path);
|
||||
}
|
||||
};
|
||||
|
||||
export const setFileLanguage = ({ getters, commit }, { fileLanguage }) => {
|
||||
if (getters.activeFile) {
|
||||
commit(types.SET_FILE_LANGUAGE, { file: getters.activeFile, fileLanguage });
|
||||
}
|
||||
};
|
||||
|
||||
export const setFileEOL = ({ getters, commit }, { eol }) => {
|
||||
if (getters.activeFile) {
|
||||
commit(types.SET_FILE_EOL, { file: getters.activeFile, eol });
|
||||
}
|
||||
};
|
||||
|
||||
export const setEditorPosition = (
|
||||
{ getters, commit },
|
||||
{ editorRow, editorColumn },
|
||||
) => {
|
||||
if (getters.activeFile) {
|
||||
commit(types.SET_FILE_POSITION, {
|
||||
file: getters.activeFile,
|
||||
editorRow,
|
||||
editorColumn,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const discardFileChanges = ({ state, commit }, path) => {
|
||||
const file = state.entries[path];
|
||||
|
||||
commit(types.DISCARD_FILE_CHANGES, path);
|
||||
commit(types.REMOVE_FILE_FROM_CHANGED, path);
|
||||
|
||||
if (file.tempFile && file.opened) {
|
||||
commit(types.TOGGLE_FILE_OPEN, path);
|
||||
}
|
||||
|
||||
eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
import flash from '~/flash';
|
||||
import service from '../../services';
|
||||
import * as types from '../mutation_types';
|
||||
|
||||
export const getProjectData = (
|
||||
{ commit, state, dispatch },
|
||||
{ namespace, projectId, force = false } = {},
|
||||
) => new Promise((resolve, reject) => {
|
||||
if (!state.projects[`${namespace}/${projectId}`] || force) {
|
||||
commit(types.TOGGLE_LOADING, { entry: state });
|
||||
service.getProjectData(namespace, projectId)
|
||||
.then(res => res.data)
|
||||
.then((data) => {
|
||||
commit(types.TOGGLE_LOADING, { entry: state });
|
||||
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
|
||||
if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
|
||||
resolve(data);
|
||||
})
|
||||
.catch(() => {
|
||||
flash('Error loading project data. Please try again.', 'alert', document, null, false, true);
|
||||
reject(new Error(`Project not loaded ${namespace}/${projectId}`));
|
||||
});
|
||||
} else {
|
||||
resolve(state.projects[`${namespace}/${projectId}`]);
|
||||
}
|
||||
});
|
||||
|
||||
export const getBranchData = (
|
||||
{ commit, state, dispatch },
|
||||
{ projectId, branchId, force = false } = {},
|
||||
) => new Promise((resolve, reject) => {
|
||||
if ((typeof state.projects[`${projectId}`] === 'undefined' ||
|
||||
!state.projects[`${projectId}`].branches[branchId])
|
||||
|| force) {
|
||||
service.getBranchData(`${projectId}`, branchId)
|
||||
.then(({ data }) => {
|
||||
const { id } = data.commit;
|
||||
commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
|
||||
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
|
||||
resolve(data);
|
||||
})
|
||||
.catch(() => {
|
||||
flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
|
||||
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
|
||||
});
|
||||
} else {
|
||||
resolve(state.projects[`${projectId}`].branches[branchId]);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,93 @@
|
|||
import { normalizeHeaders } from '~/lib/utils/common_utils';
|
||||
import flash from '~/flash';
|
||||
import service from '../../services';
|
||||
import * as types from '../mutation_types';
|
||||
import {
|
||||
findEntry,
|
||||
} from '../utils';
|
||||
import FilesDecoratorWorker from '../workers/files_decorator_worker';
|
||||
|
||||
export const toggleTreeOpen = ({ commit, dispatch }, path) => {
|
||||
commit(types.TOGGLE_TREE_OPEN, path);
|
||||
};
|
||||
|
||||
export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
|
||||
if (row.type === 'tree') {
|
||||
dispatch('toggleTreeOpen', row.path);
|
||||
} else if (row.type === 'blob' && (row.opened || row.changed)) {
|
||||
if (row.changed && !row.opened) {
|
||||
commit(types.TOGGLE_FILE_OPEN, row.path);
|
||||
}
|
||||
|
||||
dispatch('setFileActive', row.path);
|
||||
} else {
|
||||
dispatch('getFileData', row);
|
||||
}
|
||||
};
|
||||
|
||||
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
|
||||
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
|
||||
|
||||
service.getTreeLastCommit(tree.lastCommitPath)
|
||||
.then((res) => {
|
||||
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
|
||||
|
||||
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
|
||||
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
data.forEach((lastCommit) => {
|
||||
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
|
||||
|
||||
if (entry) {
|
||||
commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
|
||||
}
|
||||
});
|
||||
|
||||
dispatch('getLastCommitData', tree);
|
||||
})
|
||||
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
|
||||
};
|
||||
|
||||
export const getFiles = (
|
||||
{ state, commit, dispatch },
|
||||
{ projectId, branchId } = {},
|
||||
) => new Promise((resolve, reject) => {
|
||||
if (!state.trees[`${projectId}/${branchId}`]) {
|
||||
const selectedProject = state.projects[projectId];
|
||||
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
|
||||
|
||||
service
|
||||
.getFiles(selectedProject.web_url, branchId)
|
||||
.then(res => res.json())
|
||||
.then((data) => {
|
||||
const worker = new FilesDecoratorWorker();
|
||||
worker.addEventListener('message', (e) => {
|
||||
const { entries, treeList } = e.data;
|
||||
const selectedTree = state.trees[`${projectId}/${branchId}`];
|
||||
|
||||
commit(types.SET_ENTRIES, entries);
|
||||
commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList });
|
||||
commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
|
||||
|
||||
worker.terminate();
|
||||
|
||||
resolve();
|
||||
});
|
||||
|
||||
worker.postMessage({
|
||||
data,
|
||||
projectId,
|
||||
branchId,
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
|
||||
reject(e);
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
export const activeFile = state =>
|
||||
state.openFiles.find(file => file.active) || null;
|
||||
|
||||
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
|
||||
|
||||
export const modifiedFiles = state =>
|
||||
state.changedFiles.filter(f => !f.tempFile);
|
||||
|
||||
export const projectsWithTrees = state =>
|
||||
Object.keys(state.projects).map(projectId => {
|
||||
const project = state.projects[projectId];
|
||||
|
||||
return {
|
||||
...project,
|
||||
branches: Object.keys(project.branches).map(branchId => {
|
||||
const branch = project.branches[branchId];
|
||||
|
||||
return {
|
||||
...branch,
|
||||
tree: state.trees[branch.treeId],
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-confusing-arrow
|
||||
export const currentIcon = state =>
|
||||
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
|
||||
|
||||
export const hasChanges = state => !!state.changedFiles.length;
|
|
@ -0,0 +1,19 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import state from './state';
|
||||
import * as actions from './actions';
|
||||
import * as getters from './getters';
|
||||
import mutations from './mutations';
|
||||
import commitModule from './modules/commit';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: state(),
|
||||
actions,
|
||||
mutations,
|
||||
getters,
|
||||
modules: {
|
||||
commit: commitModule,
|
||||
},
|
||||
});
|
|
@ -0,0 +1,218 @@
|
|||
import $ from 'jquery';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
import flash from '~/flash';
|
||||
import { stripHtml } from '~/lib/utils/text_utility';
|
||||
import * as rootTypes from '../../mutation_types';
|
||||
import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
|
||||
import router from '../../../ide_router';
|
||||
import service from '../../../services';
|
||||
import * as types from './mutation_types';
|
||||
import * as consts from './constants';
|
||||
import eventHub from '../../../eventhub';
|
||||
|
||||
export const updateCommitMessage = ({ commit }, message) => {
|
||||
commit(types.UPDATE_COMMIT_MESSAGE, message);
|
||||
};
|
||||
|
||||
export const discardDraft = ({ commit }) => {
|
||||
commit(types.UPDATE_COMMIT_MESSAGE, '');
|
||||
};
|
||||
|
||||
export const updateCommitAction = ({ commit }, commitAction) => {
|
||||
commit(types.UPDATE_COMMIT_ACTION, commitAction);
|
||||
};
|
||||
|
||||
export const updateBranchName = ({ commit }, branchName) => {
|
||||
commit(types.UPDATE_NEW_BRANCH_NAME, branchName);
|
||||
};
|
||||
|
||||
export const setLastCommitMessage = ({ rootState, commit }, data) => {
|
||||
const currentProject = rootState.projects[rootState.currentProjectId];
|
||||
const commitStats = data.stats
|
||||
? sprintf(__('with %{additions} additions, %{deletions} deletions.'), {
|
||||
additions: data.stats.additions, // eslint-disable-line indent
|
||||
deletions: data.stats.deletions, // eslint-disable-line indent
|
||||
}) // eslint-disable-line indent
|
||||
: '';
|
||||
const commitMsg = sprintf(
|
||||
__('Your changes have been committed. Commit %{commitId} %{commitStats}'),
|
||||
{
|
||||
commitId: `<a href="${currentProject.web_url}/commit/${
|
||||
data.short_id
|
||||
}" class="commit-sha">${data.short_id}</a>`,
|
||||
commitStats,
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
commit(rootTypes.SET_LAST_COMMIT_MSG, commitMsg, { root: true });
|
||||
};
|
||||
|
||||
export const checkCommitStatus = ({ rootState }) =>
|
||||
service
|
||||
.getBranchData(rootState.currentProjectId, rootState.currentBranchId)
|
||||
.then(({ data }) => {
|
||||
const { id } = data.commit;
|
||||
const selectedBranch =
|
||||
rootState.projects[rootState.currentProjectId].branches[
|
||||
rootState.currentBranchId
|
||||
];
|
||||
|
||||
if (selectedBranch.workingReference !== id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.catch(() =>
|
||||
flash(
|
||||
__('Error checking branch data. Please try again.'),
|
||||
'alert',
|
||||
document,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
),
|
||||
);
|
||||
|
||||
export const updateFilesAfterCommit = (
|
||||
{ commit, dispatch, state, rootState, rootGetters },
|
||||
{ data, branch },
|
||||
) => {
|
||||
const selectedProject = rootState.projects[rootState.currentProjectId];
|
||||
const lastCommit = {
|
||||
commit_path: `${selectedProject.web_url}/commit/${data.id}`,
|
||||
commit: {
|
||||
id: data.id,
|
||||
message: data.message,
|
||||
authored_date: data.committed_date,
|
||||
author_name: data.committer_name,
|
||||
},
|
||||
};
|
||||
|
||||
commit(
|
||||
rootTypes.SET_BRANCH_WORKING_REFERENCE,
|
||||
{
|
||||
projectId: rootState.currentProjectId,
|
||||
branchId: rootState.currentBranchId,
|
||||
reference: data.id,
|
||||
},
|
||||
{ root: true },
|
||||
);
|
||||
|
||||
rootState.changedFiles.forEach(entry => {
|
||||
commit(
|
||||
rootTypes.SET_LAST_COMMIT_DATA,
|
||||
{
|
||||
entry,
|
||||
lastCommit,
|
||||
},
|
||||
{ root: true },
|
||||
);
|
||||
|
||||
eventHub.$emit(`editor.update.model.content.${entry.path}`, entry.content);
|
||||
|
||||
commit(
|
||||
rootTypes.SET_FILE_RAW_DATA,
|
||||
{
|
||||
file: entry,
|
||||
raw: entry.content,
|
||||
},
|
||||
{ root: true },
|
||||
);
|
||||
|
||||
commit(
|
||||
rootTypes.TOGGLE_FILE_CHANGED,
|
||||
{
|
||||
file: entry,
|
||||
changed: false,
|
||||
},
|
||||
{ root: true },
|
||||
);
|
||||
});
|
||||
|
||||
commit(rootTypes.REMOVE_ALL_CHANGES_FILES, null, { root: true });
|
||||
|
||||
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) {
|
||||
router.push(
|
||||
`/project/${rootState.currentProjectId}/blob/${branch}/${
|
||||
rootGetters.activeFile.path
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH);
|
||||
};
|
||||
|
||||
export const commitChanges = ({
|
||||
commit,
|
||||
state,
|
||||
getters,
|
||||
dispatch,
|
||||
rootState,
|
||||
}) => {
|
||||
const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
|
||||
const payload = createCommitPayload(
|
||||
getters.branchName,
|
||||
newBranch,
|
||||
state,
|
||||
rootState,
|
||||
);
|
||||
const getCommitStatus = newBranch
|
||||
? Promise.resolve(false)
|
||||
: dispatch('checkCommitStatus');
|
||||
|
||||
commit(types.UPDATE_LOADING, true);
|
||||
|
||||
return getCommitStatus
|
||||
.then(
|
||||
branchChanged =>
|
||||
new Promise(resolve => {
|
||||
if (branchChanged) {
|
||||
// show the modal with a Bootstrap call
|
||||
$('#ide-create-branch-modal').modal('show');
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.then(() => service.commit(rootState.currentProjectId, payload))
|
||||
.then(({ data }) => {
|
||||
commit(types.UPDATE_LOADING, false);
|
||||
|
||||
if (!data.short_id) {
|
||||
flash(data.message, 'alert', document, null, false, true);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch('setLastCommitMessage', data);
|
||||
dispatch('updateCommitMessage', '');
|
||||
|
||||
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) {
|
||||
dispatch(
|
||||
'redirectToUrl',
|
||||
createNewMergeRequestUrl(
|
||||
rootState.projects[rootState.currentProjectId].web_url,
|
||||
getters.branchName,
|
||||
rootState.currentBranchId,
|
||||
),
|
||||
{ root: true },
|
||||
);
|
||||
} else {
|
||||
dispatch('updateFilesAfterCommit', {
|
||||
data,
|
||||
branch: getters.branchName,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
let errMsg = __('Error committing changes. Please try again.');
|
||||
if (err.response.data && err.response.data.message) {
|
||||
errMsg += ` (${stripHtml(err.response.data.message)})`;
|
||||
}
|
||||
flash(errMsg, 'alert', document, null, false, true);
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
|
||||
commit(types.UPDATE_LOADING, false);
|
||||
});
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
export const COMMIT_TO_CURRENT_BRANCH = '1';
|
||||
export const COMMIT_TO_NEW_BRANCH = '2';
|
||||
export const COMMIT_TO_NEW_BRANCH_MR = '3';
|
|
@ -0,0 +1,24 @@
|
|||
import * as consts from './constants';
|
||||
|
||||
export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading;
|
||||
|
||||
export const commitButtonDisabled = (state, getters, rootState) =>
|
||||
getters.discardDraftButtonDisabled || !rootState.changedFiles.length;
|
||||
|
||||
export const newBranchName = (state, _, rootState) =>
|
||||
`${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(-5)}`;
|
||||
|
||||
export const branchName = (state, getters, rootState) => {
|
||||
if (
|
||||
state.commitAction === consts.COMMIT_TO_NEW_BRANCH ||
|
||||
state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR
|
||||
) {
|
||||
if (state.newBranchName === '') {
|
||||
return getters.newBranchName;
|
||||
}
|
||||
|
||||
return state.newBranchName;
|
||||
}
|
||||
|
||||
return rootState.currentBranchId;
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
import state from './state';
|
||||
import mutations from './mutations';
|
||||
import * as actions from './actions';
|
||||
import * as getters from './getters';
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: state(),
|
||||
mutations,
|
||||
actions,
|
||||
getters,
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
export const UPDATE_COMMIT_MESSAGE = 'UPDATE_COMMIT_MESSAGE';
|
||||
export const UPDATE_COMMIT_ACTION = 'UPDATE_COMMIT_ACTION';
|
||||
export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME';
|
||||
export const UPDATE_LOADING = 'UPDATE_LOADING';
|
|
@ -0,0 +1,24 @@
|
|||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
[types.UPDATE_COMMIT_MESSAGE](state, commitMessage) {
|
||||
Object.assign(state, {
|
||||
commitMessage,
|
||||
});
|
||||
},
|
||||
[types.UPDATE_COMMIT_ACTION](state, commitAction) {
|
||||
Object.assign(state, {
|
||||
commitAction,
|
||||
});
|
||||
},
|
||||
[types.UPDATE_NEW_BRANCH_NAME](state, newBranchName) {
|
||||
Object.assign(state, {
|
||||
newBranchName,
|
||||
});
|
||||
},
|
||||
[types.UPDATE_LOADING](state, submitCommitLoading) {
|
||||
Object.assign(state, {
|
||||
submitCommitLoading,
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
export default () => ({
|
||||
commitMessage: '',
|
||||
commitAction: '1',
|
||||
newBranchName: '',
|
||||
submitCommitLoading: false,
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
|
||||
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
|
||||
export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
|
||||
export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG';
|
||||
export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
|
||||
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
|
||||
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
|
||||
|
||||
// Project Mutation Types
|
||||
export const SET_PROJECT = 'SET_PROJECT';
|
||||
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
|
||||
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
|
||||
|
||||
// Branch Mutation Types
|
||||
export const SET_BRANCH = 'SET_BRANCH';
|
||||
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
|
||||
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
|
||||
|
||||
// Tree mutation types
|
||||
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
|
||||
export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
|
||||
export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
|
||||
export const CREATE_TREE = 'CREATE_TREE';
|
||||
export const REMOVE_ALL_CHANGES_FILES = 'REMOVE_ALL_CHANGES_FILES';
|
||||
|
||||
// File mutation types
|
||||
export const SET_FILE_DATA = 'SET_FILE_DATA';
|
||||
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
|
||||
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
|
||||
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
|
||||
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
|
||||
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
|
||||
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
|
||||
export const SET_FILE_EOL = 'SET_FILE_EOL';
|
||||
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
|
||||
export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
|
||||
export const REMOVE_FILE_FROM_CHANGED = 'REMOVE_FILE_FROM_CHANGED';
|
||||
export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
|
||||
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
|
||||
export const SET_ENTRIES = 'SET_ENTRIES';
|
||||
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
|
||||
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
|
||||
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
|
|
@ -0,0 +1,106 @@
|
|||
import * as types from './mutation_types';
|
||||
import projectMutations from './mutations/project';
|
||||
import fileMutations from './mutations/file';
|
||||
import treeMutations from './mutations/tree';
|
||||
import branchMutations from './mutations/branch';
|
||||
|
||||
export default {
|
||||
[types.SET_INITIAL_DATA](state, data) {
|
||||
Object.assign(state, data);
|
||||
},
|
||||
[types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
|
||||
if (entry.path) {
|
||||
Object.assign(state.entries[entry.path], {
|
||||
loading:
|
||||
forceValue !== undefined
|
||||
? forceValue
|
||||
: !state.entries[entry.path].loading,
|
||||
});
|
||||
} else {
|
||||
Object.assign(entry, {
|
||||
loading: forceValue !== undefined ? forceValue : !entry.loading,
|
||||
});
|
||||
}
|
||||
},
|
||||
[types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) {
|
||||
Object.assign(state, {
|
||||
leftPanelCollapsed: collapsed,
|
||||
});
|
||||
},
|
||||
[types.SET_RIGHT_PANEL_COLLAPSED](state, collapsed) {
|
||||
Object.assign(state, {
|
||||
rightPanelCollapsed: collapsed,
|
||||
});
|
||||
},
|
||||
[types.SET_RESIZING_STATUS](state, resizing) {
|
||||
Object.assign(state, {
|
||||
panelResizing: resizing,
|
||||
});
|
||||
},
|
||||
[types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
|
||||
Object.assign(entry.lastCommit, {
|
||||
id: lastCommit.commit.id,
|
||||
url: lastCommit.commit_path,
|
||||
message: lastCommit.commit.message,
|
||||
author: lastCommit.commit.author_name,
|
||||
updatedAt: lastCommit.commit.authored_date,
|
||||
});
|
||||
},
|
||||
[types.SET_LAST_COMMIT_MSG](state, lastCommitMsg) {
|
||||
Object.assign(state, {
|
||||
lastCommitMsg,
|
||||
});
|
||||
},
|
||||
[types.SET_ENTRIES](state, entries) {
|
||||
Object.assign(state, {
|
||||
entries,
|
||||
});
|
||||
},
|
||||
[types.CREATE_TMP_ENTRY](state, { data, projectId, branchId }) {
|
||||
Object.keys(data.entries).reduce((acc, key) => {
|
||||
const entry = data.entries[key];
|
||||
const foundEntry = state.entries[key];
|
||||
|
||||
if (!foundEntry) {
|
||||
Object.assign(state.entries, {
|
||||
[key]: entry,
|
||||
});
|
||||
} else {
|
||||
const tree = entry.tree.filter(
|
||||
f => foundEntry.tree.find(e => e.path === f.path) === undefined,
|
||||
);
|
||||
Object.assign(foundEntry, {
|
||||
tree: foundEntry.tree.concat(tree),
|
||||
});
|
||||
}
|
||||
|
||||
return acc.concat(key);
|
||||
}, []);
|
||||
|
||||
const foundEntry = state.trees[`${projectId}/${branchId}`].tree.find(
|
||||
e => e.path === data.treeList[0].path,
|
||||
);
|
||||
|
||||
if (!foundEntry) {
|
||||
Object.assign(state.trees[`${projectId}/${branchId}`], {
|
||||
tree: state.trees[`${projectId}/${branchId}`].tree.concat(
|
||||
data.treeList,
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
[types.UPDATE_VIEWER](state, viewer) {
|
||||
Object.assign(state, {
|
||||
viewer,
|
||||
});
|
||||
},
|
||||
[types.UPDATE_DELAY_VIEWER_CHANGE](state, delayViewerUpdated) {
|
||||
Object.assign(state, {
|
||||
delayViewerUpdated,
|
||||
});
|
||||
},
|
||||
...projectMutations,
|
||||
...fileMutations,
|
||||
...treeMutations,
|
||||
...branchMutations,
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
import * as types from '../mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_CURRENT_BRANCH](state, currentBranchId) {
|
||||
Object.assign(state, {
|
||||
currentBranchId,
|
||||
});
|
||||
},
|
||||
[types.SET_BRANCH](state, { projectPath, branchName, branch }) {
|
||||
Object.assign(state.projects[projectPath], {
|
||||
branches: {
|
||||
[branchName]: {
|
||||
...branch,
|
||||
treeId: `${projectPath}/${branchName}`,
|
||||
active: true,
|
||||
workingReference: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) {
|
||||
Object.assign(state.projects[projectId].branches[branchId], {
|
||||
workingReference: reference,
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,83 @@
|
|||
import * as types from '../mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_FILE_ACTIVE](state, { path, active }) {
|
||||
Object.assign(state.entries[path], {
|
||||
active,
|
||||
});
|
||||
},
|
||||
[types.TOGGLE_FILE_OPEN](state, path) {
|
||||
Object.assign(state.entries[path], {
|
||||
opened: !state.entries[path].opened,
|
||||
});
|
||||
|
||||
if (state.entries[path].opened) {
|
||||
state.openFiles.push(state.entries[path]);
|
||||
} else {
|
||||
Object.assign(state, {
|
||||
openFiles: state.openFiles.filter(f => f.path !== path),
|
||||
});
|
||||
}
|
||||
},
|
||||
[types.SET_FILE_DATA](state, { data, file }) {
|
||||
Object.assign(state.entries[file.path], {
|
||||
id: data.id,
|
||||
blamePath: data.blame_path,
|
||||
commitsPath: data.commits_path,
|
||||
permalink: data.permalink,
|
||||
rawPath: data.raw_path,
|
||||
binary: data.binary,
|
||||
renderError: data.render_error,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
|
||||
Object.assign(state.entries[file.path], {
|
||||
raw,
|
||||
});
|
||||
},
|
||||
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
|
||||
const changed = content !== state.entries[path].raw;
|
||||
|
||||
Object.assign(state.entries[path], {
|
||||
content,
|
||||
changed,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
|
||||
Object.assign(state.entries[file.path], {
|
||||
fileLanguage,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_EOL](state, { file, eol }) {
|
||||
Object.assign(state.entries[file.path], {
|
||||
eol,
|
||||
});
|
||||
},
|
||||
[types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) {
|
||||
Object.assign(state.entries[file.path], {
|
||||
editorRow,
|
||||
editorColumn,
|
||||
});
|
||||
},
|
||||
[types.DISCARD_FILE_CHANGES](state, path) {
|
||||
Object.assign(state.entries[path], {
|
||||
content: state.entries[path].raw,
|
||||
changed: false,
|
||||
});
|
||||
},
|
||||
[types.ADD_FILE_TO_CHANGED](state, path) {
|
||||
Object.assign(state, {
|
||||
changedFiles: state.changedFiles.concat(state.entries[path]),
|
||||
});
|
||||
},
|
||||
[types.REMOVE_FILE_FROM_CHANGED](state, path) {
|
||||
Object.assign(state, {
|
||||
changedFiles: state.changedFiles.filter(f => f.path !== path),
|
||||
});
|
||||
},
|
||||
[types.TOGGLE_FILE_CHANGED](state, { file, changed }) {
|
||||
Object.assign(state.entries[file.path], {
|
||||
changed,
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
import * as types from '../mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_CURRENT_PROJECT](state, currentProjectId) {
|
||||
Object.assign(state, {
|
||||
currentProjectId,
|
||||
});
|
||||
},
|
||||
[types.SET_PROJECT](state, { projectPath, project }) {
|
||||
// Add client side properties
|
||||
Object.assign(project, {
|
||||
tree: [],
|
||||
branches: {},
|
||||
active: true,
|
||||
});
|
||||
|
||||
Object.assign(state, {
|
||||
projects: Object.assign({}, state.projects, {
|
||||
[projectPath]: project,
|
||||
}),
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
import * as types from '../mutation_types';
|
||||
|
||||
export default {
|
||||
[types.TOGGLE_TREE_OPEN](state, path) {
|
||||
Object.assign(state.entries[path], {
|
||||
opened: !state.entries[path].opened,
|
||||
});
|
||||
},
|
||||
[types.CREATE_TREE](state, { treePath }) {
|
||||
Object.assign(state, {
|
||||
trees: Object.assign({}, state.trees, {
|
||||
[treePath]: {
|
||||
tree: [],
|
||||
loading: true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
[types.SET_DIRECTORY_DATA](state, { data, treePath }) {
|
||||
Object.assign(state, {
|
||||
trees: Object.assign(state.trees, {
|
||||
[treePath]: {
|
||||
tree: data,
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
[types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
|
||||
Object.assign(tree, {
|
||||
lastCommitPath: url,
|
||||
});
|
||||
},
|
||||
[types.REMOVE_ALL_CHANGES_FILES](state) {
|
||||
Object.assign(state, {
|
||||
changedFiles: [],
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
export default () => ({
|
||||
currentProjectId: '',
|
||||
currentBranchId: '',
|
||||
changedFiles: [],
|
||||
endpoints: {},
|
||||
lastCommitMsg: '',
|
||||
lastCommitPath: '',
|
||||
loading: false,
|
||||
openFiles: [],
|
||||
parentTreeUrl: '',
|
||||
trees: {},
|
||||
projects: {},
|
||||
leftPanelCollapsed: false,
|
||||
rightPanelCollapsed: false,
|
||||
panelResizing: false,
|
||||
entries: {},
|
||||
viewer: 'editor',
|
||||
delayViewerUpdated: false,
|
||||
});
|
|
@ -0,0 +1,125 @@
|
|||
export const dataStructure = () => ({
|
||||
id: '',
|
||||
key: '',
|
||||
type: '',
|
||||
projectId: '',
|
||||
branchId: '',
|
||||
name: '',
|
||||
url: '',
|
||||
path: '',
|
||||
tempFile: false,
|
||||
tree: [],
|
||||
loading: false,
|
||||
opened: false,
|
||||
active: false,
|
||||
changed: false,
|
||||
lastCommitPath: '',
|
||||
lastCommit: {
|
||||
id: '',
|
||||
url: '',
|
||||
message: '',
|
||||
updatedAt: '',
|
||||
author: '',
|
||||
},
|
||||
blamePath: '',
|
||||
commitsPath: '',
|
||||
permalink: '',
|
||||
rawPath: '',
|
||||
binary: false,
|
||||
html: '',
|
||||
raw: '',
|
||||
content: '',
|
||||
parentTreeUrl: '',
|
||||
renderError: false,
|
||||
base64: false,
|
||||
editorRow: 1,
|
||||
editorColumn: 1,
|
||||
fileLanguage: '',
|
||||
eol: '',
|
||||
});
|
||||
|
||||
export const decorateData = (entity) => {
|
||||
const {
|
||||
id,
|
||||
projectId,
|
||||
branchId,
|
||||
type,
|
||||
url,
|
||||
name,
|
||||
path,
|
||||
renderError,
|
||||
content = '',
|
||||
tempFile = false,
|
||||
active = false,
|
||||
opened = false,
|
||||
changed = false,
|
||||
parentTreeUrl = '',
|
||||
base64 = false,
|
||||
|
||||
file_lock,
|
||||
|
||||
} = entity;
|
||||
|
||||
return {
|
||||
...dataStructure(),
|
||||
id,
|
||||
projectId,
|
||||
branchId,
|
||||
key: `${name}-${type}-${id}`,
|
||||
type,
|
||||
name,
|
||||
url,
|
||||
path,
|
||||
tempFile,
|
||||
opened,
|
||||
active,
|
||||
parentTreeUrl,
|
||||
changed,
|
||||
renderError,
|
||||
content,
|
||||
base64,
|
||||
|
||||
file_lock,
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
export const findEntry = (tree, type, name, prop = 'name') => tree.find(
|
||||
f => f.type === type && f[prop] === name,
|
||||
);
|
||||
|
||||
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
|
||||
|
||||
export const setPageTitle = (title) => {
|
||||
document.title = title;
|
||||
};
|
||||
|
||||
export const createCommitPayload = (branch, newBranch, state, rootState) => ({
|
||||
branch,
|
||||
commit_message: state.commitMessage,
|
||||
actions: rootState.changedFiles.map(f => ({
|
||||
action: f.tempFile ? 'create' : 'update',
|
||||
file_path: f.path,
|
||||
content: f.content,
|
||||
encoding: f.base64 ? 'base64' : 'text',
|
||||
})),
|
||||
start_branch: newBranch ? rootState.currentBranchId : undefined,
|
||||
});
|
||||
|
||||
export const createNewMergeRequestUrl = (projectUrl, source, target) =>
|
||||
`${projectUrl}/merge_requests/new?merge_request[source_branch]=${source}&merge_request[target_branch]=${target}`;
|
||||
|
||||
const sortTreesByTypeAndName = (a, b) => {
|
||||
if (a.type === 'tree' && b.type === 'blob') {
|
||||
return -1;
|
||||
} else if (a.type === 'blob' && b.type === 'tree') {
|
||||
return 1;
|
||||
}
|
||||
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
|
||||
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, {
|
||||
tree: entity.tree.length ? sortTree(entity.tree) : [],
|
||||
})).sort(sortTreesByTypeAndName);
|
|
@ -0,0 +1,101 @@
|
|||
import { decorateData, sortTree } from '../utils';
|
||||
|
||||
self.addEventListener('message', e => {
|
||||
const {
|
||||
data,
|
||||
projectId,
|
||||
branchId,
|
||||
tempFile = false,
|
||||
content = '',
|
||||
base64 = false,
|
||||
} = e.data;
|
||||
|
||||
const treeList = [];
|
||||
let file;
|
||||
const entries = data.reduce((acc, path) => {
|
||||
const pathSplit = path.split('/');
|
||||
const blobName = pathSplit.pop().trim();
|
||||
|
||||
if (pathSplit.length > 0) {
|
||||
pathSplit.reduce((pathAcc, folderName) => {
|
||||
const parentFolder = acc[pathAcc[pathAcc.length - 1]];
|
||||
const folderPath = `${
|
||||
parentFolder ? `${parentFolder.path}/` : ''
|
||||
}${folderName}`;
|
||||
const foundEntry = acc[folderPath];
|
||||
|
||||
if (!foundEntry) {
|
||||
const tree = decorateData({
|
||||
projectId,
|
||||
branchId,
|
||||
id: folderPath,
|
||||
name: folderName,
|
||||
path: folderPath,
|
||||
url: `/${projectId}/tree/${branchId}/${folderPath}/`,
|
||||
type: 'tree',
|
||||
parentTreeUrl: parentFolder
|
||||
? parentFolder.url
|
||||
: `/${projectId}/tree/${branchId}/`,
|
||||
tempFile,
|
||||
changed: tempFile,
|
||||
opened: tempFile,
|
||||
});
|
||||
|
||||
Object.assign(acc, {
|
||||
[folderPath]: tree,
|
||||
});
|
||||
|
||||
if (parentFolder) {
|
||||
parentFolder.tree.push(tree);
|
||||
} else {
|
||||
treeList.push(tree);
|
||||
}
|
||||
|
||||
pathAcc.push(tree.path);
|
||||
} else {
|
||||
pathAcc.push(foundEntry.path);
|
||||
}
|
||||
|
||||
return pathAcc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
if (blobName !== '') {
|
||||
const fileFolder = acc[pathSplit.join('/')];
|
||||
file = decorateData({
|
||||
projectId,
|
||||
branchId,
|
||||
id: path,
|
||||
name: blobName,
|
||||
path,
|
||||
url: `/${projectId}/blob/${branchId}/${path}`,
|
||||
type: 'blob',
|
||||
parentTreeUrl: fileFolder
|
||||
? fileFolder.url
|
||||
: `/${projectId}/blob/${branchId}`,
|
||||
tempFile,
|
||||
changed: tempFile,
|
||||
content,
|
||||
base64,
|
||||
});
|
||||
|
||||
Object.assign(acc, {
|
||||
[path]: file,
|
||||
});
|
||||
|
||||
if (fileFolder) {
|
||||
fileFolder.tree.push(file);
|
||||
} else {
|
||||
treeList.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
self.postMessage({
|
||||
entries,
|
||||
treeList: sortTree(treeList),
|
||||
file,
|
||||
});
|
||||
});
|
|
@ -1,28 +1,25 @@
|
|||
/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
|
||||
|
||||
import $ from 'jquery';
|
||||
import { insertText } from '~/lib/utils/common_utils';
|
||||
|
||||
const textUtils = {};
|
||||
|
||||
textUtils.selectedText = function(text, textarea) {
|
||||
function selectedText(text, textarea) {
|
||||
return text.substring(textarea.selectionStart, textarea.selectionEnd);
|
||||
};
|
||||
}
|
||||
|
||||
textUtils.lineBefore = function(text, textarea) {
|
||||
function lineBefore(text, textarea) {
|
||||
var split;
|
||||
split = text.substring(0, textarea.selectionStart).trim().split('\n');
|
||||
return split[split.length - 1];
|
||||
};
|
||||
}
|
||||
|
||||
textUtils.lineAfter = function(text, textarea) {
|
||||
function lineAfter(text, textarea) {
|
||||
return text.substring(textarea.selectionEnd).trim().split('\n')[0];
|
||||
};
|
||||
}
|
||||
|
||||
textUtils.blockTagText = function(text, textArea, blockTag, selected) {
|
||||
var lineAfter, lineBefore;
|
||||
lineBefore = this.lineBefore(text, textArea);
|
||||
lineAfter = this.lineAfter(text, textArea);
|
||||
if (lineBefore === blockTag && lineAfter === blockTag) {
|
||||
function blockTagText(text, textArea, blockTag, selected) {
|
||||
const before = lineBefore(text, textArea);
|
||||
const after = lineAfter(text, textArea);
|
||||
if (before === blockTag && after === blockTag) {
|
||||
// To remove the block tag we have to select the line before & after
|
||||
if (blockTag != null) {
|
||||
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
|
||||
|
@ -32,10 +29,30 @@ textUtils.blockTagText = function(text, textArea, blockTag, selected) {
|
|||
} else {
|
||||
return blockTag + "\n" + selected + "\n" + blockTag;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
|
||||
var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
|
||||
function moveCursor(textArea, tag, wrapped, removedLastNewLine) {
|
||||
var pos;
|
||||
if (!textArea.setSelectionRange) {
|
||||
return;
|
||||
}
|
||||
if (textArea.selectionStart === textArea.selectionEnd) {
|
||||
if (wrapped) {
|
||||
pos = textArea.selectionStart - tag.length;
|
||||
} else {
|
||||
pos = textArea.selectionStart;
|
||||
}
|
||||
|
||||
if (removedLastNewLine) {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
return textArea.setSelectionRange(pos, pos);
|
||||
}
|
||||
}
|
||||
|
||||
export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap) {
|
||||
var textToInsert, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
|
||||
removedLastNewLine = false;
|
||||
removedFirstNewLine = false;
|
||||
currentLineEmpty = false;
|
||||
|
@ -67,9 +84,9 @@ textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
|
|||
|
||||
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
|
||||
if (blockTag != null && blockTag !== '') {
|
||||
insertText = this.blockTagText(text, textArea, blockTag, selected);
|
||||
textToInsert = blockTagText(text, textArea, blockTag, selected);
|
||||
} else {
|
||||
insertText = selectedSplit.map(function(val) {
|
||||
textToInsert = selectedSplit.map(function(val) {
|
||||
if (val.indexOf(tag) === 0) {
|
||||
return "" + (val.replace(tag, ''));
|
||||
} else {
|
||||
|
@ -78,78 +95,42 @@ textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
|
|||
}).join('\n');
|
||||
}
|
||||
} else {
|
||||
insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
|
||||
textToInsert = "" + startChar + tag + selected + (wrap ? tag : ' ');
|
||||
}
|
||||
|
||||
if (removedFirstNewLine) {
|
||||
insertText = '\n' + insertText;
|
||||
textToInsert = '\n' + textToInsert;
|
||||
}
|
||||
|
||||
if (removedLastNewLine) {
|
||||
insertText += '\n';
|
||||
textToInsert += '\n';
|
||||
}
|
||||
|
||||
if (document.queryCommandSupported('insertText')) {
|
||||
inserted = document.execCommand('insertText', false, insertText);
|
||||
}
|
||||
if (!inserted) {
|
||||
try {
|
||||
document.execCommand("ms-beginUndoUnit");
|
||||
} catch (error) {}
|
||||
textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
|
||||
try {
|
||||
document.execCommand("ms-endUndoUnit");
|
||||
} catch (error) {}
|
||||
}
|
||||
return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
|
||||
};
|
||||
insertText(textArea, textToInsert);
|
||||
return moveCursor(textArea, tag, wrap, removedLastNewLine);
|
||||
}
|
||||
|
||||
textUtils.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
|
||||
var pos;
|
||||
if (!textArea.setSelectionRange) {
|
||||
return;
|
||||
}
|
||||
if (textArea.selectionStart === textArea.selectionEnd) {
|
||||
if (wrapped) {
|
||||
pos = textArea.selectionStart - tag.length;
|
||||
} else {
|
||||
pos = textArea.selectionStart;
|
||||
}
|
||||
|
||||
if (removedLastNewLine) {
|
||||
pos -= 1;
|
||||
}
|
||||
|
||||
return textArea.setSelectionRange(pos, pos);
|
||||
}
|
||||
};
|
||||
|
||||
textUtils.updateText = function(textArea, tag, blockTag, wrap) {
|
||||
function updateText(textArea, tag, blockTag, wrap) {
|
||||
var $textArea, selected, text;
|
||||
$textArea = $(textArea);
|
||||
textArea = $textArea.get(0);
|
||||
text = $textArea.val();
|
||||
selected = this.selectedText(text, textArea);
|
||||
selected = selectedText(text, textArea);
|
||||
$textArea.focus();
|
||||
return this.insertText(textArea, text, tag, blockTag, selected, wrap);
|
||||
};
|
||||
return insertMarkdownText(textArea, text, tag, blockTag, selected, wrap);
|
||||
}
|
||||
|
||||
textUtils.init = function(form) {
|
||||
var self;
|
||||
self = this;
|
||||
return $('.js-md', form).off('click').on('click', function() {
|
||||
var $this;
|
||||
$this = $(this);
|
||||
return self.updateText($this.closest('.md-area').find('textarea'), $this.data('mdTag'), $this.data('mdBlock'), !$this.data('mdPrepend'));
|
||||
});
|
||||
};
|
||||
|
||||
textUtils.removeListeners = function(form) {
|
||||
return $('.js-md', form).off('click');
|
||||
};
|
||||
|
||||
textUtils.replaceRange = function(s, start, end, substitute) {
|
||||
function replaceRange(s, start, end, substitute) {
|
||||
return s.substring(0, start) + substitute + s.substring(end);
|
||||
};
|
||||
}
|
||||
|
||||
export default textUtils;
|
||||
export function addMarkdownListeners(form) {
|
||||
return $('.js-md', form).off('click').on('click', function() {
|
||||
const $this = $(this);
|
||||
return updateText($this.closest('.md-area').find('textarea'), $this.data('mdTag'), $this.data('mdBlock'), !$this.data('mdPrepend'));
|
||||
});
|
||||
}
|
||||
|
||||
export function removeMarkdownListeners(form) {
|
||||
return $('.js-md', form).off('click');
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
/* eslint-disable import/first */
|
||||
/* global ConfirmDangerModal */
|
||||
/* global $ */
|
||||
|
||||
import jQuery from 'jquery';
|
||||
|
@ -21,7 +20,6 @@ import './behaviors/';
|
|||
// everything else
|
||||
import loadAwardsHandler from './awards_handler';
|
||||
import bp from './breakpoints';
|
||||
import './confirm_danger_modal';
|
||||
import Flash, { removeFlashClickListener } from './flash';
|
||||
import './gl_dropdown';
|
||||
import initTodoToggle from './header';
|
||||
|
@ -32,7 +30,6 @@ import LazyLoader from './lazy_loader';
|
|||
import initLogoAnimation from './logo';
|
||||
import './milestone_select';
|
||||
import './projects_dropdown';
|
||||
import './render_gfm';
|
||||
import initBreadcrumbs from './breadcrumb';
|
||||
|
||||
import initDispatcher from './dispatcher';
|
||||
|
@ -215,16 +212,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
$(document).trigger('toggle.comments');
|
||||
});
|
||||
|
||||
$document.on('click', '.js-confirm-danger', (e) => {
|
||||
const btn = $(e.target);
|
||||
const form = btn.closest('form');
|
||||
const text = btn.data('confirmDangerMessage');
|
||||
e.preventDefault();
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new ConfirmDangerModal(form, text);
|
||||
});
|
||||
|
||||
$document.on('breakpoint:change', (e, breakpoint) => {
|
||||
if (breakpoint === 'sm' || breakpoint === 'xs') {
|
||||
const $gutterIcon = $sidebarGutterToggle.find('i');
|
||||
|
|
|
@ -73,6 +73,10 @@
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emptyNoDataSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emptyUnableToConnectSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -188,6 +192,7 @@
|
|||
:clusters-path="clustersPath"
|
||||
:empty-getting-started-svg-path="emptyGettingStartedSvgPath"
|
||||
:empty-loading-svg-path="emptyLoadingSvgPath"
|
||||
:empty-no-data-svg-path="emptyNoDataSvgPath"
|
||||
:empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -27,6 +27,10 @@
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emptyNoDataSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
emptyUnableToConnectSvgPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -54,7 +58,7 @@
|
|||
buttonPath: this.documentationPath,
|
||||
},
|
||||
noData: {
|
||||
svgUrl: this.emptyUnableToConnectSvgPath,
|
||||
svgUrl: this.emptyNoDataSvgPath,
|
||||
title: 'No data found',
|
||||
description: `You are connected to the Prometheus server, but there is currently
|
||||
no data to display.`,
|
||||
|
|
|
@ -105,6 +105,9 @@ export default class Notes {
|
|||
this.basePollingInterval = 15000;
|
||||
this.maxPollingSteps = 4;
|
||||
|
||||
this.$wrapperEl = hasVueMRDiscussionsCookie()
|
||||
? $(document).find('.diffs')
|
||||
: $(document);
|
||||
this.cleanBinding();
|
||||
this.addBinding();
|
||||
this.setPollingInterval();
|
||||
|
@ -138,10 +141,6 @@ export default class Notes {
|
|||
}
|
||||
|
||||
addBinding() {
|
||||
this.$wrapperEl = hasVueMRDiscussionsCookie()
|
||||
? $(document).find('.diffs')
|
||||
: $(document);
|
||||
|
||||
// Edit note link
|
||||
this.$wrapperEl.on('click', '.js-note-edit', this.showEditForm.bind(this));
|
||||
this.$wrapperEl.on('click', '.note-edit-cancel', this.cancelEdit);
|
||||
|
@ -226,14 +225,9 @@ export default class Notes {
|
|||
$(window).on('hashchange', this.onHashChange);
|
||||
this.boundGetContent = this.getContent.bind(this);
|
||||
document.addEventListener('refreshLegacyNotes', this.boundGetContent);
|
||||
this.eventsBound = true;
|
||||
}
|
||||
|
||||
cleanBinding() {
|
||||
if (!this.eventsBound) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$wrapperEl.off('click', '.js-note-edit');
|
||||
this.$wrapperEl.off('click', '.note-edit-cancel');
|
||||
this.$wrapperEl.off('click', '.js-note-delete');
|
||||
|
@ -1733,6 +1727,7 @@ export default class Notes {
|
|||
|
||||
// Get Form metadata
|
||||
const $submitBtn = $(e.target);
|
||||
$submitBtn.prop('disabled', true);
|
||||
let $form = $submitBtn.parents('form');
|
||||
const $closeBtn = $form.find('.js-note-target-close');
|
||||
const isDiscussionNote =
|
||||
|
@ -1767,7 +1762,6 @@ export default class Notes {
|
|||
// If comment is to resolve discussion, disable submit buttons while
|
||||
// comment posting is finished.
|
||||
if (isDiscussionResolve) {
|
||||
$submitBtn.disable();
|
||||
$form.find('.js-comment-submit-button').disable();
|
||||
}
|
||||
|
||||
|
@ -1815,13 +1809,16 @@ export default class Notes {
|
|||
}
|
||||
}
|
||||
|
||||
$closeBtn.text($closeBtn.data('originalText'));
|
||||
|
||||
/* eslint-disable promise/catch-or-return */
|
||||
// Make request to submit comment on server
|
||||
axios
|
||||
return axios
|
||||
.post(`${formAction}?html=true`, formData)
|
||||
.then(res => {
|
||||
const note = res.data;
|
||||
|
||||
$submitBtn.prop('disabled', false);
|
||||
// Submission successful! remove placeholder
|
||||
$notesContainer.find(`#${noteUniqueId}`).remove();
|
||||
|
||||
|
@ -1905,7 +1902,7 @@ export default class Notes {
|
|||
.catch(() => {
|
||||
// Submission failed, remove placeholder note and show Flash error message
|
||||
$notesContainer.find(`#${noteUniqueId}`).remove();
|
||||
|
||||
$submitBtn.prop('disabled', false);
|
||||
const blurEvent = new CustomEvent('blur.imageDiff', {
|
||||
detail: e,
|
||||
});
|
||||
|
@ -1933,8 +1930,6 @@ export default class Notes {
|
|||
this.reenableTargetFormSubmitButton(e);
|
||||
this.addNoteError($form);
|
||||
});
|
||||
|
||||
return $closeBtn.text($closeBtn.data('originalText'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import initSettingsPanels from '~/settings_panels';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize expandable settings panels
|
||||
initSettingsPanels();
|
||||
});
|
|
@ -1,11 +1,11 @@
|
|||
<script>
|
||||
import _ from 'underscore';
|
||||
import modal from '~/vue_shared/components/modal.vue';
|
||||
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
modal,
|
||||
DeprecatedModal,
|
||||
},
|
||||
props: {
|
||||
deleteProjectUrl: {
|
||||
|
@ -79,7 +79,7 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<modal
|
||||
<deprecated-modal
|
||||
id="delete-project-modal"
|
||||
:title="title"
|
||||
:text="text"
|
||||
|
@ -121,5 +121,5 @@
|
|||
/>
|
||||
</form>
|
||||
</template>
|
||||
</modal>
|
||||
</deprecated-modal>
|
||||
</template>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<script>
|
||||
import _ from 'underscore';
|
||||
import modal from '~/vue_shared/components/modal.vue';
|
||||
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
modal,
|
||||
DeprecatedModal,
|
||||
},
|
||||
props: {
|
||||
deleteUserUrl: {
|
||||
|
@ -113,7 +113,7 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<modal
|
||||
<deprecated-modal
|
||||
id="delete-user-modal"
|
||||
:title="title"
|
||||
:text="text"
|
||||
|
@ -170,5 +170,5 @@
|
|||
{{ secondaryButtonLabel }}
|
||||
</button>
|
||||
</template>
|
||||
</modal>
|
||||
</deprecated-modal>
|
||||
</template>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import groupAvatar from '~/group_avatar';
|
||||
import TransferDropdown from '~/groups/transfer_dropdown';
|
||||
import initConfirmDangerModal from '~/confirm_danger_modal';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
groupAvatar();
|
||||
new TransferDropdown(); // eslint-disable-line no-new
|
||||
initConfirmDangerModal();
|
||||
});
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
import Flash from '~/flash';
|
||||
import modal from '~/vue_shared/components/modal.vue';
|
||||
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
|
||||
import { n__, s__, sprintf } from '~/locale';
|
||||
import { redirectTo } from '~/lib/utils/url_utility';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
modal,
|
||||
DeprecatedModal,
|
||||
},
|
||||
props: {
|
||||
issueCount: {
|
||||
|
@ -92,7 +92,7 @@ Once deleted, it cannot be undone or recovered.`),
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<modal
|
||||
<deprecated-modal
|
||||
id="delete-milestone-modal"
|
||||
:title="title"
|
||||
:text="text"
|
||||
|
@ -106,5 +106,5 @@ Once deleted, it cannot be undone or recovered.`),
|
|||
<p v-html="props.text"></p>
|
||||
</template>
|
||||
|
||||
</modal>
|
||||
</deprecated-modal>
|
||||
</template>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue