Merge branch 'master' into build-chunks-on-object-storage
This commit is contained in:
commit
6c3eea0db0
|
@ -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:
|
||||
|
|
240
CHANGELOG.md
240
CHANGELOG.md
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.105.1
|
||||
0.107.0
|
||||
|
|
3
Gemfile
3
Gemfile
|
@ -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
|
||||
|
|
10
Gemfile.lock
10
Gemfile.lock
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable no-new */
|
||||
import Vue from 'vue';
|
||||
import pdfLab from '../../pdf/index.vue';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
|
@ -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: {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
import FilteredSearchContainer from '../filtered_search/container';
|
||||
import FilteredSearchManager from '../filtered_search/filtered_search_manager';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */
|
||||
/* global DocumentTouch */
|
||||
|
||||
import $ from 'jquery';
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
|
||||
class ListMilestone {
|
||||
constructor(obj) {
|
||||
this.id = obj.id;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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.'),
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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');
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/* eslint-disable camelcase, no-unused-vars */
|
||||
|
||||
class NoteModel {
|
||||
constructor(discussionId, noteObj) {
|
||||
this.discussionId = discussionId;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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;
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
});
|
|
@ -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,
|
||||
};
|
|
@ -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';
|
|
@ -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 });
|
||||
},
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -117,7 +117,7 @@ export default {
|
|||
class="btn btn-primary btn-sm btn-block"
|
||||
@click="toggleIsSmall"
|
||||
>
|
||||
{{ __('Commit') }}
|
||||
{{ __('Commit…') }}
|
||||
</button>
|
||||
<p
|
||||
class="text-center"
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -91,7 +91,6 @@ export default class IntegrationSettingsForm {
|
|||
}
|
||||
}
|
||||
|
||||
/* eslint-disable promise/catch-or-return, no-new */
|
||||
/**
|
||||
* Test Integration config
|
||||
*/
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import types from './mutation_types';
|
||||
|
||||
export default {
|
||||
setActiveTab({ commit }, tab) {
|
||||
commit(types.SET_ACTIVE_TAB, tab);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
isLoggedIn(state, getters) {
|
||||
return !!getters.getUserData.id;
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
import actions from '../actions';
|
||||
import getters from '../getters';
|
||||
import mutations from '../mutations';
|
||||
|
||||
export default {
|
||||
state: {
|
||||
activeTab: null,
|
||||
},
|
||||
actions,
|
||||
getters,
|
||||
mutations,
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
export default {
|
||||
SET_ACTIVE_TAB: 'SET_ACTIVE_TAB',
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import types from './mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_ACTIVE_TAB](state, tab) {
|
||||
Object.assign(state, { activeTab: tab });
|
||||
},
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue