Merge branch 'master' into build-chunks-on-object-storage

This commit is contained in:
Shinya Maeda 2018-06-25 16:21:13 +09:00
commit 6c3eea0db0
876 changed files with 13302 additions and 5271 deletions

View File

@ -815,8 +815,6 @@ lint:javascript:report:
- setup-test-env
before_script: []
script:
- date
- find app/ spec/ -name '*.js' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files
- date
- yarn run eslint-report || true # ignore exit code
artifacts:

View File

@ -2,6 +2,246 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 11.0.0 (2018-06-22)
### Security (3 changes)
- Fix API to remove deploy key from project instead of deleting it entirely.
- Fixed bug that allowed importing arbitrary project attributes.
- Prevent user passwords from being changed without providing the previous password.
### Removed (2 changes)
- Removed API v3 from the codebase. !18970
- Removes outdated `g t` shortcut for TODO in favor of `Shift+T`. !19002
### Fixed (69 changes, 23 of them are from the community)
- Optimize the upload migration proces. !15947
- Import bitbucket issues that are reported by an anonymous user. !18199 (bartl)
- Fix an issue where the notification email address would be set to an unconfirmed email address. !18474
- Stop logging email information when emails are disabled. !18521 (Marc Shaw)
- Fix double-brackets being linkified in wiki markdown. !18524 (brewingcode)
- Use case in-sensitive ordering by name for dashboard. !18553 (@vedharish)
- Fix width of contributors graphs. !18639 (Paul Vorbach)
- Fix modal width of shorcuts help page. !18766 (Lars Greiss)
- Add missing tooltip to creation date on container registry overview. !18767 (Lars Greiss)
- Add missing migration for minimal Project build_timeout. !18775
- Update commit status from external CI services less aggressively. !18802
- Fix Runner contacted at tooltip cache. !18810
- Added support for LFS Download in the importing process. !18871
- Fix issue board bug with long strings in titles. !18924
- Does not log failed sign-in attempts when the database is in read-only mode. !18957
- Fixes 500 error on /estimate BIG_VALUE. !18964 (Jacopo Beschi @jacopo-beschi)
- Forbid to patch traces for finished jobs. !18969
- Do not allow to trigger manual actions that were skipped. !18985
- Renamed 'Overview' to 'Project' in collapsed contextual navigation at a project level. !18996 (Constance Okoghenun)
- Fixed bug where generated api urls didn't add the base url if set. !19003
- Fixed badge api endpoint route when relative url is set. !19004
- Fixes: Runners search input placeholder is cut off. !19015 (Jacopo Beschi @jacopo-beschi)
- Exclude CI_PIPELINE_ID from variables supported in dynamic environment name. !19032
- Updates updated_at on label changes. !19065 (Jacopo Beschi @jacopo-beschi)
- Disallow updating job status if the job is not running. !19101
- Fix FreeBSD can not upload artifacts due to wrong tmp path. !19148
- Check for nil AutoDevOps when saving project CI/CD settings. !19190
- Missing timeout value in object storage pre-authorization. !19201
- Use strings as properties key in kubernetes service spec. !19265 (Jasper Maes)
- Fixed HTTP_PROXY environment not honored when reading remote traces. !19282 (NLR)
- Updates ReactiveCaching clear_reactive_caching method to clear both data and alive caching. !19311
- Fixes the styling on the modal headers. !19312 (samdbeckham)
- Fixes a spelling error on the new label page. !19316 (samdbeckham)
- Rails5 fix arel from. !19340 (Jasper Maes)
- Support rails5 in postgres indexes function and fix some migrations. !19400 (Jasper Maes)
- Fix repository archive generation when hashed storage is enabled. !19441
- Rails 5 fix unknown keywords: changes, key_id, project, gl_repository, action, secret_token, protocol. !19466 (Jasper Maes)
- Rails 5 fix glob spec. !19469 (Jasper Maes)
- Showing project import_status in a humanized form no longer gives an error. !19470
- Make avatars/icons hidden on mobile. !19585 (Takuya Noguchi)
- Fix active tab highlight when creating new merge request. !19781 (Jan Beckmann)
- Fixes Web IDE button on merge requests when GitLab is installed with relative URL.
- Unverified hover state color changed to black.
- Fix   after sign-in with Google button.
- Don't trim incoming emails that create new issues. (Cameron Crockett)
- Wrapping problem on the issues page has been fixed.
- Fix resolvable check if note's commit could not be found.
- Fix filename matching when processing file or blob search results.
- Allow maintainers to retry pipelines on forked projects (if allowed in merge request).
- Fix deletion of Object Store uploads.
- Fix overflowing Failed Jobs table in sm viewports on IE11.
- Adjust insufficient diff hunks being persisted on NoteDiffFile.
- Render calendar feed inline when accessed from GitLab.
- Line height fixed. (Murat Dogan)
- Use upload ID for creating lease key for file uploaders.
- Use Github repo visibility during import while respecting restricted visibility levels.
- Adjust permitted params filtering on merge scheduling.
- Fix unscrollable Markdown preview of WebIDE on Firefox.
- Enforce UTF-8 encoding on user input in LogrageWithTimestamp formatter and filter out file content from logs.
- Fix project destruction failing due to idle in transaction timeouts.
- Add a unique and not null constraint on the project_features.project_id column.
- Expire Wiki content cache after importing a repository.
- Fix admin counters not working when PostgreSQL has secondaries.
- Fix backup creation and restore for specific Rake tasks.
- Fix cross-origin errors when attempting to download JavaScript attachments.
- Fix api_json.log not always reporting the right HTTP status code.
- Fix attr_encryption key settings.
- Remove gray button styles.
- Fix print styles for markdown pages.
### Deprecated (4 changes)
- Deprecate Gemnasium project service. !18954
- Rephrasing Merge Request's 'allow edits from maintainer' functionality. !19061
- Rename issue scope created-by-me to created_by_me, and assigned-to-me to assigned_to_me. !44799
- Migrate any remaining jobs from deprecated `object_storage_upload` queue.
### Changed (42 changes, 11 of them are from the community)
- Add support for smarter system notes. !17164
- Automatically accepts project/group invite by email after user signup. !17634 (Jacopo Beschi @jacopo-beschi)
- Dynamically fetch GCP cluster creation parameters. !17806
- Label list page redesign. !18466
- Move discussion actions to the right for small viewports. !18476 (George Tsiolis)
- Add 2FA filter to the group members page. !18483
- made listing and showing public issue apis available without authentication. !18638 (haseebeqx)
- Refactoring UrlValidators to include url blocking. !18686
- Removed "(Beta)" from "Auto DevOps" messages. !18759
- Expose runner ip address to runners API. !18799 (Lars Greiss)
- Moves MR widget external link icon to the right. !18828 (Jacopo Beschi @jacopo-beschi)
- Add support for 'active' setting on Runner Registration API endpoint. !18848
- Add dot to separate system notes content. !18864
- Remove modalbox confirmation when retrying a pipeline. !18879
- Remove docker pull prefix from registry clipboard feature. !18933 (Lars Greiss)
- Move project sidebar sub-entries 'Environments' and 'Kubernetes' from 'CI/CD' to a new entry 'Operations'. !18941
- Updated icons for branch and tag names in commit details. !18953 (Constance Okoghenun)
- Expose readme url in Project API. !18960 (Imre Farkas)
- Changes keyboard shortcut of Activity feed to `g v`. !19002
- Updated Mattermost integration to use API v4 and only allow creation of Mattermost slash commands in the current user's teams. !19043 (Harrison Healey)
- Add shortcuts to Web IDE docs and modal. !19044
- Rename merge request widget author component. !19079 (George Tsiolis)
- Rename the Master role to Maintainer. !19080
- Use "right now" for short time periods. !19095
- Update 404 and 403 pages with helpful actions. !19096
- Add username to terms message in git and API calls. !19126
- Change the IDE file buttons for an "Open in file view" button. !19129 (Sam Beckham)
- Removes redundant script failure message from Job page. !19138
- Add flash notice if user has already accepted terms and allow users to continue to root path. !19156
- Redesign group settings page into expandable sections. !19184
- Hashed Storage: migration rake task now can be executed to specific project. !19268
- Make CI job update entrypoint to work as keep-alive endpoint. !19543
- Avoid checking the user format in every url validation. !19575
- Apply notification settings level of groups to all child objects.
- Support restoring repositories into gitaly.
- Bump omniauth-gitlab to 1.0.3.
- Move API group deletion to Sidekiq.
- Improve Failed Jobs tab in the Pipeline detail page.
- Add additional theme color options.
- Include milestones from parent groups when assigning a milestone to an issue or merge request.
- Restore API v3 user endpoint.
- Hide merge request option in IDE when disabled.
### Performance (28 changes, 1 of them is from the community)
- Add backgound migration for filling nullfied file_store columns. !18557
- Add a cronworker to rescue stale live traces. !18680
- Move SquashBeforeMerge vue component. !18813 (George Tsiolis)
- Add index on runner_type for ci_runners. !18897
- Fix CarrierWave reads local files into memoery when migrates to ObjectStorage. !19102
- Remove double-checked internal id generation. !19181
- Throttle updates to Project#last_repository_updated_at. !19183
- Add background migrations for archiving legacy job traces. !19194
- Use NPM provided version of SortableJS. !19274
- Improve performance of group issues filtering on GitLab.com. !19429
- Improve performance of LFS integrity check. !19494
- Fix an N+1 when loading user avatars.
- Only preload member records for the relevant projects/groups/user in projects API.
- Fix some sources of excessive query counts when calculating notification recipients.
- Optimise PagesWorker usage.
- Optimise paused runners to reduce amount of used requests.
- Update runner cached informations without performing validations.
- Improve performance of project pipelines pages.
- Persist truncated note diffs on a new table.
- Remove unused running_or_pending_build_count.
- Remove N+1 query for author in issues API.
- Eliminate N+1 queries with authors and push_data_payload in Events API.
- Eliminate cached N+1 queries for projects in Issue API.
- Eliminate N+1 queries for CI job artifacts in /api/prjoects/:id/pipelines/:pipeline_id/jobs.
- Fix N+1 with source_projects in merge requests API.
- Replace grape-route-helpers with our own grape-path-helpers.
- Move PR IO operations out of a transaction.
- Improve performance of GroupsController#show.
### Added (25 changes, 10 of them are from the community)
- Closes MR check out branch modal with escape. (19050)
- Allow changing the default favicon to a custom icon. !14497 (Alexis Reigel)
- Export assigned issues in iCalendar feed. !17783 (Imre Farkas)
- When MR becomes unmergeable, notify and create todo for author and merge user. !18042
- Display help text below auto devops domain with nip.io domain name (#45561). !18496
- Add per-project pipeline id. !18558
- New design for wiki page deletion confirmation. !18712 (Constance Okoghenun)
- Updates updated_at on issuable when setting time spent. !18757 (Jacopo Beschi @jacopo-beschi)
- Expose artifacts_expire_at field for job entity in api. !18872 (Semyon Pupkov)
- Add support for variables expression pattern matching syntax. !18902
- Add API endpoint to render markdown text. !18926 (@blackst0ne)
- Add `Squash and merge` to GitLab Core (CE). !18956 (@blackst0ne)
- Adds keyboard shortcut `g k` for Kubernetes on Project pages. !19002
- Adds keyboard shortcut `g e` for Environments on Project pages. !19002
- Setup graphql with initial project & merge request query. !19008
- Adds JupyterHub to cluster applications. !19019
- Added ability to search by wiki titles. !19112
- Add Avatar API. !19121 (Imre Farkas)
- Add variables to POST api/v4/projects/:id/pipeline. !19124 (Jacopo Beschi @jacopo-beschi)
- Add deploy strategies to the Auto DevOps settings. !19172
- Automatize Deploy Token creation for Auto Devops. !19507
- Add anchor for incoming email regex.
- Support direct_upload with S3 Multipart uploads.
- Add Open in Xcode link for xcode repositories.
- Add pipeline status to the status bar of the Web IDE.
### Other (40 changes, 17 of them are from the community)
- Expand documentation for Runners API. !16484
- Order UsersController#projects.json by updated_at. !18227 (Takuya Noguchi)
- Replace the `project/issues/references.feature` spinach test with an rspec analog. !18769 (@blackst0ne)
- Replace the `project/merge_requests/references.feature` spinach test with an rspec analog. !18794 (@blackst0ne)
- Replace the `project/deploy_keys.feature` spinach test with an rspec analog. !18796 (@blackst0ne)
- Replace the `project/ff_merge_requests.feature` spinach test with an rspec analog. !18800 (@blackst0ne)
- Apply NestingDepth (level 5) (pages/pipelines.scss). !18830 (Takuya Noguchi)
- Replace the `project/forked_merge_requests.feature` spinach test with an rspec analog. !18867 (@blackst0ne)
- Remove Spinach. !18869 (@blackst0ne)
- Add NOT NULL constraints to project_authorizations. !18980
- Add helpful messages to empty wiki view. !19007
- Increase text limit for GPG keys (mysql only). !19069
- Take two for MR metrics population background migration. !19097
- Remove Gemnasium badge from project README.md. !19136 (Takuya Noguchi)
- Update awesome_print to 1.8.0. !19163 (Takuya Noguchi)
- Update email_spec to 2.2.0. !19164 (Takuya Noguchi)
- Update redis-namespace to 1.6.0. !19166 (Takuya Noguchi)
- Update rdoc to 6.0.4. !19167 (Takuya Noguchi)
- Updates the version of kubeclient from 3.0 to 3.1.0. !19199
- Fix UI broken in line profiling modal due to Bootstrap 4. !19253 (Takuya Noguchi)
- Add migration to disable the usage of DSA keys. !19299
- Use the default strings of timeago.js for timeago. !19350 (Takuya Noguchi)
- Update selenium-webdriver to 3.12.0. !19351 (Takuya Noguchi)
- Include username in output when testing SSH to GitLab. !19358
- Update screenshot in Gitlab.com integration documentation. !19433 (Tuğçe Nur Taş)
- Users can accept terms during registration. !19583
- Fix issue count on sidebar.
- Add merge requests list endpoint for groups.
- Upgrade GitLab from Bootstrap 3 to 4.
- Make ActiveRecordSubscriber rails 5 compatible.
- Show a more helpful error for import status.
- Log response body to production_json.log when a controller responds with a 422 status.
- Log Workhorse queue duration for Grape API calls.
- Adjust SQL and transaction Prometheus buckets.
- Adding branches through the WebUI is handled by Gitaly.
- Remove shellout implementation for Repository checksums.
- Refs containting sha checks are done by Gitaly.
- Finding a wiki page is done by Gitaly by default.
- Workhorse will use Gitaly to create archives.
- Workhorse to send raw diff and patch for commits.
## 10.8.4 (2018-06-06)
- No changes.

View File

@ -27,25 +27,26 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Helping others](#helping-others)
- [I want to contribute!](#i-want-to-contribute)
- [Workflow labels](#workflow-labels)
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.)](#team-labels-cicd-discussion-quality-platform-etc)
- [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release)
- [Priority labels (~P1, ~P2, ~P3 , ~P4)](#bug-priority-labels-p1-p2-p3-p4)
- [Severity labels (~S1, ~S2, ~S3 , ~S4)](#bug-severity-labels-s1-s2-s3-s4)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design--ui-elements)
- [Type labels](#type-labels)
- [Subject labels](#subject-labels)
- [Team labels](#team-labels)
- [Release Scoping labels](#release-scoping-labels)
- [Bug Priority labels](#bug-priority-labels)
- [Bug Severity labels](#bug-severity-labels)
- [Severity impact guidance](#severity-impact-guidance)
- [Label for community contributors](#label-for-community-contributors)
- [Implement design & UI elements](#implement-design-ui-elements)
- [Issue tracker](#issue-tracker)
- [Issue triaging](#issue-triaging)
- [Feature proposals](#feature-proposals)
- [Issue tracker guidelines](#issue-tracker-guidelines)
- [Issue weight](#issue-weight)
- [Regression issues](#regression-issues)
- [Technical and UX debt](#technical-and-ux-debt)
- [Stewardship](#stewardship)
- [Issue triaging](#issue-triaging)
- [Feature proposals](#feature-proposals)
- [Issue tracker guidelines](#issue-tracker-guidelines)
- [Issue weight](#issue-weight)
- [Regression issues](#regression-issues)
- [Technical and UX debt](#technical-and-ux-debt)
- [Stewardship](#stewardship)
- [Merge requests](#merge-requests)
- [Merge request guidelines](#merge-request-guidelines)
- [Contribution acceptance criteria](#contribution-acceptance-criteria)
- [Merge request guidelines](#merge-request-guidelines)
- [Contribution acceptance criteria](#contribution-acceptance-criteria)
- [Definition of done](#definition-of-done)
- [Style guides](#style-guides)
- [Code of conduct](#code-of-conduct)
@ -132,7 +133,7 @@ Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, etc.
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.
- Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Release Scoping: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Priority: ~P1, ~P2, ~P3, ~P4
- Severity: ~S1, ~S2, ~S3, ~S4
@ -145,7 +146,7 @@ labels, you can _always_ add the team and type, and often also the subject.
[milestones-page]: https://gitlab.com/gitlab-org/gitlab-ce/milestones
[labels-page]: https://gitlab.com/gitlab-org/gitlab-ce/labels
### Type labels (~"feature proposal", ~bug, ~customer, etc.)
### Type labels
Type labels are very important. They define what kind of issue this is. Every
issue should have one or more.
@ -161,28 +162,41 @@ already reserved for subject labels).
The descriptions on the [labels page][labels-page] explain what falls under each type label.
### Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)
### Subject labels
Subject labels are labels that define what area or feature of GitLab this issue
hits. They are not always necessary, but very convenient.
Examples of subject labels are ~wiki, ~ldap, ~api,
~issues, ~"merge requests", ~labels, and ~"container registry".
If you are an expert in a particular area, it makes it easier to find issues to
work on. You can also subscribe to those labels to receive an email each time an
issue is labeled with a subject label corresponding to your expertise.
Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api,
~issues, ~"merge requests", ~labels, and ~"container registry".
Subject labels are always all-lowercase.
### Team labels (~"CI/CD", ~Discussion, ~Quality, ~Platform, etc.)
### Team labels
Team labels specify what team is responsible for this issue.
Assigning a team label makes sure issues get the attention of the appropriate
people.
The current team labels are ~Distribution, ~"CI/CD", ~Discussion, ~Documentation, ~Quality,
~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products", ~"Configuration", and ~"UX".
The current team labels are:
- ~Configuration
- ~"CI/CD"
- ~Discussion
- ~Distribution
- ~Documentation
- ~Geo
- ~Gitaly
- ~Monitoring
- ~Platform
- ~Quality
- ~Release
- ~"Security Products"
- ~UX
The descriptions on the [labels page][labels-page] explain what falls under the
responsibility of each team.
@ -193,10 +207,10 @@ indicate if an issue needs backend work, frontend work, or both.
Team labels are always capitalized so that they show up as the first label for
any issue.
### Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")
### Release Scoping labels
Milestone labels help us clearly communicate expectations of the work for the
release. There are three levels of Milestone labels:
Release Scoping labels help us clearly communicate expectations of the work for the
release. There are three levels of Release Scoping labels:
- ~Deliverable: Issues that are expected to be delivered in the current
milestone.
@ -211,9 +225,9 @@ Each issue scheduled for the current milestone should be labeled ~Deliverable
or ~"Stretch". Any open issue for a previous milestone should be labeled
~"Next Patch Release", or otherwise rescheduled to a different milestone.
### Bug Priority labels (~P1, ~P2, ~P3, ~P4)
### Bug Priority labels
Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be.
Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be.
If there are multiple defects, the priority decides which defect has to be fixed immediately versus later.
This label documents the planned timeline & urgency which is used to measure against our actual SLA on delivering ~bug fixes.
@ -224,7 +238,7 @@ This label documents the planned timeline & urgency which is used to measure aga
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
### Bug Severity labels (~S1, ~S2, ~S3, ~S4)
### Bug Severity labels
Severity labels help us clearly communicate the impact of a ~bug on users.
@ -240,11 +254,11 @@ Severity labels help us clearly communicate the impact of a ~bug on users.
| Label | Security Impact | Availability / Performance Impact |
|-------|---------------------------------------------------------------------|--------------------------------------------------------------|
| ~S1 | >50% users impacted (possible company extinction level event) | |
| ~S2 | Many users or multiple paid customers impacted (but not apocalyptic)| The issue is (almost) guaranteed to occur in the near future |
| ~S2 | Many users or multiple paid customers impacted (but not apocalyptic)| The issue is (almost) guaranteed to occur in the near future |
| ~S3 | A few users or a single paid customer impacted | The issue is likely to occur in the near future |
| ~S4 | No paid users/customer impacted, or expected impact within 30 days | The issue _may_ occur but it's not likely |
### Label for community contributors (~"Accepting Merge Requests")
### Label for community contributors
Issues that are beneficial to our users, 'nice to haves', that we currently do
not have the capacity for or want to give the priority to, are labeled as
@ -300,14 +314,14 @@ For guidance on UX implementation at GitLab, please refer to our [Design System]
The UX team uses labels to manage their workflow.
The ~"UX" label on an issue is a signal to the UX team that it will need UX attention.
The ~"UX" label on an issue is a signal to the UX team that it will need UX attention.
To better understand the priority by which UX tackles issues, see the [UX section](https://about.gitlab.com/handbook/engineering/ux) of the handbook.
Once an issue has been worked on and is ready for development, a UXer removes the ~"UX" label and applies the ~"UX ready" label to that issue.
The UX team has a special type label called ~"design artifact". This label indicates that the final output
The UX team has a special type label called ~"design artifact". This label indicates that the final output
for an issue is a UX solution/design. The solution will be developed by frontend and/or backend in a subsequent milestone.
Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is
Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is
needed until the solution has been decided.
~"design artifact" issues are like any other issue and should contain a milestone label, ~"Deliverable" or ~"Stretch", when scheduled in the current milestone.

View File

@ -1 +1 @@
0.105.1
0.107.0

View File

@ -299,7 +299,6 @@ gem 'peek-sidekiq', '~> 1.0.3'
# Metrics
group :metrics do
gem 'allocations', '~> 1.0', require: false, platform: :mri
gem 'method_source', '~> 0.8', require: false
gem 'influxdb', '~> 0.2', require: false
@ -419,7 +418,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.101.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.102.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed

View File

@ -49,7 +49,6 @@ GEM
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)
asana (0.6.0)
faraday (~> 0.9)
@ -283,7 +282,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gitaly-proto (0.101.0)
gitaly-proto (0.102.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
@ -803,7 +802,7 @@ GEM
rubyzip (1.2.1)
rufus-scheduler (3.4.0)
et-orbi (~> 1.0)
rugged (0.27.1)
rugged (0.27.2)
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
@ -868,7 +867,7 @@ GEM
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
sprockets (3.7.1)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
@ -974,7 +973,6 @@ DEPENDENCIES
acts-as-taggable-on (~> 5.0)
addressable (~> 2.5.2)
akismet (~> 2.0)
allocations (~> 1.0)
asana (~> 0.6.0)
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
@ -1039,7 +1037,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.101.0)
gitaly-proto (~> 0.102.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)

View File

@ -52,7 +52,6 @@ GEM
public_suffix (>= 2.0.2, < 4.0)
aes_key_wrap (1.0.1)
akismet (2.0.0)
allocations (1.0.5)
arel (7.1.4)
asana (0.6.0)
faraday (~> 0.9)
@ -286,7 +285,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gitaly-proto (0.101.0)
gitaly-proto (0.102.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
@ -299,7 +298,7 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
gitlab-gollum-lib (4.2.7.2)
gitlab-gollum-lib (4.2.7.4)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
@ -307,7 +306,7 @@ GEM
rouge (~> 3.1)
sanitize (~> 2.1)
stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4)
gitlab-gollum-rugged_adapter (0.4.4.1)
mime-types (>= 1.15)
rugged (~> 0.25)
gitlab-grit (2.8.2)
@ -984,7 +983,6 @@ DEPENDENCIES
acts-as-taggable-on (~> 5.0)
addressable (~> 2.5.2)
akismet (~> 2.0)
allocations (~> 1.0)
asana (~> 0.6.0)
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
@ -1049,7 +1047,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.101.0)
gitaly-proto (~> 0.102.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)

View File

@ -1 +1 @@
11.0.0-pre
11.1.0-pre

View File

@ -1,4 +1,4 @@
/* eslint-disable no-param-reassign, class-methods-use-this */
/* eslint-disable class-methods-use-this */
import $ from 'jquery';
import Cookies from 'js-cookie';

View File

@ -1,4 +1,4 @@
/* eslint-disable no-param-reassign, prefer-template, no-var, no-void, consistent-return */
/* eslint-disable no-param-reassign, prefer-template, no-void, consistent-return */
import AccessorUtilities from './lib/utils/accessor';
@ -31,7 +31,9 @@ export default class Autosave {
// https://github.com/vuejs/vue/issues/2804#issuecomment-216968137
const event = new Event('change', { bubbles: true, cancelable: false });
const field = this.field.get(0);
field.dispatchEvent(event);
if (field) {
field.dispatchEvent(event);
}
}
save() {

View File

@ -11,7 +11,8 @@ import axios from './lib/utils/axios_utils';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
const requestAnimationFrame = window.requestAnimationFrame ||
const requestAnimationFrame =
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.setTimeout;
@ -37,21 +38,28 @@ class AwardsHandler {
this.emoji = emoji;
this.eventListeners = [];
// If the user shows intent let's pre-build the menu
this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => {
const $menu = $('.emoji-menu');
if ($menu.length === 0) {
requestAnimationFrame(() => {
this.createEmojiMenu();
});
}
});
this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => {
this.registerEventListener(
'one',
$(document),
'mouseenter focus',
'.js-add-award',
'mouseenter focus',
() => {
const $menu = $('.emoji-menu');
if ($menu.length === 0) {
requestAnimationFrame(() => {
this.createEmojiMenu();
});
}
},
);
this.registerEventListener('on', $(document), 'click', '.js-add-award', e => {
e.stopPropagation();
e.preventDefault();
this.showEmojiMenu($(e.currentTarget));
});
this.registerEventListener('on', $('html'), 'click', (e) => {
this.registerEventListener('on', $('html'), 'click', e => {
const $target = $(e.target);
if (!$target.closest('.emoji-menu').length) {
$('.js-awards-block.current').removeClass('current');
@ -61,12 +69,14 @@ class AwardsHandler {
}
}
});
this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', (e) => {
this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', e => {
e.preventDefault();
const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji');
const $spriteIconElement = $target.find('.icon');
const emojiName = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name');
const emojiName = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data(
'name',
);
$target.closest('.js-awards-block').addClass('current');
this.addAward(this.getVotesBlock(), this.getAwardUrl(), emojiName);
@ -83,7 +93,10 @@ class AwardsHandler {
showEmojiMenu($addBtn) {
if ($addBtn.hasClass('js-note-emoji')) {
$addBtn.closest('.note').find('.js-awards-block').addClass('current');
$addBtn
.closest('.note')
.find('.js-awards-block')
.addClass('current');
} else {
$addBtn.closest('.js-awards-block').addClass('current');
}
@ -177,32 +190,38 @@ class AwardsHandler {
const remainingCategories = Object.keys(categoryMap).slice(1);
const allCategoriesAddedPromise = remainingCategories.reduce(
(promiseChain, categoryNameKey) =>
promiseChain.then(() =>
new Promise((resolve) => {
const emojisInCategory = categoryMap[categoryNameKey];
const categoryMarkup = this.renderCategory(
categoryLabelMap[categoryNameKey],
emojisInCategory,
);
requestAnimationFrame(() => {
emojiContentElement.insertAdjacentHTML('beforeend', categoryMarkup);
resolve();
});
}),
),
promiseChain.then(
() =>
new Promise(resolve => {
const emojisInCategory = categoryMap[categoryNameKey];
const categoryMarkup = this.renderCategory(
categoryLabelMap[categoryNameKey],
emojisInCategory,
);
requestAnimationFrame(() => {
emojiContentElement.insertAdjacentHTML('beforeend', categoryMarkup);
resolve();
});
}),
),
Promise.resolve(),
);
allCategoriesAddedPromise.then(() => {
// Used for tests
// We check for the menu in case it was destroyed in the meantime
if (menu) {
menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish'));
}
}).catch((err) => {
emojiContentElement.insertAdjacentHTML('beforeend', '<p>We encountered an error while adding the remaining categories</p>');
throw new Error(`Error occurred in addRemainingEmojiMenuCategories: ${err.message}`);
});
allCategoriesAddedPromise
.then(() => {
// Used for tests
// We check for the menu in case it was destroyed in the meantime
if (menu) {
menu.dispatchEvent(new CustomEvent('build-emoji-menu-finish'));
}
})
.catch(err => {
emojiContentElement.insertAdjacentHTML(
'beforeend',
'<p>We encountered an error while adding the remaining categories</p>',
);
throw new Error(`Error occurred in addRemainingEmojiMenuCategories: ${err.message}`);
});
}
renderCategory(name, emojiList, opts = {}) {
@ -211,7 +230,9 @@ class AwardsHandler {
${name}
</h5>
<ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
${emojiList.map(emojiName => `
${emojiList
.map(
emojiName => `
<li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button">
${this.emoji.glEmojiTag(emojiName, {
@ -219,7 +240,9 @@ class AwardsHandler {
})}
</button>
</li>
`).join('\n')}
`,
)
.join('\n')}
</ul>
`;
}
@ -232,7 +255,7 @@ class AwardsHandler {
top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`,
};
if (position === 'right') {
css.left = `${($addBtn.offset().left - $menu.outerWidth()) + 20}px`;
css.left = `${$addBtn.offset().left - $menu.outerWidth() + 20}px`;
$menu.addClass('is-aligned-right');
} else {
css.left = `${$addBtn.offset().left}px`;
@ -416,7 +439,10 @@ class AwardsHandler {
</button>
`;
const $emojiButton = $(buttonHtml);
$emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('name', emojiName);
$emojiButton
.insertBefore(votesBlock.find('.js-award-holder'))
.find('.emoji-icon')
.data('name', emojiName);
this.animateEmoji($emojiButton);
$('.award-control').tooltip();
votesBlock.removeClass('current');
@ -426,7 +452,7 @@ class AwardsHandler {
const className = 'pulse animated once short';
$emoji.addClass(className);
this.registerEventListener('on', $emoji, animationEndEventString, (e) => {
this.registerEventListener('on', $emoji, animationEndEventString, e => {
$(e.currentTarget).removeClass(className);
});
}
@ -444,15 +470,16 @@ class AwardsHandler {
if (this.isUserAuthored($emojiButton)) {
this.userAuthored($emojiButton);
} else {
axios.post(awardUrl, {
name: emoji,
})
.then(({ data }) => {
if (data.ok) {
callback();
}
})
.catch(() => flash(__('Something went wrong on our end.')));
axios
.post(awardUrl, {
name: emoji,
})
.then(({ data }) => {
if (data.ok) {
callback();
}
})
.catch(() => flash(__('Something went wrong on our end.')));
}
}
@ -486,26 +513,33 @@ class AwardsHandler {
}
getFrequentlyUsedEmojis() {
return this.frequentlyUsedEmojis || (() => {
const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
inputName => this.emoji.isEmojiNameValid(inputName),
);
return (
this.frequentlyUsedEmojis ||
(() => {
const frequentlyUsedEmojis = _.uniq(
(Cookies.get('frequently_used_emojis') || '').split(','),
);
this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(inputName =>
this.emoji.isEmojiNameValid(inputName),
);
return this.frequentlyUsedEmojis;
})();
return this.frequentlyUsedEmojis;
})()
);
}
setupSearch() {
const $search = $('.js-emoji-menu-search');
this.registerEventListener('on', $search, 'input', (e) => {
const term = $(e.target).val().trim();
this.registerEventListener('on', $search, 'input', e => {
const term = $(e.target)
.val()
.trim();
this.searchEmojis(term);
});
const $menu = $('.emoji-menu');
this.registerEventListener('on', $menu, transitionEndEventString, (e) => {
this.registerEventListener('on', $menu, transitionEndEventString, e => {
if (e.target === e.currentTarget) {
// Clear the search
this.searchEmojis('');
@ -523,19 +557,26 @@ class AwardsHandler {
// Generate a search result block
const h5 = $('<h5 class="emoji-search-title"/>').text('Search results');
const foundEmojis = this.findMatchingEmojiElements(term).show();
const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
const ul = $('<ul>')
.addClass('emoji-menu-list emoji-menu-search')
.append(foundEmojis);
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
$('.emoji-menu-content').append(h5).append(ul);
$('.emoji-menu-content')
.append(h5)
.append(ul);
} else {
$('.emoji-menu-content').children().show();
$('.emoji-menu-content')
.children()
.show();
}
}
findMatchingEmojiElements(query) {
const emojiMatches = this.emoji.filterEmojiNamesByAlias(query);
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
const $matchingElements = $emojiElements
.filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0);
const $matchingElements = $emojiElements.filter(
(i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0,
);
return $matchingElements.closest('li').clone();
}
@ -550,16 +591,13 @@ class AwardsHandler {
$emojiMenu.addClass(IS_RENDERED);
// enqueues animation as a microtask, so it begins ASAP once IS_RENDERED added
return Promise.resolve()
.then(() => $emojiMenu.addClass(IS_VISIBLE));
return Promise.resolve().then(() => $emojiMenu.addClass(IS_VISIBLE));
}
hideMenuElement($emojiMenu) {
$emojiMenu.on(transitionEndEventString, (e) => {
$emojiMenu.on(transitionEndEventString, e => {
if (e.currentTarget === e.target) {
$emojiMenu
.removeClass(IS_RENDERED)
.off(transitionEndEventString);
$emojiMenu.removeClass(IS_RENDERED).off(transitionEndEventString);
}
});
@ -567,7 +605,7 @@ class AwardsHandler {
}
destroy() {
this.eventListeners.forEach((entry) => {
this.eventListeners.forEach(entry => {
entry.element.off.call(entry.element, ...entry.args);
});
$('.emoji-menu').remove();
@ -577,8 +615,9 @@ class AwardsHandler {
let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) {
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji')
.then(Emoji => new AwardsHandler(Emoji));
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(
Emoji => new AwardsHandler(Emoji),
);
}
return awardsHandlerPromise;
}

View File

@ -1,4 +1,4 @@
/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
/* eslint-disable object-shorthand, no-unused-vars, no-use-before-define, max-len, no-restricted-syntax, guard-for-in, no-continue */
import $ from 'jquery';
import _ from 'underscore';

View File

@ -1,4 +1,3 @@
/* eslint-disable no-new */
import Vue from 'vue';
import pdfLab from '../../pdf/index.vue';

View File

@ -1,5 +1,4 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */
/* global EditBlob */
/* eslint-disable no-new */
import $ from 'jquery';
import NewCommitForm from '../new_commit_form';

View File

@ -1,4 +1,4 @@
/* eslint-disable comma-dangle, space-before-function-paren, one-var */
/* eslint-disable comma-dangle */
import Sortable from 'sortablejs';
import Vue from 'vue';

View File

@ -1,4 +1,4 @@
/* eslint-disable comma-dangle, space-before-function-paren, no-alert */
/* eslint-disable comma-dangle, no-alert */
import $ from 'jquery';
import Vue from 'vue';

View File

@ -1,4 +1,4 @@
/* eslint-disable comma-dangle, space-before-function-paren, no-new */
/* eslint-disable comma-dangle, no-new */
import $ from 'jquery';
import Vue from 'vue';

View File

@ -1,8 +1,8 @@
import Vue from 'vue';
<script>
import ModalStore from '../../stores/modal_store';
import modalMixin from '../../mixins/modal_mixins';
gl.issueBoards.ModalEmptyState = Vue.extend({
export default {
mixins: [modalMixin],
props: {
newIssuePath: {
@ -38,32 +38,36 @@ gl.issueBoards.ModalEmptyState = Vue.extend({
return obj;
},
},
template: `
<section class="empty-state">
<div class="row">
<div class="col-12 col-md-6 order-md-last">
<aside class="svg-content"><img :src="emptyStateSvg"/></aside>
</div>
<div class="col-12 col-md-6 order-md-first">
<div class="text-content">
<h4>{{ contents.title }}</h4>
<p v-html="contents.content"></p>
<a
:href="newIssuePath"
class="btn btn-success btn-inverted"
v-if="activeTab === 'all'">
New issue
</a>
<button
type="button"
class="btn btn-default"
@click="changeTab('all')"
v-if="activeTab === 'selected'">
Open issues
</button>
</div>
};
</script>
<template>
<section class="empty-state">
<div class="row">
<div class="col-12 col-md-6 order-md-last">
<aside class="svg-content"><img :src="emptyStateSvg"/></aside>
</div>
<div class="col-12 col-md-6 order-md-first">
<div class="text-content">
<h4>{{ contents.title }}</h4>
<p v-html="contents.content"></p>
<a
v-if="activeTab === 'all'"
:href="newIssuePath"
class="btn btn-success btn-inverted"
>
New issue
</a>
<button
v-if="activeTab === 'selected'"
class="btn btn-default"
type="button"
@click="changeTab('all')"
>
Open issues
</button>
</div>
</div>
</section>
`,
});
</div>
</section>
</template>

View File

@ -6,15 +6,15 @@ import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import './header';
import './list';
import './footer';
import './empty_state';
import EmptyState from './empty_state.vue';
import ModalStore from '../../stores/modal_store';
gl.issueBoards.IssuesModal = Vue.extend({
components: {
EmptyState,
'modal-header': gl.issueBoards.ModalHeader,
'modal-list': gl.issueBoards.ModalList,
'modal-footer': gl.issueBoards.ModalFooter,
'empty-state': gl.issueBoards.ModalEmptyState,
loadingIcon,
},
props: {

View File

@ -1,4 +1,4 @@
/* eslint-disable func-names, no-new, space-before-function-paren, one-var, promise/catch-or-return, max-len */
/* eslint-disable func-names, no-new, promise/catch-or-return */
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';

View File

@ -1,4 +1,3 @@
/* eslint-disable class-methods-use-this */
import FilteredSearchContainer from '../filtered_search/container';
import FilteredSearchManager from '../filtered_search/filtered_search_manager';

View File

@ -1,4 +1,4 @@
/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
/* eslint-disable quote-props, comma-dangle */
import $ from 'jquery';
import _ from 'underscore';

View File

@ -1,4 +1,3 @@
/* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */
/* global DocumentTouch */
import $ from 'jquery';

View File

@ -1,4 +1,4 @@
/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */
/* eslint-disable no-unused-vars, comma-dangle */
/* global ListLabel */
/* global ListMilestone */
/* global ListAssignee */

View File

@ -1,4 +1,4 @@
/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */
/* eslint-disable no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len */
/* global ListIssue */
import ListLabel from '~/vue_shared/models/label';

View File

@ -1,5 +1,3 @@
/* eslint-disable no-unused-vars */
class ListMilestone {
constructor(obj) {
this.id = obj.id;

View File

@ -1,4 +1,4 @@
/* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */
/* eslint-disable comma-dangle, no-shadow */
/* global List */
import $ from 'jquery';

View File

@ -1,4 +1,4 @@
/* eslint-disable func-names, prefer-arrow-callback, no-return-assign */
/* eslint-disable func-names, prefer-arrow-callback */
import $ from 'jquery';
import { visitUrl } from './lib/utils/url_utility';

View File

@ -1,4 +1,4 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */
/* eslint-disable func-names, wrap-iife, no-var, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, max-len */
import $ from 'jquery';
@ -95,7 +95,7 @@ export default class ImageFile {
});
return [maxWidth, maxHeight];
}
// eslint-disable-next-line
views = {
'two-up': function() {
return $('.two-up.view .wrap', this.file).each((function(_this) {

View File

@ -1,4 +1,4 @@
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */
/* eslint-disable func-names, one-var, no-var, one-var-declaration-per-line, object-shorthand, no-else-return, max-len */
import $ from 'jquery';
import { __ } from './locale';

View File

@ -366,7 +366,7 @@ export default class CreateMergeRequestDropdown {
removeMessage(target) {
const { input, message } = this.getTargetData(target);
const inputClasses = ['gl-field-error-outline', 'gl-field-success-outline'];
const messageClasses = ['gl-field-hint', 'gl-field-error-message', 'gl-field-success-message'];
const messageClasses = ['text-muted', 'text-danger', 'text-success'];
inputClasses.forEach(cssClass => input.classList.remove(cssClass));
messageClasses.forEach(cssClass => message.classList.remove(cssClass));
@ -393,7 +393,7 @@ export default class CreateMergeRequestDropdown {
this.removeMessage(target);
input.classList.add('gl-field-success-outline');
message.classList.add('gl-field-success-message');
message.classList.add('text-success');
message.textContent = sprintf(__('%{text} is available'), { text });
message.style.display = 'inline-block';
}
@ -403,7 +403,7 @@ export default class CreateMergeRequestDropdown {
const text = target === 'branch' ? __('branch name') : __('source');
this.removeMessage(target);
message.classList.add('gl-field-hint');
message.classList.add('text-muted');
message.textContent = sprintf(__('Checking %{text} availability…'), { text });
message.style.display = 'inline-block';
}
@ -415,7 +415,7 @@ export default class CreateMergeRequestDropdown {
this.removeMessage(target);
input.classList.add('gl-field-error-outline');
message.classList.add('gl-field-error-message');
message.classList.add('text-danger');
message.textContent = text;
message.style.display = 'inline-block';
}

View File

@ -1,11 +1,10 @@
/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, space-before-function-paren, no-lonely-if, no-continue, brace-style, max-len, quotes */
/* global DiscussionMixins */
/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, no-lonely-if, no-continue, brace-style, max-len, quotes */
/* global CommentsStore */
import $ from 'jquery';
import Vue from 'vue';
import '../mixins/discussion';
import DiscussionMixins from '../mixins/discussion';
const JumpToDiscussion = Vue.extend({
mixins: [DiscussionMixins],

View File

@ -1,4 +1,3 @@
/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, max-len */
/* global CommentsStore */
/* global ResolveService */
@ -41,54 +40,54 @@ const ResolveBtn = Vue.extend({
required: true,
},
},
data: function () {
data() {
return {
discussions: CommentsStore.state,
loading: false
loading: false,
};
},
computed: {
discussion: function () {
discussion() {
return this.discussions[this.discussionId];
},
note: function () {
note() {
return this.discussion ? this.discussion.getNote(this.noteId) : {};
},
buttonText: function () {
buttonText() {
if (this.isResolved) {
return `Resolved by ${this.resolvedByName}`;
} else if (this.canResolve) {
return 'Mark as resolved';
} else {
return 'Unable to resolve';
}
return 'Unable to resolve';
},
isResolved: function () {
isResolved() {
if (this.note) {
return this.note.resolved;
} else {
return false;
}
return false;
},
resolvedByName: function () {
resolvedByName() {
return this.note.resolved_by;
},
},
watch: {
'discussions': {
discussions: {
handler: 'updateTooltip',
deep: true
}
deep: true,
},
},
mounted: function () {
mounted() {
$(this.$refs.button).tooltip({
container: 'body'
container: 'body',
});
},
beforeDestroy: function () {
beforeDestroy() {
CommentsStore.delete(this.discussionId, this.noteId);
},
created: function () {
created() {
CommentsStore.create({
discussionId: this.discussionId,
noteId: this.noteId,
@ -101,43 +100,41 @@ const ResolveBtn = Vue.extend({
});
},
methods: {
updateTooltip: function () {
updateTooltip() {
this.$nextTick(() => {
$(this.$refs.button)
.tooltip('hide')
.tooltip('_fixTitle');
});
},
resolve: function () {
resolve() {
if (!this.canResolve) return;
let promise;
this.loading = true;
if (this.isResolved) {
promise = ResolveService
.unresolve(this.noteId);
promise = ResolveService.unresolve(this.noteId);
} else {
promise = ResolveService
.resolve(this.noteId);
promise = ResolveService.resolve(this.noteId);
}
promise
.then(resp => resp.json())
.then((data) => {
.then(data => {
this.loading = false;
const resolved_by = data ? data.resolved_by : null;
const resolvedBy = data ? data.resolved_by : null;
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolvedBy);
this.discussion.updateHeadline(data);
gl.mrWidget.checkStatus();
document.dispatchEvent(new CustomEvent('refreshVueNotes'));
this.updateTooltip();
})
.catch(() => new Flash('An error occurred when trying to resolve a comment. Please try again.'));
}
.catch(
() => new Flash('An error occurred when trying to resolve a comment. Please try again.'),
);
},
},
});

View File

@ -1,10 +1,9 @@
/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */
/* global DiscussionMixins */
/* eslint-disable comma-dangle, object-shorthand, func-names */
/* global CommentsStore */
import Vue from 'vue';
import '../mixins/discussion';
import DiscussionMixins from '../mixins/discussion';
window.ResolveCount = Vue.extend({
mixins: [DiscussionMixins],

View File

@ -1,4 +1,4 @@
/* eslint-disable object-shorthand, func-names, space-before-function-paren, comma-dangle, no-else-return, quotes, max-len */
/* eslint-disable object-shorthand, func-names, comma-dangle, no-else-return, quotes */
/* global CommentsStore */
/* global ResolveService */

View File

@ -1,5 +1,4 @@
/* eslint-disable func-names, comma-dangle, new-cap, no-new, max-len */
/* global ResolveCount */
/* eslint-disable func-names, new-cap */
import $ from 'jquery';
import Vue from 'vue';
@ -15,12 +14,13 @@ import './components/resolve_count';
import './components/resolve_discussion_btn';
import './components/diff_note_avatars';
import './components/new_issue_for_discussion';
import { hasVueMRDiscussionsCookie } from '../lib/utils/common_utils';
export default () => {
const projectPathHolder = document.querySelector('.merge-request') || document.querySelector('.commit-box');
const projectPathHolder =
document.querySelector('.merge-request') || document.querySelector('.commit-box');
const projectPath = projectPathHolder.dataset.projectPath;
const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
const COMPONENT_SELECTOR =
'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
window.gl = window.gl || {};
window.gl.diffNoteApps = {};
@ -28,9 +28,9 @@ export default () => {
window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath);
gl.diffNotesCompileComponents = () => {
$('diff-note-avatars').each(function () {
$('diff-note-avatars').each(function() {
const tmp = Vue.extend({
template: $(this).get(0).outerHTML
template: $(this).get(0).outerHTML,
});
const tmpApp = new tmp().$mount();
@ -41,12 +41,12 @@ export default () => {
});
});
const $components = $(COMPONENT_SELECTOR).filter(function () {
const $components = $(COMPONENT_SELECTOR).filter(function() {
return $(this).closest('resolve-count').length !== 1;
});
if ($components) {
$components.each(function () {
$components.each(function() {
const $this = $(this);
const noteId = $this.attr(':note-id');
const discussionId = $this.attr(':discussion-id');
@ -54,7 +54,7 @@ export default () => {
if ($this.is('comment-and-resolve-btn') && !discussionId) return;
const tmp = Vue.extend({
template: $this.get(0).outerHTML
template: $this.get(0).outerHTML,
});
const tmpApp = new tmp().$mount();
@ -69,15 +69,5 @@ export default () => {
gl.diffNotesCompileComponents();
const resolveCountAppEl = document.querySelector('#resolve-count-app');
if (!hasVueMRDiscussionsCookie() && resolveCountAppEl) {
new Vue({
el: resolveCountAppEl,
components: {
'resolve-count': ResolveCount
},
});
}
$(window).trigger('resize.nav');
};

View File

@ -1,6 +1,6 @@
/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-param-reassign, max-len */
/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, */
window.DiscussionMixins = {
const DiscussionMixins = {
computed: {
discussionCount: function () {
return Object.keys(this.discussions).length;
@ -33,3 +33,5 @@ window.DiscussionMixins = {
}
}
};
export default DiscussionMixins;

View File

@ -1,4 +1,4 @@
/* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */
/* eslint-disable camelcase, guard-for-in, no-restricted-syntax */
/* global NoteModel */
import $ from 'jquery';

View File

@ -1,5 +1,3 @@
/* eslint-disable camelcase, no-unused-vars */
class NoteModel {
constructor(discussionId, noteObj) {
this.discussionId = discussionId;

View File

@ -8,8 +8,12 @@ window.gl = window.gl || {};
class ResolveServiceClass {
constructor(root) {
this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`);
this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`);
this.noteResource = Vue.resource(
`${root}/notes{/noteId}/resolve?html=true`,
);
this.discussionResource = Vue.resource(
`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`,
);
}
resolve(noteId) {
@ -33,7 +37,7 @@ class ResolveServiceClass {
promise
.then(resp => resp.json())
.then((data) => {
.then(data => {
discussion.loading = false;
const resolvedBy = data ? data.resolved_by : null;
@ -45,9 +49,13 @@ class ResolveServiceClass {
if (gl.mrWidget) gl.mrWidget.checkStatus();
discussion.updateHeadline(data);
document.dispatchEvent(new CustomEvent('refreshVueNotes'));
})
.catch(() => new Flash('An error occurred when trying to resolve a discussion. Please try again.'));
.catch(
() =>
new Flash(
'An error occurred when trying to resolve a discussion. Please try again.',
),
);
}
resolveAll(mergeRequestId, discussionId) {
@ -55,10 +63,13 @@ class ResolveServiceClass {
discussion.loading = true;
return this.discussionResource.save({
mergeRequestId,
discussionId,
}, {});
return this.discussionResource.save(
{
mergeRequestId,
discussionId,
},
{},
);
}
unResolveAll(mergeRequestId, discussionId) {
@ -66,10 +77,13 @@ class ResolveServiceClass {
discussion.loading = true;
return this.discussionResource.delete({
mergeRequestId,
discussionId,
}, {});
return this.discussionResource.delete(
{
mergeRequestId,
discussionId,
},
{},
);
}
}

View File

@ -1,4 +1,4 @@
/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */
/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len */
/* global DiscussionModel */
import Vue from 'vue';

View File

@ -0,0 +1,197 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
import createFlash from '~/flash';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import CompareVersions from './compare_versions.vue';
import ChangedFiles from './changed_files.vue';
import DiffFile from './diff_file.vue';
import NoChanges from './no_changes.vue';
import HiddenFilesWarning from './hidden_files_warning.vue';
export default {
name: 'DiffsApp',
components: {
Icon,
LoadingIcon,
CompareVersions,
ChangedFiles,
DiffFile,
NoChanges,
HiddenFilesWarning,
},
props: {
endpoint: {
type: String,
required: true,
},
shouldShow: {
type: Boolean,
required: false,
default: false,
},
currentUser: {
type: Object,
required: true,
},
},
data() {
return {
activeFile: '',
};
},
computed: {
...mapState({
isLoading: state => state.diffs.isLoading,
diffFiles: state => state.diffs.diffFiles,
diffViewType: state => state.diffs.diffViewType,
mergeRequestDiffs: state => state.diffs.mergeRequestDiffs,
mergeRequestDiff: state => state.diffs.mergeRequestDiff,
latestVersionPath: state => state.diffs.latestVersionPath,
startVersion: state => state.diffs.startVersion,
commit: state => state.diffs.commit,
targetBranchName: state => state.diffs.targetBranchName,
renderOverflowWarning: state => state.diffs.renderOverflowWarning,
numTotalFiles: state => state.diffs.realSize,
numVisibleFiles: state => state.diffs.size,
plainDiffPath: state => state.diffs.plainDiffPath,
emailPatchPath: state => state.diffs.emailPatchPath,
}),
...mapGetters(['isParallelView']),
targetBranch() {
return {
branchName: this.targetBranchName,
versionIndex: -1,
path: '',
};
},
notAllCommentsDisplayed() {
if (this.commit) {
return __('Only comments from the following commit are shown below');
} else if (this.startVersion) {
return __(
"Not all comments are displayed because you're comparing two versions of the diff.",
);
}
return __(
"Not all comments are displayed because you're viewing an old version of the diff.",
);
},
showLatestVersion() {
if (this.commit) {
return __('Show latest version of the diff');
}
return __('Show latest version');
},
},
watch: {
diffViewType() {
this.adjustView();
},
shouldShow() {
this.adjustView();
},
},
mounted() {
this.setEndpoint(this.endpoint);
this
.fetchDiffFiles()
.catch(() => {
createFlash(__('Something went wrong on our end. Please try again!'));
});
},
created() {
this.adjustView();
},
methods: {
...mapActions(['setEndpoint', 'fetchDiffFiles']),
setActive(filePath) {
this.activeFile = filePath;
},
unsetActive(filePath) {
if (this.activeFile === filePath) {
this.activeFile = '';
}
},
adjustView() {
if (this.shouldShow && this.isParallelView) {
window.mrTabs.expandViewContainer();
} else {
window.mrTabs.resetViewContainer();
}
},
},
};
</script>
<template>
<div v-if="shouldShow">
<div
v-if="isLoading"
class="loading"
>
<loading-icon />
</div>
<div
v-else
id="diffs"
:class="{ active: shouldShow }"
class="diffs tab-pane"
>
<compare-versions
v-if="!commit && mergeRequestDiffs.length > 1"
:merge-request-diffs="mergeRequestDiffs"
:merge-request-diff="mergeRequestDiff"
:start-version="startVersion"
:target-branch="targetBranch"
/>
<hidden-files-warning
v-if="renderOverflowWarning"
:visible="numVisibleFiles"
:total="numTotalFiles"
:plain-diff-path="plainDiffPath"
:email-patch-path="emailPatchPath"
/>
<div
v-if="commit || startVersion || (mergeRequestDiff && !mergeRequestDiff.latest)"
class="mr-version-controls"
>
<div class="content-block comments-disabled-notif clearfix">
<i class="fa fa-info-circle"></i>
{{ notAllCommentsDisplayed }}
<div class="pull-right">
<a
:href="latestVersionPath"
class="btn btn-sm"
>
{{ showLatestVersion }}
</a>
</div>
</div>
</div>
<changed-files
:diff-files="diffFiles"
:active-file="activeFile"
/>
<div
v-if="diffFiles.length > 0"
class="files"
>
<diff-file
v-for="file in diffFiles"
:key="file.newPath"
:file="file"
:current-user="currentUser"
@setActive="setActive(file.filePath)"
@unsetActive="unsetActive(file.filePath)"
/>
</div>
<no-changes v-else />
</div>
</div>
</template>

View File

@ -0,0 +1,184 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '~/lib/utils/text_utility';
import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
import { contentTop } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import ChangedFilesDropdown from './changed_files_dropdown.vue';
import changedFilesMixin from '../mixins/changed_files';
export default {
components: {
Icon,
ChangedFilesDropdown,
ClipboardButton,
},
mixins: [changedFilesMixin],
props: {
activeFile: {
type: String,
required: false,
default: '',
},
},
data() {
return {
isStuck: false,
maxWidth: 'auto',
offsetTop: 0,
};
},
computed: {
...mapGetters(['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
sumAddedLines() {
return this.sumValues('addedLines');
},
sumRemovedLines() {
return this.sumValues('removedLines');
},
whitespaceVisible() {
return !getParameterValues('w')[0];
},
toggleWhitespaceText() {
if (this.whitespaceVisible) {
return __('Hide whitespace changes');
}
return __('Show whitespace changes');
},
toggleWhitespacePath() {
if (this.whitespaceVisible) {
return mergeUrlParams({ w: 1 }, window.location.href);
}
return mergeUrlParams({ w: 0 }, window.location.href);
},
top() {
return `${this.offsetTop}px`;
},
},
created() {
document.addEventListener('scroll', this.handleScroll);
this.offsetTop = contentTop();
},
beforeDestroy() {
document.removeEventListener('scroll', this.handleScroll);
},
methods: {
...mapActions(['setInlineDiffViewType', 'setParallelDiffViewType', 'expandAllFiles']),
pluralize,
handleScroll() {
if (!this.updating) {
requestAnimationFrame(this.updateIsStuck);
this.updating = true;
}
},
updateIsStuck() {
if (!this.$refs.wrapper) {
return;
}
const scrollPosition = window.scrollY;
this.isStuck = scrollPosition + this.offsetTop >= this.$refs.placeholder.offsetTop;
this.updating = false;
},
sumValues(key) {
return this.diffFiles.reduce((total, file) => total + file[key], 0);
},
},
};
</script>
<template>
<span>
<div ref="placeholder"></div>
<div
ref="wrapper"
:style="{ top }"
:class="{'is-stuck': isStuck}"
class="content-block oneline-block diff-files-changed diff-files-changed-merge-request
files-changed js-diff-files-changed"
>
<div class="files-changed-inner">
<div
class="inline-parallel-buttons d-none d-md-block"
>
<a
v-if="areAllFilesCollapsed"
class="btn btn-default"
@click="expandAllFiles"
>
{{ __('Expand all') }}
</a>
<a
:href="toggleWhitespacePath"
class="btn btn-default"
>
{{ toggleWhitespaceText }}
</a>
<div class="btn-group">
<button
id="inline-diff-btn"
:class="{ active: isInlineView }"
type="button"
class="btn js-inline-diff-button"
data-view-type="inline"
@click="setInlineDiffViewType"
>
{{ __('Inline') }}
</button>
<button
id="parallel-diff-btn"
:class="{ active: isParallelView }"
type="button"
class="btn js-parallel-diff-button"
data-view-type="parallel"
@click="setParallelDiffViewType"
>
{{ __('Side-by-side') }}
</button>
</div>
</div>
<div class="commit-stat-summary dropdown">
<changed-files-dropdown
:diff-files="diffFiles"
/>
<span
v-show="activeFile"
class="prepend-left-5"
>
<strong class="prepend-right-5">
{{ truncatedDiffPath(activeFile) }}
</strong>
<clipboard-button
:text="activeFile"
:title="s__('Copy file name to clipboard')"
tooltip-placement="bottom"
tooltip-container="body"
class="btn btn-default btn-transparent btn-clipboard"
/>
</span>
<span
v-show="!isStuck"
id="diff-stats"
class="diff-stats-additions-deletions-expanded"
>
with
<strong class="cgreen">
{{ pluralize(`${sumAddedLines} addition`, sumAddedLines) }}
</strong>
and
<strong class="cred">
{{ pluralize(`${sumRemovedLines} deletion`, sumRemovedLines) }}
</strong>
</span>
</div>
</div>
</div>
</span>
</template>

View File

@ -0,0 +1,124 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import changedFilesMixin from '../mixins/changed_files';
export default {
components: {
Icon,
},
mixins: [changedFilesMixin],
data() {
return {
searchText: '',
};
},
computed: {
filteredDiffFiles() {
return this.diffFiles.filter(file =>
file.filePath.toLowerCase().includes(this.searchText.toLowerCase()),
);
},
},
methods: {
clearSearch() {
this.searchText = '';
},
},
};
</script>
<template>
<span>
Showing
<button
class="diff-stats-summary-toggler"
data-toggle="dropdown"
type="button"
aria-expanded="false"
>
<span>
{{ n__('%d changed file', '%d changed files', diffFiles.length) }}
</span>
<icon
:size="8"
name="chevron-down"
/>
</button>
<div class="dropdown-menu diff-file-changes">
<div class="dropdown-input">
<input
v-model="searchText"
type="search"
class="dropdown-input-field"
placeholder="Search files"
autocomplete="off"
/>
<i
v-if="searchText.length === 0"
aria-hidden="true"
data-hidden="true"
class="fa fa-search dropdown-input-search">
</i>
<i
v-else
role="button"
class="fa fa-times dropdown-input-search"
@click="clearSearch"
></i>
</div>
<ul>
<li
v-for="diffFile in filteredDiffFiles"
:key="diffFile.name"
>
<a
:href="`#${diffFile.fileHash}`"
:title="diffFile.newPath"
class="diff-changed-file"
>
<icon
:name="fileChangedIcon(diffFile)"
:size="16"
:class="fileChangedClass(diffFile)"
class="diff-file-changed-icon append-right-8"
/>
<span class="diff-changed-file-content append-right-8">
<strong
v-if="diffFile.blob && diffFile.blob.name"
class="diff-changed-file-name"
>
{{ diffFile.blob.name }}
</strong>
<strong
v-else
class="diff-changed-blank-file-name"
>
{{ s__('Diffs|No file name available') }}
</strong>
<span class="diff-changed-file-path prepend-top-5">
{{ truncatedDiffPath(diffFile.blob.path) }}
</span>
</span>
<span class="diff-changed-stats">
<span class="cgreen">
+{{ diffFile.addedLines }}
</span>
<span class="cred">
-{{ diffFile.removedLines }}
</span>
</span>
</a>
</li>
<li
v-show="filteredDiffFiles.length === 0"
class="dropdown-menu-empty-item"
>
<a>
{{ __('No files found') }}
</a>
</li>
</ul>
</div>
</span>
</template>

View File

@ -0,0 +1,55 @@
<script>
import CompareVersionsDropdown from './compare_versions_dropdown.vue';
export default {
components: {
CompareVersionsDropdown,
},
props: {
mergeRequestDiffs: {
type: Array,
required: true,
},
mergeRequestDiff: {
type: Object,
required: true,
},
startVersion: {
type: Object,
required: false,
default: null,
},
targetBranch: {
type: Object,
required: false,
default: null,
},
},
computed: {
comparableDiffs() {
return this.mergeRequestDiffs.slice(1);
},
},
};
</script>
<template>
<div class="mr-version-controls">
<div class="mr-version-menus-container content-block">
Changes between
<compare-versions-dropdown
:other-versions="mergeRequestDiffs"
:merge-request-version="mergeRequestDiff"
:show-commit-count="true"
class="mr-version-dropdown"
/>
and
<compare-versions-dropdown
:other-versions="comparableDiffs"
:start-version="startVersion"
:target-branch="targetBranch"
class="mr-version-compare-dropdown"
/>
</div>
</div>
</template>

View File

@ -0,0 +1,165 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import { n__, __ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
Icon,
TimeAgo,
},
props: {
otherVersions: {
type: Array,
required: false,
default: () => [],
},
mergeRequestVersion: {
type: Object,
required: false,
default: null,
},
startVersion: {
type: Object,
required: false,
default: null,
},
targetBranch: {
type: Object,
required: false,
default: null,
},
showCommitCount: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
baseVersion() {
return {
name: 'hii',
versionIndex: -1,
};
},
targetVersions() {
if (this.mergeRequestVersion) {
return this.otherVersions;
}
return [...this.otherVersions, this.targetBranch];
},
selectedVersionName() {
const selectedVersion = this.startVersion || this.targetBranch || this.mergeRequestVersion;
return this.versionName(selectedVersion);
},
},
methods: {
commitsText(version) {
return n__(
`${version.commitsCount} commit,`,
`${version.commitsCount} commits,`,
version.commitsCount,
);
},
href(version) {
if (this.showCommitCount) {
return version.versionPath;
}
return version.comparePath;
},
versionName(version) {
if (this.isLatest(version)) {
return __('latest version');
}
if (this.targetBranch && (this.isBase(version) || !version)) {
return this.targetBranch.branchName;
}
return `version ${version.versionIndex}`;
},
isActive(version) {
if (!version) {
return false;
}
if (this.targetBranch) {
return (
(this.isBase(version) && !this.startVersion) ||
(this.startVersion && this.startVersion.versionIndex === version.versionIndex)
);
}
return version.versionIndex === this.mergeRequestVersion.versionIndex;
},
isBase(version) {
if (!version || !this.targetBranch) {
return false;
}
return version.versionIndex === -1;
},
isLatest(version) {
return (
this.mergeRequestVersion && version.versionIndex === this.targetVersions[0].versionIndex
);
},
},
};
</script>
<template>
<span class="dropdown inline">
<a
class="dropdown-toggle btn btn-default"
data-toggle="dropdown"
aria-expanded="false"
>
<span>
{{ selectedVersionName }}
</span>
<Icon
:size="12"
name="angle-down"
/>
</a>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
<div class="dropdown-content">
<ul>
<li
v-for="version in targetVersions"
:key="version.id"
>
<a
:class="{ 'is-active': isActive(version) }"
:href="href(version)"
>
<div>
<strong>
{{ versionName(version) }}
<template v-if="isBase(version)">
(base)
</template>
</strong>
</div>
<div>
<small class="commit-sha">
{{ version.truncatedCommitSha }}
</small>
</div>
<div>
<small>
<template v-if="showCommitCount">
{{ commitsText(version) }}
</template>
<time-ago
v-if="version.createdAt"
:time="version.createdAt"
class="js-timeago js-timeago-render"
/>
</small>
</div>
</a>
</li>
</ul>
</div>
</div>
</span>
</template>

View File

@ -0,0 +1,38 @@
<script>
import { mapGetters } from 'vuex';
import InlineDiffView from './inline_diff_view.vue';
import ParallelDiffView from './parallel_diff_view.vue';
export default {
components: {
InlineDiffView,
ParallelDiffView,
},
props: {
diffFile: {
type: Object,
required: true,
},
},
computed: {
...mapGetters(['isInlineView', 'isParallelView']),
},
};
</script>
<template>
<div class="diff-content">
<div class="diff-viewer">
<inline-diff-view
v-if="isInlineView"
:diff-file="diffFile"
:diff-lines="diffFile.highlightedDiffLines || []"
/>
<parallel-diff-view
v-if="isParallelView"
:diff-file="diffFile"
:diff-lines="diffFile.parallelDiffLines || []"
/>
</div>
</div>
</template>

View File

@ -0,0 +1,39 @@
<script>
import noteableDiscussion from '../../notes/components/noteable_discussion.vue';
export default {
components: {
noteableDiscussion,
},
props: {
discussions: {
type: Array,
required: true,
},
},
};
</script>
<template>
<div
v-if="discussions.length"
>
<div
v-for="discussion in discussions"
:key="discussion.id"
class="discussion-notes diff-discussions"
>
<ul
:data-discussion-id="discussion.id"
class="notes"
>
<noteable-discussion
:discussion="discussion"
:render-header="false"
:render-diff-file="false"
:always-expanded="true"
/>
</ul>
</div>
</div>
</template>

View File

@ -0,0 +1,191 @@
<script>
import { mapActions } from 'vuex';
import _ from 'underscore';
import { __, sprintf } from '~/locale';
import createFlash from '~/flash';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue';
export default {
components: {
DiffFileHeader,
DiffContent,
LoadingIcon,
},
props: {
file: {
type: Object,
required: true,
},
currentUser: {
type: Object,
required: true,
},
},
data() {
return {
isActive: false,
isLoadingCollapsedDiff: false,
forkMessageVisible: false,
};
},
computed: {
isDiscussionsExpanded() {
return true; // TODO: @fatihacet - Fix this.
},
isCollapsed() {
return this.file.collapsed || false;
},
viewBlobLink() {
return sprintf(
__('You can %{linkStart}view the blob%{linkEnd} instead.'),
{
linkStart: `<a href="${_.escape(this.file.viewPath)}">`,
linkEnd: '</a>',
},
false,
);
},
},
mounted() {
document.addEventListener('scroll', this.handleScroll);
},
beforeDestroy() {
document.removeEventListener('scroll', this.handleScroll);
},
methods: {
...mapActions(['loadCollapsedDiff']),
handleToggle() {
const { collapsed, highlightedDiffLines, parallelDiffLines } = this.file;
if (collapsed && !highlightedDiffLines && !parallelDiffLines.length) {
this.handleLoadCollapsedDiff();
} else {
this.file.collapsed = !this.file.collapsed;
}
},
handleScroll() {
if (!this.updating) {
requestAnimationFrame(this.scrollUpdate.bind(this));
this.updating = true;
}
},
scrollUpdate() {
const header = document.querySelector('.js-diff-files-changed');
if (!header) {
this.updating = false;
return;
}
const { top, bottom } = this.$el.getBoundingClientRect();
const { top: topOfFixedHeader, bottom: bottomOfFixedHeader } = header.getBoundingClientRect();
const headerOverlapsContent = top < topOfFixedHeader && bottom > bottomOfFixedHeader;
const fullyAboveHeader = bottom < bottomOfFixedHeader;
const fullyBelowHeader = top > topOfFixedHeader;
if (headerOverlapsContent && !this.isActive) {
this.$emit('setActive');
this.isActive = true;
} else if (this.isActive && (fullyAboveHeader || fullyBelowHeader)) {
this.$emit('unsetActive');
this.isActive = false;
}
this.updating = false;
},
handleLoadCollapsedDiff() {
this.isLoadingCollapsedDiff = true;
this.loadCollapsedDiff(this.file)
.then(() => {
this.isLoadingCollapsedDiff = false;
this.file.collapsed = false;
})
.catch(() => {
this.isLoadingCollapsedDiff = false;
createFlash(__('Something went wrong on our end. Please try again!'));
});
},
showForkMessage() {
this.forkMessageVisible = true;
},
hideForkMessage() {
this.forkMessageVisible = false;
},
},
};
</script>
<template>
<div
:id="file.fileHash"
class="diff-file file-holder"
>
<diff-file-header
:current-user="currentUser"
:diff-file="file"
:collapsible="true"
:expanded="!isCollapsed"
:discussions-expanded="isDiscussionsExpanded"
:add-merge-request-buttons="true"
class="js-file-title file-title"
@toggleFile="handleToggle"
@showForkMessage="showForkMessage"
/>
<div
v-if="forkMessageVisible"
class="js-file-fork-suggestion-section file-fork-suggestion">
<span class="file-fork-suggestion-note">
You're not allowed to <span class="js-file-fork-suggestion-section-action">edit</span>
files in this project directly. Please fork this project,
make your changes there, and submit a merge request.
</span>
<a
:href="file.forkPath"
class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success"
>
Fork
</a>
<button
class="js-cancel-fork-suggestion-button btn btn-grouped"
type="button"
@click="hideForkMessage"
>
Cancel
</button>
</div>
<diff-content
v-show="!isCollapsed"
:class="{ hidden: isCollapsed || file.tooLarge }"
:diff-file="file"
/>
<loading-icon
v-if="isLoadingCollapsedDiff"
class="diff-content loading"
/>
<div
v-show="isCollapsed && !isLoadingCollapsedDiff && !file.tooLarge"
class="nothing-here-block diff-collapsed"
>
{{ __('This diff is collapsed.') }}
<a
class="click-to-expand js-click-to-expand"
href="#"
@click.prevent="handleToggle"
>
{{ __('Click to expand it.') }}
</a>
</div>
<div
v-if="file.tooLarge"
class="nothing-here-block diff-collapsed js-too-large-diff"
>
{{ __('This source diff could not be displayed because it is too large.') }}
<span v-html="viewBlobLink"></span>
</div>
</div>
</template>

View File

@ -0,0 +1,254 @@
<script>
import _ from 'underscore';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale';
import EditButton from './edit_button.vue';
export default {
components: {
ClipboardButton,
EditButton,
Icon,
},
directives: {
Tooltip,
},
props: {
diffFile: {
type: Object,
required: true,
},
collapsible: {
type: Boolean,
required: false,
default: false,
},
addMergeRequestButtons: {
type: Boolean,
required: false,
default: false,
},
expanded: {
type: Boolean,
required: false,
default: true,
},
discussionsExpanded: {
type: Boolean,
required: false,
default: true,
},
currentUser: {
type: Object,
required: true,
},
},
data() {
return {
blobForkSuggestion: null,
};
},
computed: {
icon() {
if (this.diffFile.submodule) {
return 'archive';
}
return this.diffFile.blob.icon;
},
titleLink() {
if (this.diffFile.submodule) {
return this.diffFile.submoduleTreeUrl || this.diffFile.submoduleLink;
}
return `#${this.diffFile.fileHash}`;
},
filePath() {
if (this.diffFile.submodule) {
return `${this.diffFile.filePath} @ ${truncateSha(this.diffFile.blob.id)}`;
}
if (this.diffFile.deletedFile) {
return sprintf(__('%{filePath} deleted'), { filePath: this.diffFile.filePath }, false);
}
return this.diffFile.filePath;
},
titleTag() {
return this.diffFile.fileHash ? 'a' : 'span';
},
isUsingLfs() {
return this.diffFile.storedExternally && this.diffFile.externalStorage === 'lfs';
},
collapseIcon() {
return this.expanded ? 'chevron-down' : 'chevron-right';
},
isDiscussionsExpanded() {
return this.discussionsExpanded && this.expanded;
},
viewFileButtonText() {
const truncatedContentSha = _.escape(truncateSha(this.diffFile.contentSha));
return sprintf(
s__('MergeRequests|View file @ %{commitId}'),
{
commitId: `<span class="commit-sha">${truncatedContentSha}</span>`,
},
false,
);
},
viewReplacedFileButtonText() {
const truncatedBaseSha = _.escape(truncateSha(this.diffFile.diffRefs.baseSha));
return sprintf(
s__('MergeRequests|View replaced file @ %{commitId}'),
{
commitId: `<span class="commit-sha">${truncatedBaseSha}</span>`,
},
false,
);
},
},
methods: {
handleToggle(e, checkTarget) {
if (!checkTarget || e.target === this.$refs.header) {
this.$emit('toggleFile');
}
},
showForkMessage() {
this.$emit('showForkMessage');
},
},
};
</script>
<template>
<div
ref="header"
class="js-file-title file-title file-title-flex-parent"
@click="handleToggle($event, true)"
>
<div class="file-header-content">
<icon
v-if="collapsible"
:name="collapseIcon"
:size="16"
aria-hidden="true"
class="diff-toggle-caret"
@click.stop="handleToggle"
/>
<a
ref="titleWrapper"
:href="titleLink"
>
<i
:class="`fa-${icon}`"
class="fa fa-fw"
aria-hidden="true"
></i>
<span v-if="diffFile.renamedFile">
<strong
v-tooltip
:title="diffFile.oldPath"
class="file-title-name"
data-container="body"
>
{{ diffFile.oldPath }}
</strong>
<strong
v-tooltip
:title="diffFile.newPath"
class="file-title-name"
data-container="body"
>
{{ diffFile.newPath }}
</strong>
</span>
<strong
v-tooltip
v-else
:title="filePath"
class="file-title-name"
data-container="body"
>
{{ filePath }}
</strong>
</a>
<clipboard-button
:title="__('Copy file path to clipboard')"
:text="diffFile.filePath"
css-class="btn-default btn-transparent btn-clipboard"
/>
<small
v-if="diffFile.modeChanged"
ref="fileMode"
>
{{ diffFile.aMode }} {{ diffFile.bMode }}
</small>
<span
v-if="isUsingLfs"
class="label label-lfs append-right-5"
>
{{ __('LFS') }}
</span>
</div>
<div
v-if="!diffFile.submodule && addMergeRequestButtons"
class="file-actions d-none d-md-block"
>
<template
v-if="diffFile.blob && diffFile.blob.readableText"
>
<button
:class="{ active: isDiscussionsExpanded }"
:title="s__('MergeRequests|Toggle comments for this file')"
class="btn js-toggle-diff-comments"
type="button"
>
<icon name="comment" />
</button>
<edit-button
v-if="!diffFile.deletedFile"
:current-user="currentUser"
:edit-path="diffFile.editPath"
:can-modify-blob="diffFile.canModifyBlob"
@showForkMessage="showForkMessage"
/>
</template>
<a
v-if="diffFile.replacedViewPath"
:href="diffFile.replacedViewPath"
class="btn view-file js-view-file"
v-html="viewReplacedFileButtonText"
>
</a>
<a
:href="diffFile.viewPath"
class="btn view-file js-view-file"
v-html="viewFileButtonText"
>
</a>
<a
v-tooltip
v-if="diffFile.externalUrl"
:href="diffFile.externalUrl"
:title="`View on ${diffFile.formattedExternalUrl}`"
target="_blank"
rel="noopener noreferrer"
class="btn btn-file-option"
>
<icon name="external-link" />
</a>
</div>
</div>
</template>

View File

@ -0,0 +1,105 @@
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { pluralize, truncate } from '~/lib/utils/text_utility';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants';
export default {
directives: {
tooltip,
},
components: {
Icon,
UserAvatarImage,
},
props: {
discussions: {
type: Array,
required: true,
},
},
computed: {
discussionsExpanded() {
return this.discussions.every(discussion => discussion.expanded);
},
allDiscussions() {
return this.discussions.reduce((acc, note) => acc.concat(note.notes), []);
},
notesInGutter() {
return this.allDiscussions.slice(0, COUNT_OF_AVATARS_IN_GUTTER).map(n => ({
note: n.note,
author: n.author,
}));
},
moreCount() {
return this.allDiscussions.length - this.notesInGutter.length;
},
moreText() {
if (this.moreCount === 0) {
return '';
}
return pluralize(`${this.moreCount} more comment`, this.moreCount);
},
},
methods: {
...mapActions(['toggleDiscussion']),
getTooltipText(noteData) {
let note = noteData.note;
if (note.length > LENGTH_OF_AVATAR_TOOLTIP) {
note = truncate(note, LENGTH_OF_AVATAR_TOOLTIP);
}
return `${noteData.author.name}: ${note}`;
},
toggleDiscussions() {
this.discussions.forEach(discussion => {
this.toggleDiscussion({
discussionId: discussion.id,
});
});
},
},
};
</script>
<template>
<div class="diff-comment-avatar-holders">
<button
v-if="discussionsExpanded"
type="button"
aria-label="Show comments"
class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button"
@click="toggleDiscussions"
>
<icon
:size="12"
name="collapse"
/>
</button>
<template v-else>
<user-avatar-image
v-for="note in notesInGutter"
:key="note.id"
:img-src="note.author.avatar_url"
:tooltip-text="getTooltipText(note)"
:size="19"
class="diff-comment-avatar js-diff-comment-avatar"
@click.native="toggleDiscussions"
/>
<span
v-tooltip
v-if="moreText"
:title="moreText"
class="diff-comments-more-count has-tooltip js-diff-comment-avatar js-diff-comment-plus"
data-container="body"
data-placement="top"
role="button"
@click="toggleDiscussions"
>+{{ moreCount }}</span>
</template>
</div>
</template>

View File

@ -0,0 +1,203 @@
<script>
import createFlash from '~/flash';
import { s__ } from '~/locale';
import { mapState, mapGetters, mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import {
MATCH_LINE_TYPE,
CONTEXT_LINE_TYPE,
OLD_NO_NEW_LINE_TYPE,
NEW_NO_NEW_LINE_TYPE,
LINE_POSITION_RIGHT,
UNFOLD_COUNT,
} from '../constants';
import * as utils from '../store/utils';
export default {
components: {
DiffGutterAvatars,
Icon,
},
props: {
fileHash: {
type: String,
required: true,
},
contextLinesPath: {
type: String,
required: true,
},
lineType: {
type: String,
required: false,
default: '',
},
lineNumber: {
type: Number,
required: false,
default: 0,
},
lineCode: {
type: String,
required: false,
default: '',
},
linePosition: {
type: String,
required: false,
default: '',
},
metaData: {
type: Object,
required: false,
default: () => ({}),
},
showCommentButton: {
type: Boolean,
required: false,
default: false,
},
isBottom: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState({
diffViewType: state => state.diffs.diffViewType,
diffFiles: state => state.diffs.diffFiles,
}),
...mapGetters(['isLoggedIn', 'discussionsByLineCode']),
isMatchLine() {
return this.lineType === MATCH_LINE_TYPE;
},
isContextLine() {
return this.lineType === CONTEXT_LINE_TYPE;
},
isMetaLine() {
return this.lineType === OLD_NO_NEW_LINE_TYPE || this.lineType === NEW_NO_NEW_LINE_TYPE;
},
lineHref() {
return this.lineCode ? `#${this.lineCode}` : '#';
},
shouldShowCommentButton() {
return (
this.isLoggedIn &&
this.showCommentButton &&
!this.isMatchLine &&
!this.isContextLine &&
!this.hasDiscussions &&
!this.isMetaLine
);
},
discussions() {
return this.discussionsByLineCode[this.lineCode] || [];
},
hasDiscussions() {
return this.discussions.length > 0;
},
shouldShowAvatarsOnGutter() {
let render = this.hasDiscussions && this.showCommentButton;
if (!this.lineType && this.linePosition === LINE_POSITION_RIGHT) {
render = false;
}
return render;
},
},
methods: {
...mapActions(['loadMoreLines']),
handleCommentButton() {
this.$emit('showCommentForm', { lineCode: this.lineCode });
},
handleLoadMoreLines() {
if (this.isRequesting) {
return;
}
this.isRequesting = true;
const endpoint = this.contextLinesPath;
const oldLineNumber = this.metaData.oldPos || 0;
const newLineNumber = this.metaData.newPos || 0;
const offset = newLineNumber - oldLineNumber;
const bottom = this.isBottom;
const fileHash = this.fileHash;
const view = this.diffViewType;
let unfold = true;
let lineNumber = newLineNumber - 1;
let since = lineNumber - UNFOLD_COUNT;
let to = lineNumber;
if (bottom) {
lineNumber = newLineNumber + 1;
since = lineNumber;
to = lineNumber + UNFOLD_COUNT;
} else {
const diffFile = utils.findDiffFile(this.diffFiles, this.fileHash);
const indexForInline = utils.findIndexInInlineLines(diffFile.highlightedDiffLines, {
oldLineNumber,
newLineNumber,
});
const prevLine = diffFile.highlightedDiffLines[indexForInline - 2];
const prevLineNumber = (prevLine && prevLine.newLine) || 0;
if (since <= prevLineNumber + 1) {
since = prevLineNumber + 1;
unfold = false;
}
}
const params = { since, to, bottom, offset, unfold, view };
const lineNumbers = { oldLineNumber, newLineNumber };
this.loadMoreLines({ endpoint, params, lineNumbers, fileHash })
.then(() => {
this.isRequesting = false;
})
.catch(() => {
createFlash(s__('Diffs|Something went wrong while fetching diff lines.'));
this.isRequesting = false;
});
},
},
};
</script>
<template>
<div>
<span
v-if="isMatchLine"
class="context-cell"
role="button"
@click="handleLoadMoreLines"
>...</span>
<template
v-else
>
<button
v-show="shouldShowCommentButton"
type="button"
class="add-diff-note js-add-diff-note-button"
title="Add a comment to this line"
@click="handleCommentButton"
>
<icon
:size="12"
name="comment"
/>
</button>
<a
v-if="lineNumber"
:data-linenumber="lineNumber"
:href="lineHref"
>
</a>
<diff-gutter-avatars
v-if="shouldShowAvatarsOnGutter"
:discussions="discussions"
/>
</template>
</div>
</template>

View File

@ -0,0 +1,93 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import noteForm from '../../notes/components/note_form.vue';
import { getNoteFormData } from '../store/utils';
export default {
components: {
noteForm,
},
props: {
diffFile: {
type: Object,
required: true,
},
diffLines: {
type: Array,
required: true,
},
line: {
type: Object,
required: true,
},
position: {
type: String,
required: false,
default: '',
},
noteTargetLine: {
type: Object,
required: true,
},
},
computed: {
...mapState({
noteableData: state => state.notes.noteableData,
diffViewType: state => state.diffs.diffViewType,
}),
...mapGetters(['noteableType', 'getNotesDataByProp']),
},
methods: {
...mapActions(['cancelCommentForm', 'saveNote', 'fetchDiscussions']),
handleCancelCommentForm() {
this.cancelCommentForm({
lineCode: this.line.lineCode,
});
},
handleSaveNote(note) {
const postData = getNoteFormData({
note,
noteableData: this.noteableData,
noteableType: this.noteableType,
noteTargetLine: this.noteTargetLine,
diffViewType: this.diffViewType,
diffFile: this.diffFile,
linePosition: this.position,
});
this.saveNote(postData)
.then(() => {
const endpoint = this.getNotesDataByProp('discussionsPath');
this.fetchDiscussions(endpoint)
.then(() => {
this.handleCancelCommentForm();
})
.catch(() => {
createFlash(s__('MergeRequests|Updating discussions failed'));
});
})
.catch(() => {
createFlash(s__('MergeRequests|Saving the comment failed'));
});
},
},
};
</script>
<template>
<div
class="content discussion-form discussion-form-container discussion-notes"
>
<note-form
:is-editing="true"
:line-code="line.lineCode"
save-button-title="Comment"
class="diff-comment-form"
@cancelForm="handleCancelCommentForm"
@handleFormUpdate="handleSaveNote"
/>
</div>
</template>

View File

@ -0,0 +1,42 @@
<script>
export default {
props: {
editPath: {
type: String,
required: true,
},
currentUser: {
type: Object,
required: true,
},
canModifyBlob: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
handleEditClick(evt) {
if (!this.currentUser || this.canModifyBlob) {
// if we can Edit, do default Edit button behavior
return;
}
if (this.currentUser.canFork && this.currentUser.canCreateMergeRequest) {
evt.preventDefault();
this.$emit('showForkMessage');
}
},
},
};
</script>
<template>
<a
:href="editPath"
class="btn btn-default js-edit-blob"
@click="handleEditClick"
>
Edit
</a>
</template>

View File

@ -0,0 +1,51 @@
<script>
export default {
props: {
total: {
type: String,
required: true,
},
visible: {
type: Number,
required: true,
},
plainDiffPath: {
type: String,
required: true,
},
emailPatchPath: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="alert alert-warning">
<h4>
{{ __('Too many changes to show.') }}
<div class="pull-right">
<a
:href="plainDiffPath"
class="btn btn-sm"
>
{{ __('Plain diff') }}
</a>
<a
:href="emailPatchPath"
class="btn btn-sm"
>
{{ __('Email patch') }}
</a>
</div>
</h4>
<p>
To preserve performance only
<strong>
{{ visible }} of {{ total }}
</strong>
files are displayed.
</p>
</div>
</template>

View File

@ -0,0 +1,117 @@
<script>
import diffContentMixin from '../mixins/diff_content';
import {
MATCH_LINE_TYPE,
CONTEXT_LINE_TYPE,
OLD_NO_NEW_LINE_TYPE,
NEW_NO_NEW_LINE_TYPE,
LINE_HOVER_CLASS_NAME,
LINE_UNFOLD_CLASS_NAME,
} from '../constants';
export default {
mixins: [diffContentMixin],
methods: {
handleMouse(lineCode, isOver) {
this.hoveredLineCode = isOver ? lineCode : null;
},
getLineClass(line) {
const isSameLine = this.hoveredLineCode && this.hoveredLineCode === line.lineCode;
const isMatchLine = line.type === MATCH_LINE_TYPE;
const isContextLine = line.type === CONTEXT_LINE_TYPE;
const isMetaLine = line.type === OLD_NO_NEW_LINE_TYPE || line.type === NEW_NO_NEW_LINE_TYPE;
return {
[line.type]: line.type,
[LINE_UNFOLD_CLASS_NAME]: isMatchLine,
[LINE_HOVER_CLASS_NAME]:
this.isLoggedIn && isSameLine && !isMatchLine && !isContextLine && !isMetaLine,
};
},
},
};
</script>
<template>
<table
:class="userColorScheme"
:data-commit-id="commitId"
class="code diff-wrap-lines js-syntax-highlight text-file">
<tbody>
<template
v-for="(line, index) in normalizedDiffLines"
>
<tr
:id="line.lineCode || `${fileHash}_${line.oldLine}_${line.newLine}`"
:key="line.lineCode"
:class="getRowClass(line)"
class="line_holder"
@mouseover="handleMouse(line.lineCode, true)"
@mouseout="handleMouse(line.lineCode, false)"
>
<td
:class="getLineClass(line)"
class="diff-line-num old_line"
>
<diff-line-gutter-content
:file-hash="fileHash"
:line-type="line.type"
:line-code="line.lineCode"
:line-number="line.oldLine"
:meta-data="line.metaData"
:show-comment-button="true"
:context-lines-path="diffFile.contextLinesPath"
:is-bottom="index + 1 === diffLinesLength"
@showCommentForm="handleShowCommentForm"
/>
</td>
<td
:class="getLineClass(line)"
class="diff-line-num new_line"
>
<diff-line-gutter-content
:file-hash="fileHash"
:line-type="line.type"
:line-code="line.lineCode"
:line-number="line.newLine"
:meta-data="line.metaData"
:is-bottom="index + 1 === diffLinesLength"
:context-lines-path="diffFile.contextLinesPath"
/>
</td>
<td
:class="line.type"
class="line_content"
v-html="line.richText"
>
</td>
</tr>
<tr
v-if="isDiscussionExpanded(line.lineCode) || diffLineCommentForms[line.lineCode]"
:key="index"
:class="discussionsByLineCode[line.lineCode] ? '' : 'js-temp-notes-holder'"
class="notes_holder"
>
<td
class="notes_line"
colspan="2"
></td>
<td class="notes_content">
<div class="content">
<diff-discussions
:discussions="discussionsByLineCode[line.lineCode] || []"
/>
<diff-line-note-form
v-if="diffLineCommentForms[line.lineCode]"
:diff-file="diffFile"
:diff-lines="diffLines"
:line="line"
:note-target-line="diffLines[index]"
/>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</template>

View File

@ -0,0 +1,49 @@
<script>
import { mapState } from 'vuex';
import emptyImage from '~/../../views/shared/icons/_mr_widget_empty_state.svg';
export default {
data() {
return {
emptyImage,
};
},
computed: {
...mapState({
sourceBranch: state => state.notes.noteableData.source_branch,
targetBranch: state => state.notes.noteableData.target_branch,
newBlobPath: state => state.notes.noteableData.new_blob_path,
}),
},
};
</script>
<template>
<div
class="row empty-state nothing-here-block"
>
<div class="col-xs-12">
<div class="svg-content">
<span
v-html="emptyImage"
></span>
</div>
</div>
<div class="col-xs-12">
<div class="text-content text-center">
No changes between
<span class="ref-name">{{ sourceBranch }}</span>
and
<span class="ref-name">{{ targetBranch }}</span>
<div class="text-center">
<a
:href="newBlobPath"
class="btn btn-save"
>
{{ __('Create commit') }}
</a>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,224 @@
<script>
import diffContentMixin from '../mixins/diff_content';
import {
EMPTY_CELL_TYPE,
MATCH_LINE_TYPE,
CONTEXT_LINE_TYPE,
OLD_NO_NEW_LINE_TYPE,
NEW_NO_NEW_LINE_TYPE,
LINE_HOVER_CLASS_NAME,
LINE_UNFOLD_CLASS_NAME,
LINE_POSITION_RIGHT,
} from '../constants';
export default {
mixins: [diffContentMixin],
computed: {
parallelDiffLines() {
return this.normalizedDiffLines.map(line => {
if (!line.left) {
Object.assign(line, { left: { type: EMPTY_CELL_TYPE } });
} else if (!line.right) {
Object.assign(line, { right: { type: EMPTY_CELL_TYPE } });
}
return line;
});
},
},
methods: {
hasDiscussion(line) {
const discussions = this.discussionsByLineCode;
const hasDiscussion = discussions[line.left.lineCode] || discussions[line.right.lineCode];
return hasDiscussion;
},
getClassName(line, position) {
const { type, lineCode } = line[position];
const isMatchLine = type === MATCH_LINE_TYPE;
const isContextLine = !isMatchLine && type !== EMPTY_CELL_TYPE && type !== CONTEXT_LINE_TYPE;
const isMetaLine = type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE;
const isSameLine = this.hoveredLineCode && this.hoveredLineCode === lineCode;
const isSameSection = position === this.hoveredSection;
return {
[type]: type,
[LINE_UNFOLD_CLASS_NAME]: isMatchLine,
[LINE_HOVER_CLASS_NAME]:
this.isLoggedIn && isContextLine && isSameLine && isSameSection && !isMetaLine,
};
},
handleMouse(e, line, isHover) {
if (isHover) {
const cell = e.target.closest('td');
if (this.$refs.leftLines.indexOf(cell) > -1) {
this.hoveredLineCode = line.left.lineCode;
this.hoveredSection = 'left';
} else if (this.$refs.rightLines.indexOf(cell) > -1) {
this.hoveredLineCode = line.right.lineCode;
this.hoveredSection = 'right';
}
} else {
this.hoveredLineCode = null;
this.hoveredSection = null;
}
},
shouldRenderDiscussionsRow(line) {
const hasDiscussion = this.hasDiscussion(line) && this.hasAnyExpandedDiscussion(line);
const hasCommentFormOnLeft = this.diffLineCommentForms[line.left.lineCode];
const hasCommentFormOnRight = this.diffLineCommentForms[line.right.lineCode];
return hasDiscussion || hasCommentFormOnLeft || hasCommentFormOnRight;
},
shouldRenderDiscussions(line, position) {
const { lineCode } = line[position];
let render = this.discussionsByLineCode[lineCode] && this.isDiscussionExpanded(lineCode);
// Avoid rendering context line discussions on the right side in parallel view
if (position === LINE_POSITION_RIGHT) {
render = render && line.right.type;
}
return render;
},
hasAnyExpandedDiscussion(line) {
const isLeftExpanded = this.isDiscussionExpanded(line.left.lineCode);
const isRightExpanded = this.isDiscussionExpanded(line.right.lineCode);
return isLeftExpanded || isRightExpanded;
},
getLineCode(line, side) {
const lineCode = side.lineCode;
if (lineCode) {
return lineCode;
}
return `${this.fileHash}_${line.left.oldLine}_${line.right.newLine}`;
},
},
};
</script>
<template>
<div
:class="userColorScheme"
:data-commit-id="commitId"
class="code diff-wrap-lines js-syntax-highlight text-file">
<table>
<tbody>
<template
v-for="(line, index) in parallelDiffLines"
>
<tr
:key="index"
:class="getRowClass(line)"
class="line_holder parallel"
@mouseover="handleMouse($event, line, true)"
@mouseout="handleMouse($event, line, false)"
>
<td
ref="leftLines"
:class="getClassName(line, 'left')"
class="diff-line-num old_line"
>
<diff-line-gutter-content
:file-hash="fileHash"
:line-type="line.left.type"
:line-code="line.left.lineCode"
:line-number="line.left.oldLine"
:meta-data="line.left.metaData"
:show-comment-button="true"
:context-lines-path="diffFile.contextLinesPath"
:is-bottom="index + 1 === diffLinesLength"
line-position="left"
@showCommentForm="handleShowCommentForm"
/>
</td>
<td
ref="leftLines"
:class="getClassName(line, 'left')"
:id="getLineCode(line, line.left)"
class="line_content parallel left-side"
v-html="line.left.richText"
>
</td>
<td
ref="rightLines"
:class="getClassName(line, 'right')"
class="diff-line-num new_line"
>
<diff-line-gutter-content
:file-hash="fileHash"
:line-type="line.right.type"
:line-code="line.right.lineCode"
:line-number="line.right.newLine"
:meta-data="line.right.metaData"
:show-comment-button="true"
:context-lines-path="diffFile.contextLinesPath"
:is-bottom="index + 1 === diffLinesLength"
line-position="right"
@showCommentForm="handleShowCommentForm"
/>
</td>
<td
ref="rightLines"
:class="getClassName(line, 'right')"
:id="getLineCode(line, line.right)"
class="line_content parallel right-side"
v-html="line.right.richText"
>
</td>
</tr>
<tr
v-if="shouldRenderDiscussionsRow(line)"
:key="line.left.lineCode || line.right.lineCode"
:class="hasDiscussion(line) ? '' : 'js-temp-notes-holder'"
class="notes_holder"
>
<td class="notes_line old"></td>
<td class="notes_content parallel old">
<div
v-if="shouldRenderDiscussions(line, 'left')"
class="content"
>
<diff-discussions
:discussions="discussionsByLineCode[line.left.lineCode]"
/>
</div>
<diff-line-note-form
v-if="diffLineCommentForms[line.left.lineCode] &&
diffLineCommentForms[line.left.lineCode]"
:diff-file="diffFile"
:diff-lines="diffLines"
:line="line.left"
:note-target-line="diffLines[index].left"
position="left"
/>
</td>
<td class="notes_line new"></td>
<td class="notes_content parallel new">
<div
v-if="shouldRenderDiscussions(line, 'right')"
class="content"
>
<diff-discussions
:discussions="discussionsByLineCode[line.right.lineCode]"
/>
</div>
<diff-line-note-form
v-if="diffLineCommentForms[line.right.lineCode] &&
diffLineCommentForms[line.right.lineCode] && line.right.type"
:diff-file="diffFile"
:diff-lines="diffLines"
:line="line.right"
:note-target-line="diffLines[index].right"
position="right"
/>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</template>

View File

@ -0,0 +1,24 @@
export const INLINE_DIFF_VIEW_TYPE = 'inline';
export const PARALLEL_DIFF_VIEW_TYPE = 'parallel';
export const MATCH_LINE_TYPE = 'match';
export const OLD_NO_NEW_LINE_TYPE = 'old-nonewline';
export const NEW_NO_NEW_LINE_TYPE = 'new-nonewline';
export const CONTEXT_LINE_TYPE = 'context';
export const EMPTY_CELL_TYPE = 'empty-cell';
export const COMMENT_FORM_TYPE = 'commentForm';
export const DIFF_NOTE_TYPE = 'DiffNote';
export const NEW_LINE_TYPE = 'new';
export const OLD_LINE_TYPE = 'old';
export const TEXT_DIFF_POSITION_TYPE = 'text';
export const LINE_POSITION_LEFT = 'left';
export const LINE_POSITION_RIGHT = 'right';
export const DIFF_VIEW_COOKIE_NAME = 'diff_view';
export const LINE_HOVER_CLASS_NAME = 'is-over';
export const LINE_UNFOLD_CLASS_NAME = 'unfold js-unfold';
export const CONTEXT_LINE_CLASS_NAME = 'diff-expanded';
export const UNFOLD_COUNT = 20;
export const COUNT_OF_AVATARS_IN_GUTTER = 3;
export const LENGTH_OF_AVATAR_TOOLTIP = 17;

View File

@ -0,0 +1,39 @@
import Vue from 'vue';
import { mapState } from 'vuex';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import diffsApp from './components/app.vue';
export default function initDiffsApp(store) {
return new Vue({
el: '#js-diffs-app',
name: 'MergeRequestDiffs',
components: {
diffsApp,
},
store,
data() {
const { dataset } = document.querySelector(this.$options.el);
return {
endpoint: dataset.endpoint,
currentUser: convertObjectPropsToCamelCase(JSON.parse(dataset.currentUserData), {
deep: true,
}),
};
},
computed: {
...mapState({
activeTab: state => state.page.activeTab,
}),
},
render(createElement) {
return createElement('diffs-app', {
props: {
endpoint: this.endpoint,
currentUser: this.currentUser,
shouldShow: this.activeTab === 'diffs',
},
});
},
});
}

View File

@ -0,0 +1,38 @@
export default {
props: {
diffFiles: {
type: Array,
required: true,
},
},
methods: {
fileChangedIcon(diffFile) {
if (diffFile.deletedFile) {
return 'file-deletion';
} else if (diffFile.newFile) {
return 'file-addition';
}
return 'file-modified';
},
fileChangedClass(diffFile) {
if (diffFile.deletedFile) {
return 'cred';
} else if (diffFile.newFile) {
return 'cgreen';
}
return '';
},
truncatedDiffPath(path) {
const maxLength = 60;
if (path.length > maxLength) {
const start = path.length - maxLength;
const end = start + maxLength;
return `...${path.slice(start, end)}`;
}
return path;
},
},
};

View File

@ -0,0 +1,89 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import diffDiscussions from '../components/diff_discussions.vue';
import diffLineGutterContent from '../components/diff_line_gutter_content.vue';
import diffLineNoteForm from '../components/diff_line_note_form.vue';
import { trimFirstCharOfLineContent } from '../store/utils';
import { CONTEXT_LINE_TYPE, CONTEXT_LINE_CLASS_NAME } from '../constants';
export default {
props: {
diffFile: {
type: Object,
required: true,
},
diffLines: {
type: Array,
required: true,
},
},
data() {
return {
hoveredLineCode: null,
hoveredSection: null,
};
},
components: {
diffDiscussions,
diffLineNoteForm,
diffLineGutterContent,
},
computed: {
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
...mapGetters(['discussionsByLineCode', 'isLoggedIn', 'commit']),
commitId() {
return this.commit && this.commit.id;
},
userColorScheme() {
return window.gon.user_color_scheme;
},
normalizedDiffLines() {
return this.diffLines.map(line => {
if (line.richText) {
return this.trimFirstChar(line);
}
if (line.left) {
Object.assign(line, { left: this.trimFirstChar(line.left) });
}
if (line.right) {
Object.assign(line, { right: this.trimFirstChar(line.right) });
}
return line;
});
},
diffLinesLength() {
return this.normalizedDiffLines.length;
},
fileHash() {
return this.diffFile.fileHash;
},
},
methods: {
...mapActions(['showCommentForm', 'cancelCommentForm']),
getRowClass(line) {
const isContextLine = line.left
? line.left.type === CONTEXT_LINE_TYPE
: line.type === CONTEXT_LINE_TYPE;
return {
[line.type]: line.type,
[CONTEXT_LINE_CLASS_NAME]: isContextLine,
};
},
trimFirstChar(line) {
return trimFirstCharOfLineContent(line);
},
handleShowCommentForm(params) {
this.showCommentForm({ lineCode: params.lineCode });
},
isDiscussionExpanded(lineCode) {
const discussions = this.discussionsByLineCode[lineCode];
return discussions ? discussions.every(discussion => discussion.expanded) : false;
},
},
};

View File

@ -0,0 +1,99 @@
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import Cookies from 'js-cookie';
import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import * as types from './mutation_types';
import {
PARALLEL_DIFF_VIEW_TYPE,
INLINE_DIFF_VIEW_TYPE,
DIFF_VIEW_COOKIE_NAME,
} from '../constants';
export const setEndpoint = ({ commit }, endpoint) => {
commit(types.SET_ENDPOINT, endpoint);
};
export const setLoadingState = ({ commit }, state) => {
commit(types.SET_LOADING, state);
};
export const fetchDiffFiles = ({ state, commit }) => {
commit(types.SET_LOADING, true);
return axios
.get(state.endpoint)
.then(res => {
commit(types.SET_LOADING, false);
commit(types.SET_MERGE_REQUEST_DIFFS, res.data.merge_request_diffs || []);
commit(types.SET_DIFF_DATA, res.data);
return Vue.nextTick();
})
.then(handleLocationHash);
};
export const setInlineDiffViewType = ({ commit }) => {
commit(types.SET_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE);
Cookies.set(DIFF_VIEW_COOKIE_NAME, INLINE_DIFF_VIEW_TYPE);
const url = mergeUrlParams({ view: INLINE_DIFF_VIEW_TYPE }, window.location.href);
historyPushState(url);
};
export const setParallelDiffViewType = ({ commit }) => {
commit(types.SET_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE);
Cookies.set(DIFF_VIEW_COOKIE_NAME, PARALLEL_DIFF_VIEW_TYPE);
const url = mergeUrlParams({ view: PARALLEL_DIFF_VIEW_TYPE }, window.location.href);
historyPushState(url);
};
export const showCommentForm = ({ commit }, params) => {
commit(types.ADD_COMMENT_FORM_LINE, params);
};
export const cancelCommentForm = ({ commit }, params) => {
commit(types.REMOVE_COMMENT_FORM_LINE, params);
};
export const loadMoreLines = ({ commit }, options) => {
const { endpoint, params, lineNumbers, fileHash } = options;
params.from_merge_request = true;
return axios.get(endpoint, { params }).then(res => {
const contextLines = res.data || [];
commit(types.ADD_CONTEXT_LINES, {
lineNumbers,
contextLines,
params,
fileHash,
});
});
};
export const loadCollapsedDiff = ({ commit }, file) =>
axios.get(file.loadCollapsedDiffUrl).then(res => {
commit(types.ADD_COLLAPSED_DIFFS, {
file,
data: res.data,
});
});
export const expandAllFiles = ({ commit }) => {
commit(types.EXPAND_ALL_FILES);
};
export default {
setEndpoint,
setLoadingState,
fetchDiffFiles,
setInlineDiffViewType,
setParallelDiffViewType,
showCommentForm,
cancelCommentForm,
loadMoreLines,
loadCollapsedDiff,
expandAllFiles,
};

View File

@ -0,0 +1,16 @@
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
export default {
isParallelView(state) {
return state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
},
isInlineView(state) {
return state.diffViewType === INLINE_DIFF_VIEW_TYPE;
},
areAllFilesCollapsed(state) {
return state.diffFiles.every(file => file.collapsed);
},
commit(state) {
return state.commit;
},
};

View File

@ -0,0 +1,11 @@
import Vue from 'vue';
import Vuex from 'vuex';
import diffsModule from './modules';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
diffs: diffsModule,
},
});

View File

@ -0,0 +1,25 @@
import Cookies from 'js-cookie';
import { getParameterValues } from '~/lib/utils/url_utility';
import actions from '../actions';
import getters from '../getters';
import mutations from '../mutations';
import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants';
const viewTypeFromQueryString = getParameterValues('view')[0];
const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME);
const defaultViewType = INLINE_DIFF_VIEW_TYPE;
export default {
state: {
isLoading: true,
endpoint: '',
commit: null,
diffFiles: [],
mergeRequestDiffs: [],
diffLineCommentForms: {},
diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
},
getters,
actions,
mutations,
};

View File

@ -0,0 +1,11 @@
export const SET_ENDPOINT = 'SET_ENDPOINT';
export const SET_LOADING = 'SET_LOADING';
export const SET_DIFF_DATA = 'SET_DIFF_DATA';
export const SET_DIFF_FILES = 'SET_DIFF_FILES';
export const SET_DIFF_VIEW_TYPE = 'SET_DIFF_VIEW_TYPE';
export const SET_MERGE_REQUEST_DIFFS = 'SET_MERGE_REQUEST_DIFFS';
export const ADD_COMMENT_FORM_LINE = 'ADD_COMMENT_FORM_LINE';
export const REMOVE_COMMENT_FORM_LINE = 'REMOVE_COMMENT_FORM_LINE';
export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES';
export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS';
export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';

View File

@ -0,0 +1,85 @@
import Vue from 'vue';
import _ from 'underscore';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { findDiffFile, addLineReferences, removeMatchLine, addContextLines } from './utils';
import * as types from './mutation_types';
export default {
[types.SET_ENDPOINT](state, endpoint) {
Object.assign(state, { endpoint });
},
[types.SET_LOADING](state, isLoading) {
Object.assign(state, { isLoading });
},
[types.SET_DIFF_DATA](state, data) {
Object.assign(state, {
...convertObjectPropsToCamelCase(data, { deep: true }),
});
},
[types.SET_DIFF_FILES](state, diffFiles) {
Object.assign(state, {
diffFiles: convertObjectPropsToCamelCase(diffFiles, { deep: true }),
});
},
[types.SET_MERGE_REQUEST_DIFFS](state, mergeRequestDiffs) {
Object.assign(state, {
mergeRequestDiffs: convertObjectPropsToCamelCase(mergeRequestDiffs, { deep: true }),
});
},
[types.SET_DIFF_VIEW_TYPE](state, diffViewType) {
Object.assign(state, { diffViewType });
},
[types.ADD_COMMENT_FORM_LINE](state, { lineCode }) {
Vue.set(state.diffLineCommentForms, lineCode, true);
},
[types.REMOVE_COMMENT_FORM_LINE](state, { lineCode }) {
Vue.delete(state.diffLineCommentForms, lineCode);
},
[types.ADD_CONTEXT_LINES](state, options) {
const { lineNumbers, contextLines, fileHash } = options;
const { bottom } = options.params;
const diffFile = findDiffFile(state.diffFiles, fileHash);
const { highlightedDiffLines, parallelDiffLines } = diffFile;
removeMatchLine(diffFile, lineNumbers, bottom);
const lines = addLineReferences(contextLines, lineNumbers, bottom);
addContextLines({
inlineLines: highlightedDiffLines,
parallelLines: parallelDiffLines,
contextLines: lines,
bottom,
lineNumbers,
});
},
[types.ADD_COLLAPSED_DIFFS](state, { file, data }) {
const normalizedData = convertObjectPropsToCamelCase(data, { deep: true });
const [newFileData] = normalizedData.diffFiles.filter(f => f.fileHash === file.fileHash);
if (newFileData) {
const index = _.findIndex(state.diffFiles, f => f.fileHash === file.fileHash);
state.diffFiles.splice(index, 1, newFileData);
}
},
[types.EXPAND_ALL_FILES](state) {
const diffFiles = [];
state.diffFiles.forEach((file) => {
diffFiles.push({
...file,
collapsed: false,
});
});
Object.assign(state, { diffFiles });
},
};

View File

@ -0,0 +1,172 @@
import _ from 'underscore';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import {
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
TEXT_DIFF_POSITION_TYPE,
DIFF_NOTE_TYPE,
NEW_LINE_TYPE,
OLD_LINE_TYPE,
MATCH_LINE_TYPE,
} from '../constants';
export function findDiffFile(files, hash) {
return files.filter(file => file.fileHash === hash)[0];
}
export const getReversePosition = linePosition => {
if (linePosition === LINE_POSITION_RIGHT) {
return LINE_POSITION_LEFT;
}
return LINE_POSITION_RIGHT;
};
export function getNoteFormData(params) {
const {
note,
noteableType,
noteableData,
diffFile,
noteTargetLine,
diffViewType,
linePosition,
} = params;
const position = JSON.stringify({
base_sha: diffFile.diffRefs.baseSha,
start_sha: diffFile.diffRefs.startSha,
head_sha: diffFile.diffRefs.headSha,
old_path: diffFile.oldPath,
new_path: diffFile.newPath,
position_type: TEXT_DIFF_POSITION_TYPE,
old_line: noteTargetLine.oldLine,
new_line: noteTargetLine.newLine,
});
const postData = {
view: diffViewType,
line_type: linePosition === LINE_POSITION_RIGHT ? NEW_LINE_TYPE : OLD_LINE_TYPE,
merge_request_diff_head_sha: diffFile.diffRefs.headSha,
in_reply_to_discussion_id: '',
note_project_id: '',
target_type: noteableData.targetType,
target_id: noteableData.id,
note: {
note,
position,
noteable_type: noteableType,
noteable_id: noteableData.id,
commit_id: '',
type: DIFF_NOTE_TYPE,
line_code: noteTargetLine.lineCode,
},
};
return {
endpoint: noteableData.create_note_path,
data: postData,
};
}
export const findIndexInInlineLines = (lines, lineNumbers) => {
const { oldLineNumber, newLineNumber } = lineNumbers;
return _.findIndex(
lines,
line => line.oldLine === oldLineNumber && line.newLine === newLineNumber,
);
};
export const findIndexInParallelLines = (lines, lineNumbers) => {
const { oldLineNumber, newLineNumber } = lineNumbers;
return _.findIndex(
lines,
line =>
line.left &&
line.right &&
line.left.oldLine === oldLineNumber &&
line.right.newLine === newLineNumber,
);
};
export function removeMatchLine(diffFile, lineNumbers, bottom) {
const indexForInline = findIndexInInlineLines(diffFile.highlightedDiffLines, lineNumbers);
const indexForParallel = findIndexInParallelLines(diffFile.parallelDiffLines, lineNumbers);
const factor = bottom ? 1 : -1;
diffFile.highlightedDiffLines.splice(indexForInline + factor, 1);
diffFile.parallelDiffLines.splice(indexForParallel + factor, 1);
}
export function addLineReferences(lines, lineNumbers, bottom) {
const { oldLineNumber, newLineNumber } = lineNumbers;
const lineCount = lines.length;
let matchLineIndex = -1;
const linesWithNumbers = lines.map((l, index) => {
const line = convertObjectPropsToCamelCase(l);
if (line.type === MATCH_LINE_TYPE) {
matchLineIndex = index;
} else {
Object.assign(line, {
oldLine: bottom ? oldLineNumber + index + 1 : oldLineNumber + index - lineCount,
newLine: bottom ? newLineNumber + index + 1 : newLineNumber + index - lineCount,
});
}
return line;
});
if (matchLineIndex > -1) {
const line = linesWithNumbers[matchLineIndex];
const targetLine = bottom
? linesWithNumbers[matchLineIndex - 1]
: linesWithNumbers[matchLineIndex + 1];
Object.assign(line, {
metaData: {
oldPos: targetLine.oldLine,
newPos: targetLine.newLine,
},
});
}
return linesWithNumbers;
}
export function addContextLines(options) {
const { inlineLines, parallelLines, contextLines, lineNumbers } = options;
const normalizedParallelLines = contextLines.map(line => ({
left: line,
right: line,
}));
if (options.bottom) {
inlineLines.push(...contextLines);
parallelLines.push(...normalizedParallelLines);
} else {
const inlineIndex = findIndexInInlineLines(inlineLines, lineNumbers);
const parallelIndex = findIndexInParallelLines(parallelLines, lineNumbers);
inlineLines.splice(inlineIndex, 0, ...contextLines);
parallelLines.splice(parallelIndex, 0, ...normalizedParallelLines);
}
}
export function trimFirstCharOfLineContent(line) {
if (!line.richText) {
return line;
}
const firstChar = line.richText.charAt(0);
if (firstChar === ' ' || firstChar === '+' || firstChar === '-') {
Object.assign(line, {
richText: line.richText.substring(1),
});
}
return line;
}

View File

@ -1,4 +1,4 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
/* eslint-disable consistent-return, no-new */
import $ from 'jquery';
import Flash from './flash';

View File

@ -1,4 +1,4 @@
/* eslint-disable func-names, no-underscore-dangle, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* eslint-disable func-names, no-underscore-dangle, no-var, one-var, one-var-declaration-per-line, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func */
/* global fuzzaldrinPlus */
import $ from 'jquery';

View File

@ -148,7 +148,6 @@ export default {
if (!parentGroup.isOpen) {
if (parentGroup.children.length === 0) {
parentGroup.isChildrenLoading = true;
// eslint-disable-next-line promise/catch-or-return
this.fetchGroups({
parentId: parentGroup.id,
})

View File

@ -117,7 +117,7 @@ export default {
class="btn btn-primary btn-sm btn-block"
@click="toggleIsSmall"
>
{{ __('Commit') }}
{{ __('Commit') }}
</button>
<p
class="text-center"

View File

@ -38,14 +38,17 @@ export default {
return this.modifiedFilesLength ? 'multi-file-modified' : '';
},
additionsTooltip() {
return sprintf(n__('1 %{type} addition', '%d %{type} additions', this.addedFilesLength), {
return sprintf(n__('1 %{type} addition', '%{count} %{type} additions', this.addedFilesLength), {
type: this.title.toLowerCase(),
count: this.addedFilesLength,
});
},
modifiedTooltip() {
return sprintf(
n__('1 %{type} modification', '%d %{type} modifications', this.modifiedFilesLength),
{ type: this.title.toLowerCase() },
n__('1 %{type} modification', '%{count} %{type} modifications', this.modifiedFilesLength), {
type: this.title.toLowerCase(),
count: this.modifiedFilesLength,
},
);
},
titleTooltip() {

View File

@ -91,7 +91,6 @@ export default class IntegrationSettingsForm {
}
}
/* eslint-disable promise/catch-or-return, no-new */
/**
* Test Integration config
*/

View File

@ -1,4 +1,4 @@
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, prefer-arrow-callback, max-len, no-unused-vars */
import $ from 'jquery';
import _ from 'underscore';

View File

@ -1,4 +1,4 @@
/* eslint-disable func-names, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
/* eslint-disable no-new, no-unused-vars, consistent-return, no-else-return */
/* global GitLab */
import $ from 'jquery';

View File

@ -1,4 +1,4 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
/* eslint-disable no-var, one-var, one-var-declaration-per-line, no-unused-vars, consistent-return, quotes, max-len */
import $ from 'jquery';
import axios from './lib/utils/axios_utils';

View File

@ -1,4 +1,4 @@
/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */
/* eslint-disable class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, func-names, max-len */
import $ from 'jquery';
import Sortable from 'sortablejs';

View File

@ -1,4 +1,4 @@
/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread */
/* eslint-disable no-useless-return, func-names, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, dot-notation, no-empty */
/* global Issuable */
/* global ListLabel */

View File

@ -1,10 +1,14 @@
import $ from 'jquery';
import Cookies from 'js-cookie';
import axios from './axios_utils';
import { getLocationHash } from './url_utility';
import { convertToCamelCase } from './text_utility';
import { isObject } from './type_utility';
export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
export const getPagePath = (index = 0) => {
const page = $('body').attr('data-page') || '';
return page.split(':')[index];
};
export const isInGroupsPage = () => getPagePath() === 'groups';
@ -34,17 +38,18 @@ export const checkPageAndAction = (page, action) => {
export const isInIssuePage = () => checkPageAndAction('issues', 'show');
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
export const isInEpicPage = () => checkPageAndAction('epics', 'show');
export const isInNoteablePage = () => isInIssuePage() || isInMRPage();
export const hasVueMRDiscussionsCookie = () => Cookies.get('vue_mr_discussions');
export const ajaxGet = url => axios.get(url, {
params: { format: 'js' },
responseType: 'text',
}).then(({ data }) => {
$.globalEval(data);
});
export const ajaxGet = url =>
axios
.get(url, {
params: { format: 'js' },
responseType: 'text',
})
.then(({ data }) => {
$.globalEval(data);
});
export const rstrip = (val) => {
export const rstrip = val => {
if (val) {
return val.replace(/\s+$/, '');
}
@ -60,7 +65,7 @@ export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventNa
closestSubmit.disable();
}
// eslint-disable-next-line func-names
return field.on(eventName, function () {
return field.on(eventName, function() {
if (rstrip($(this).val()) === '') {
return closestSubmit.disable();
}
@ -79,7 +84,7 @@ export const handleLocationHash = () => {
const target = document.getElementById(hash) || document.getElementById(`user-content-${hash}`);
const fixedTabs = document.querySelector('.js-tabs-affix');
const fixedDiffStats = document.querySelector('.js-diff-files-changed.is-stuck');
const fixedDiffStats = document.querySelector('.js-diff-files-changed');
const fixedNav = document.querySelector('.navbar-gitlab');
let adjustment = 0;
@ -102,7 +107,7 @@ export const handleLocationHash = () => {
// Check if element scrolled into viewport from above or below
// Courtesy http://stackoverflow.com/a/7557433/414749
export const isInViewport = (el) => {
export const isInViewport = el => {
const rect = el.getBoundingClientRect();
return (
@ -113,13 +118,13 @@ export const isInViewport = (el) => {
);
};
export const parseUrl = (url) => {
export const parseUrl = url => {
const parser = document.createElement('a');
parser.href = url;
return parser;
};
export const parseUrlPathname = (url) => {
export const parseUrlPathname = url => {
const parsedUrl = parseUrl(url);
// parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11
// We have to make sure we always have an absolute path.
@ -128,10 +133,14 @@ export const parseUrlPathname = (url) => {
// We can trust that each param has one & since values containing & will be encoded
// Remove the first character of search as it is always ?
export const getUrlParamsArray = () => window.location.search.slice(1).split('&').map((param) => {
const split = param.split('=');
return [decodeURI(split[0]), split[1]].join('=');
});
export const getUrlParamsArray = () =>
window.location.search
.slice(1)
.split('&')
.map(param => {
const split = param.split('=');
return [decodeURI(split[0]), split[1]].join('=');
});
export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
@ -141,18 +150,28 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
// 3) Middle-click or Mouse Wheel Click (e.which is 2)
export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2;
export const scrollToElement = (element) => {
export const contentTop = () => {
const perfBar = $('#js-peek').height() || 0;
const mrTabsHeight = $('.merge-request-tabs').height() || 0;
const headerHeight = $('.navbar-gitlab').height() || 0;
const diffFilesChanged = $('.js-diff-files-changed').height() || 0;
return perfBar + mrTabsHeight + headerHeight + diffFilesChanged;
};
export const scrollToElement = element => {
let $el = element;
if (!(element instanceof $)) {
$el = $(element);
}
const top = $el.offset().top;
const mrTabsHeight = $('.merge-request-tabs').height() || 0;
const headerHeight = $('.navbar-gitlab').height() || 0;
return $('body, html').animate({
scrollTop: top - mrTabsHeight - headerHeight,
}, 200);
return $('body, html').animate(
{
scrollTop: top - contentTop(),
},
200,
);
};
/**
@ -212,7 +231,8 @@ export const insertText = (target, text) => {
};
export const nodeMatchesSelector = (node, selector) => {
const matches = Element.prototype.matches ||
const matches =
Element.prototype.matches ||
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
@ -241,10 +261,10 @@ export const nodeMatchesSelector = (node, selector) => {
this will take in the headers from an API response and normalize them
this way we don't run into production issues when nginx gives us lowercased header keys
*/
export const normalizeHeaders = (headers) => {
export const normalizeHeaders = headers => {
const upperCaseHeaders = {};
Object.keys(headers || {}).forEach((e) => {
Object.keys(headers || {}).forEach(e => {
upperCaseHeaders[e.toUpperCase()] = headers[e];
});
@ -255,11 +275,11 @@ export const normalizeHeaders = (headers) => {
this will take in the getAllResponseHeaders result and normalize them
this way we don't run into production issues when nginx gives us lowercased header keys
*/
export const normalizeCRLFHeaders = (headers) => {
export const normalizeCRLFHeaders = headers => {
const headersObject = {};
const headersArray = headers.split('\n');
headersArray.forEach((header) => {
headersArray.forEach(header => {
const keyValue = header.split(': ');
headersObject[keyValue[0]] = keyValue[1];
});
@ -295,15 +315,13 @@ export const parseIntPagination = paginationInformation => ({
export const parseQueryStringIntoObject = (query = '') => {
if (query === '') return {};
return query
.split('&')
.reduce((acc, element) => {
const val = element.split('=');
Object.assign(acc, {
[val[0]]: decodeURIComponent(val[1]),
});
return acc;
}, {});
return query.split('&').reduce((acc, element) => {
const val = element.split('=');
Object.assign(acc, {
[val[0]]: decodeURIComponent(val[1]),
});
return acc;
}, {});
};
/**
@ -312,9 +330,13 @@ export const parseQueryStringIntoObject = (query = '') => {
*
* @param {Object} params
*/
export const objectToQueryString = (params = {}) => Object.keys(params).map(param => `${param}=${params[param]}`).join('&');
export const objectToQueryString = (params = {}) =>
Object.keys(params)
.map(param => `${param}=${params[param]}`)
.join('&');
export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname);
export const buildUrlWithCurrentLocation = param =>
(param ? `${window.location.pathname}${param}` : window.location.pathname);
/**
* Based on the current location and the string parameters provided
@ -322,7 +344,7 @@ export const buildUrlWithCurrentLocation = param => (param ? `${window.location.
*
* @param {String} param
*/
export const historyPushState = (newUrl) => {
export const historyPushState = newUrl => {
window.history.pushState({}, document.title, newUrl);
};
@ -371,7 +393,7 @@ export const backOff = (fn, timeout = 60000) => {
let timeElapsed = 0;
return new Promise((resolve, reject) => {
const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
const next = () => {
if (timeElapsed < timeout) {
@ -447,7 +469,8 @@ export const resetFavicon = () => {
};
export const setCiStatusFavicon = pageUrl =>
axios.get(pageUrl)
axios
.get(pageUrl)
.then(({ data }) => {
if (data && data.favicon) {
return setFaviconOverlay(data.favicon);
@ -469,28 +492,38 @@ export const spriteIcon = (icon, className = '') => {
* Reasoning for this method is to ensure consistent property
* naming conventions across JS code.
*/
export const convertObjectPropsToCamelCase = (obj = {}) => {
export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
if (obj === null) {
return {};
}
const initial = Array.isArray(obj) ? [] : {};
return Object.keys(obj).reduce((acc, prop) => {
const result = acc;
const val = obj[prop];
result[convertToCamelCase(prop)] = obj[prop];
if (options.deep && (isObject(val) || Array.isArray(val))) {
result[convertToCamelCase(prop)] = convertObjectPropsToCamelCase(val, options);
} else {
result[convertToCamelCase(prop)] = obj[prop];
}
return acc;
}, {});
}, initial);
};
export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
export const imagePath = imgUrl =>
`${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => {
// Click a .js-select-on-focus field, select the contents
// Prevent a mouseup event from deselecting the input
$(selector).on('focusin', function selectOnFocusCallback() {
$(this).select().one('mouseup', (e) => {
e.preventDefault();
});
$(this)
.select()
.one('mouseup', e => {
e.preventDefault();
});
});
};

View File

@ -1,7 +1,4 @@
import $ from 'jquery';
import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie } from './common_utils';
const isVueMRDiscussions = () => isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
import { isInIssuePage, isInMRPage, isInEpicPage } from './common_utils';
export const addClassIfElementExists = (element, className) => {
if (element) {
@ -9,4 +6,4 @@ export const addClassIfElementExists = (element, className) => {
}
};
export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isVueMRDiscussions();
export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isInMRPage();

View File

@ -1,4 +1,4 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, max-len */
/* eslint-disable func-names, no-var, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, max-len */
function notificationGranted(message, opts, onclick) {
var notification;

View File

@ -1,4 +1,4 @@
/* 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 */
/* eslint-disable func-names, no-var, no-param-reassign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, max-len, consistent-return, no-unused-vars, max-len */
import $ from 'jquery';
import { insertText } from '~/lib/utils/common_utils';

View File

@ -57,6 +57,14 @@ export const slugify = str => str.trim().toLowerCase();
*/
export const truncate = (string, maxLength) => `${string.substr(0, maxLength - 3)}...`;
/**
* Truncate SHA to 8 characters
*
* @param {String} sha
* @returns {String}
*/
export const truncateSha = sha => sha.substr(0, 8);
/**
* Capitalizes first character
*
@ -98,3 +106,16 @@ export const convertToSentenceCase = string => {
return splitWord.join(' ');
};
/**
* Splits camelCase or PascalCase words
* e.g. HelloWorld => Hello World
*
* @param {*} string
*/
export const splitCamelCase = string => (
string
.replace(/([A-Z]+)([A-Z][a-z])/g, ' $1 $2')
.replace(/([a-z\d])([A-Z])/g, '$1 $2')
.trim()
);

View File

@ -1,4 +1,4 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-else-return, max-len */
/* eslint-disable func-names, no-var, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, no-else-return, max-len */
import $ from 'jquery';
@ -142,14 +142,14 @@ LineHighlighter.prototype.highlightLine = function(lineNumber) {
//
// range - Array containing the starting and ending line numbers
LineHighlighter.prototype.highlightRange = function(range) {
var i, lineNumber, ref, ref1, results;
if (range[1]) {
results = [];
const results = [];
const ref = range[0] <= range[1] ? range : range.reverse();
// eslint-disable-next-line no-multi-assign
for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) {
for (let lineNumber = range[0]; lineNumber <= ref[1]; lineNumber += 1) {
results.push(this.highlightLine(lineNumber));
}
return results;
} else {
return this.highlightLine(range[0]);

View File

@ -1,4 +1,4 @@
/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-new, no-param-reassign, max-len */
/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-param-reassign, max-len */
/* global ace */
import Vue from 'vue';

View File

@ -1,4 +1,4 @@
/* eslint-disable no-param-reassign, comma-dangle */
/* eslint-disable no-param-reassign */
import Vue from 'vue';
import actionsMixin from '../mixins/line_conflict_actions';

View File

@ -1,4 +1,4 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, max-len, prefer-arrow-callback */
/* eslint-disable func-names, no-var, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, comma-dangle, max-len, prefer-arrow-callback */
import $ from 'jquery';
import { __ } from '~/locale';
@ -49,6 +49,7 @@ MergeRequest.prototype.initTabs = function() {
if (window.mrTabs) {
window.mrTabs.unbindEvents();
}
window.mrTabs = new MergeRequestTabs(this.opts);
};

View File

@ -1,6 +1,7 @@
/* eslint-disable no-new, class-methods-use-this */
import $ from 'jquery';
import Vue from 'vue';
import Cookies from 'js-cookie';
import axios from './lib/utils/axios_utils';
import flash from './flash';
@ -8,6 +9,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
import { parseUrlPathname, handleLocationHash, isMetaClick } from './lib/utils/common_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
import { getLocationHash } from './lib/utils/url_utility';
import initDiscussionTab from './image_diff/init_discussion_tab';
import Diff from './diff';
@ -70,11 +72,13 @@ export default class MergeRequestTabs {
const navbar = document.querySelector('.navbar-gitlab');
const peek = document.getElementById('js-peek');
const paddingTop = 16;
this.commitsTab = document.querySelector('.tab-content .commits.tab-pane');
this.diffsLoaded = false;
this.pipelinesLoaded = false;
this.commitsLoaded = false;
this.fixedLayoutPref = null;
this.eventHub = new Vue();
this.setUrl = setUrl !== undefined ? setUrl : true;
this.setCurrentAction = this.setCurrentAction.bind(this);
@ -149,7 +153,9 @@ export default class MergeRequestTabs {
this.resetViewContainer();
this.destroyPipelinesView();
} else if (this.isDiffAction(action)) {
this.loadDiff($target.attr('href'));
if (!isInVueNoteablePage()) {
this.loadDiff($target.attr('href'));
}
if (bp.getBreakpointSize() !== 'lg') {
this.shrinkView();
}
@ -157,6 +163,7 @@ export default class MergeRequestTabs {
this.expandViewContainer();
}
this.destroyPipelinesView();
this.commitsTab.classList.remove('active');
} else if (action === 'pipelines') {
this.resetViewContainer();
this.mountPipelinesView();
@ -172,6 +179,8 @@ export default class MergeRequestTabs {
if (this.setUrl) {
this.setCurrentAction(action);
}
this.eventHub.$emit('MergeRequestTabChange', this.getCurrentAction());
}
scrollToElement(container) {

View File

@ -1,4 +1,4 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
/* eslint-disable max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
/* global Issuable */
/* global ListMilestone */
@ -16,10 +16,10 @@ export default class MilestoneSelect {
typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
}
this.init(els, options);
MilestoneSelect.init(els, options);
}
init(els, options) {
static init(els, options) {
let $els = $(els);
if (!els) {
@ -224,7 +224,6 @@ export default class MilestoneSelect {
$selectBox.hide();
$value.css('display', '');
if (data.milestone != null) {
data.milestone.full_path = this.currentProject.full_path;
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));

View File

@ -1,20 +1,32 @@
import $ from 'jquery';
import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import initDiffsApp from '../diffs';
import notesApp from '../notes/components/notes_app.vue';
import discussionCounter from '../notes/components/discussion_counter.vue';
import store from '../notes/stores';
import store from './stores';
import MergeRequest from '../merge_request';
export default function initMrNotes() {
const mrShowNode = document.querySelector('.merge-request');
// eslint-disable-next-line no-new
new MergeRequest({
action: mrShowNode.dataset.mrAction,
});
// eslint-disable-next-line no-new
new Vue({
el: '#js-vue-mr-discussions',
name: 'MergeRequestDiscussions',
components: {
notesApp,
},
store,
data() {
const notesDataset = document.getElementById('js-vue-mr-discussions')
.dataset;
const notesDataset = document.getElementById('js-vue-mr-discussions').dataset;
const noteableData = JSON.parse(notesDataset.noteableData);
noteableData.noteableType = notesDataset.noteableType;
noteableData.targetType = notesDataset.targetType;
return {
noteableData,
@ -22,12 +34,42 @@ export default function initMrNotes() {
notesData: JSON.parse(notesDataset.notesData),
};
},
computed: {
...mapGetters(['discussionTabCounter']),
...mapState({
activeTab: state => state.page.activeTab,
}),
},
watch: {
discussionTabCounter() {
this.updateDiscussionTabCounter();
},
},
mounted() {
this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge');
this.setActiveTab(window.mrTabs.getCurrentAction());
window.mrTabs.eventHub.$on('MergeRequestTabChange', tab => {
this.setActiveTab(tab);
});
$(document).on('visibilitychange', this.updateDiscussionTabCounter);
},
beforeDestroy() {
$(document).off('visibilitychange', this.updateDiscussionTabCounter);
},
methods: {
...mapActions(['setActiveTab']),
updateDiscussionTabCounter() {
this.notesCountBadge.text(this.discussionTabCounter);
},
},
render(createElement) {
return createElement('notes-app', {
props: {
noteableData: this.noteableData,
notesData: this.notesData,
userData: this.currentUserData,
shouldShow: this.activeTab === 'show',
},
});
},
@ -36,6 +78,7 @@ export default function initMrNotes() {
// eslint-disable-next-line no-new
new Vue({
el: '#js-vue-discussion-counter',
name: 'DiscussionCounter',
components: {
discussionCounter,
},
@ -44,4 +87,6 @@ export default function initMrNotes() {
return createElement('discussion-counter');
},
});
initDiffsApp(store);
}

View File

@ -0,0 +1,7 @@
import types from './mutation_types';
export default {
setActiveTab({ commit }, tab) {
commit(types.SET_ACTIVE_TAB, tab);
},
};

View File

@ -0,0 +1,5 @@
export default {
isLoggedIn(state, getters) {
return !!getters.getUserData.id;
},
};

View File

@ -0,0 +1,15 @@
import Vue from 'vue';
import Vuex from 'vuex';
import notesModule from '~/notes/stores/modules';
import diffsModule from '~/diffs/store/modules';
import mrPageModule from './modules';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
page: mrPageModule,
notes: notesModule,
diffs: diffsModule,
},
});

View File

@ -0,0 +1,12 @@
import actions from '../actions';
import getters from '../getters';
import mutations from '../mutations';
export default {
state: {
activeTab: null,
},
actions,
getters,
mutations,
};

View File

@ -0,0 +1,3 @@
export default {
SET_ACTIVE_TAB: 'SET_ACTIVE_TAB',
};

View File

@ -0,0 +1,7 @@
import types from './mutation_types';
export default {
[types.SET_ACTIVE_TAB](state, tab) {
Object.assign(state, { activeTab: tab });
},
};

View File

@ -1,4 +1,4 @@
/* eslint-disable func-names, space-before-function-paren, no-var, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */
/* eslint-disable func-names, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */
import $ from 'jquery';
import Api from './api';

View File

@ -1,4 +1,4 @@
/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */
/* eslint-disable func-names, no-var, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */
import $ from 'jquery';
import { __ } from '../locale';
@ -113,8 +113,7 @@ export default (function() {
});
ref = this.days;
// eslint-disable-next-line no-multi-assign
for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) {
for (mm = 0, len = ref.length; mm < len; mm += 1) {
day = ref[mm];
if (cuday !== day[0] || cumonth !== day[1]) {
// Dates
@ -288,8 +287,7 @@ export default (function() {
ref = commit.parents;
results = [];
// eslint-disable-next-line no-multi-assign
for (i = j = 0, len = ref.length; j < len; i = (j += 1)) {
for (i = 0, len = ref.length; i < len; i += 1) {
parent = ref[i];
parentCommit = this.preparedCommits[parent[0]];
parentY = this.offsetY + this.unitTime * parentCommit.time;

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