Update upstream source from tag 'upstream/11.2.8+dfsg'

Update to upstream version '11.2.8+dfsg'
with Debian dir 11d3012532
This commit is contained in:
Pirate Praveen 2018-11-18 11:01:12 +05:30
commit 1fb51b2ec9
3064 changed files with 103682 additions and 25503 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
Dangerfile gitlab-language=ruby

View file

@ -1,4 +1,4 @@
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.17-chrome-67.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29"
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.18-chrome-67.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29"
.dedicated-runner: &dedicated-runner
retry: 1
@ -86,7 +86,9 @@ stages:
.rails5: &rails5
allow_failure: true
only:
- /rails5/
variables:
- $CI_COMMIT_REF_NAME =~ /rails5/
- $RAILS5_ENABLED
variables:
BUNDLE_GEMFILE: "Gemfile.rails5"
RAILS5: "true"
@ -436,6 +438,26 @@ setup-test-env:
- config/secrets.yml
- vendor/gitaly-ruby
danger-review:
image: registry.gitlab.com/gitlab-org/gitlab-build-images:danger
stage: test
allow_failure: true
cache: {}
dependencies: []
before_script: []
only:
variables:
- $DANGER_GITLAB_API_TOKEN
except:
refs:
- master
variables:
- $CI_COMMIT_REF_NAME =~ /^ce-to-ee-.*/
- $CI_COMMIT_REF_NAME =~ /.*-stable(-ee)?-prepare-.*/
script:
- git version
- danger --fail-on-errors=true
rspec-pg 0 30: *rspec-metadata-pg
rspec-pg 1 30: *rspec-metadata-pg
rspec-pg 2 30: *rspec-metadata-pg

View file

@ -34,17 +34,17 @@ When removing columns, tables, indexes or other structures:
## General checklist
- [ ] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) added, if necessary
- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/doc_styleguide.html)
- [ ] API support added
- [ ] Tests added for this feature/bug
- Conform by the [code review guidelines](https://docs.gitlab.com/ee/development/code_review.html)
- [ ] Has been reviewed by a Backend maintainer
- [ ] Has been reviewed by a Database specialist
- [ ] Conform by the [merge request performance guides](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html)
- [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides)
- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/documentation/index.html#contributing-to-docs)
- [ ] [API support added](https://docs.gitlab.com/ee/development/api_styleguide.html)
- [ ] [Tests added for this feature/bug](https://docs.gitlab.com/ee/development/testing_guide/index.html)
- Conforms to the [code review guidelines](https://docs.gitlab.com/ee/development/code_review.html)
- [ ] Has been reviewed by a Backend [maintainer](https://about.gitlab.com/handbook/engineering/#maintainer)
- [ ] Has been reviewed by a Database [specialist](https://about.gitlab.com/team/structure/#specialist)
- [ ] Conforms to the [merge request performance guidelines](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html)
- [ ] Conforms to the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides)
- [ ] If you have multiple commits, please combine them into a few logically organized commits by [squashing them](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
- [ ] Internationalization required/considered
- [ ] If paid feature, have we considered GitLab.com plan and how it works for groups and is there a design for promoting it to users who aren't on the correct plan
- [ ] End-to-end tests pass (`package-and-qa` manual pipeline job)
- [ ] [Internationalization required/considered](https://docs.gitlab.com/ee/development/i18n/index.html)
- [ ] For a paid feature, have we considered GitLab.com plans, how it works for groups, and is there a design for promoting it to users who aren't on the correct plan?
- [ ] [End-to-end tests](https://docs.gitlab.com/ee/development/testing_guide/end_to_end_tests.html#testing-code-in-merge-requests) pass (`package-and-qa` manual pipeline job)
/label ~database

View file

@ -1,4 +1,4 @@
<!--See the general Documentation guidelines https://docs.gitlab.com/ce/development/writing_documentation.html -->
<!--See the general Documentation guidelines https://docs.gitlab.com/ee/development/documentation/index.html -->
## What does this MR do?
@ -13,17 +13,17 @@ Closes
## Moving docs to a new location?
Read the guidelines:
https://docs.gitlab.com/ce/development/writing_documentation.html#changing-document-location
https://docs.gitlab.com/ee/development/documentation/#changing-document-location
- [ ] Make sure the old link is not removed and has its contents replaced with
a link to the new location.
- [ ] Make sure internal links pointing to the document in question are not broken.
- [ ] Search and replace any links referring to old docs in GitLab Rails app,
specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories.
- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ce/development/writing_documentation.html#redirections-for-pages-with-disqus-comments)
- [ ] Search and replace any links referring to the old docs in the GitLab Rails app,
specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories.
- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ee/development/documentation/index.html#redirections-for-pages-with-disqus-comments)
to the new document if there are any Disqus comments on the old document thread.
- [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE
with the changes as well (https://docs.gitlab.com/ce/development/writing_documentation.html#cherry-picking-from-ce-to-ee).
- [ ] If working on CE and the `ee-compat-check` jobs fails, [submit an MR to EE
with the changes](https://docs.gitlab.com/ee/development/documentation/index.html#cherry-picking-from-ce-to-ee) as well.
- [ ] Ping one of the technical writers for review.
/label ~Documentation

View file

@ -10,9 +10,9 @@ AllCops:
Exclude:
- 'vendor/**/*'
- 'node_modules/**/*'
- 'db/**/*'
- 'db/fixtures/**/*'
- 'ee/db/**/*'
- 'db/schema.rb'
- 'ee/db/geo/schema.rb'
- 'tmp/**/*'
- 'bin/**/*'
- 'generator_templates/**/*'
@ -34,6 +34,8 @@ Style/MutableConstant:
Naming/FileName:
ExpectMatchingDefinition: true
Exclude:
- 'db/**/*'
- 'ee/db/**/*'
- 'spec/**/*'
- 'features/**/*'
- 'ee/spec/**/*'

View file

@ -199,12 +199,6 @@ Naming/HeredocDelimiterCase:
Naming/HeredocDelimiterNaming:
Enabled: false
# Offense count: 27
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect.
Performance/HashEachMethods:
Enabled: false
# Offense count: 1
Performance/UnfreezeString:
Exclude:

View file

@ -2,16 +2,38 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 11.1.8 (2018-10-05)
## 11.2.8 (2018-10-31)
### Security (1 change)
- Monkey kubeclient to not follow any redirects.
## 11.2.7 (2018-10-27)
- No changes.
## 11.2.6 (2018-10-26)
### Security (5 changes)
- Escape entity title while autocomplete template rendering to prevent XSS. !2558
- Fix XSS in merge request source branch name.
- Redact personal tokens in unsubscribe links.
- Persist only SHA digest of PersonalAccessToken#token.
- Prevent SSRF attacks in HipChat integration.
## 11.2.5 (2018-10-05)
### Security (3 changes)
- Filter user sensitive data from discussions JSON. !2539
- Filter user sensitive data from discussions JSON. !2538
- Properly filter private references from system notes.
- Markdown API no longer displays confidential title references unless authorized.
## 11.1.7 (2018-09-26)
## 11.2.4 (2018-09-26)
### Security (6 changes)
@ -23,11 +45,11 @@ entry.
- Block loopback addresses in UrlBlocker.
## 11.1.6 (2018-08-28)
## 11.2.3 (2018-08-28)
- No changes.
## 11.1.5 (2018-08-27)
## 11.2.2 (2018-08-27)
### Security (3 changes)
@ -35,32 +57,271 @@ entry.
- Adding CSRF protection to Hooks resend action.
- Block link-local addresses in URLBlocker.
### Fixed (1 change, 1 of them is from the community)
## 11.2.1 (2018-08-22)
### Fixed (2 changes)
- Fix wrong commit count in push event payload. !21338
- Fix broken Git over HTTP clones with LDAP users. !21352
### Performance (1 change)
- Eliminate unnecessary and duplicate system hook fires. !21337
## 11.2.0 (2018-08-22)
### Security (5 changes)
- Bump Gitaly to 0.117.1 for Rouge update. !21277
- Fix symlink vulnerability in project import.
- Bump rugged to 0.27.4 for security fixes.
- Fixed XSS in branch name in Web IDE.
- Adding CSRF protection to Hooks test action.
### Removed (1 change)
- Remove gitlab:user:check_repos, gitlab:check_repo, gitlab:git:prune, gitlab:git:gc, and gitlab:git:repack. !20806
### Fixed (81 changes, 26 of them are from the community)
- Fix namespace move callback behavior, especially to fix Geo replication of namespace moves during certain exceptions. !19297
- Fix breadcrumbs in Admin/User interface. !19608 (Robin Naundorf)
- Remove changes_count from MR API documentation where necessary. !19745 (Jan Beckmann)
- Fix email confirmation bug when user adds additional email to account. !20084 (muhammadn)
- Add support for daylight savings time to pipleline schedules. !20145
- Fixing milestone date change when editing. !20279 (Orlando Del Aguila)
- Add missing maximum_timeout parameter. !20355 (gfyoung)
- [Rails5] Fix 'Invalid single-table inheritance type: Group is not a subclass of Gitlab::BackgroundMigration::FixCrossProjectLabelLinks::Namespace'. !20462 (@blackst0ne)
- Rails5 fix mysql milliseconds problem in specs. !20464 (Jasper Maes)
- Update Gemfile.rails5.lock with latest Gemfile.lock changes. !20466 (Jasper Maes)
- Rails5 mysql fix milliseconds problem in pull request importer spec. !20475 (Jasper Maes)
- Rails5 MySQL fix rename_column as part of cleanup_concurrent_column_type_change. !20514 (Jasper Maes)
- Process commits as normal in forks when the upstream project is deleted. !20534
- Fix project visibility tooltip. !20535 (Jamie Schembri)
- Fix archived parameter for projects API. !20566 (Peter Marko)
- Limit maximum project build timeout setting to 1 month. !20591
- Fix GitLab project imports not loading due to API timeouts. !20599
- Avoid process deadlock in popen by consuming input pipes. !20600
- Disable SAML and Bitbucket if OmniAuth is disabled. !20608
- Support multiple scopes when authing container registry scopes. !20617
- Adds the ability to view group milestones on the dashboard milestone page. !20618
- Allow issues API to receive an internal ID (iid) on create. !20626 (Jamie Schembri)
- Fix typo in CSS transform property for Memory Graph component. !20650
- Update design for system metrics popovers. !20655
- Toggle Show / Hide Button for Kubernetes Password. !20659 (gfyoung)
- Board label edit dropdown shows incorrect selected labels summary. !20673
- Resolve "Unable to save user profile update with Safari". !20676
- Escape username and password in UrlSanitizer#full_url. !20684
- Remove background color from card-body style. !20689 (George Tsiolis)
- Update total storage size when changing size of artifacts. !20697 (Peter Marko)
- Rails5 fix user sees revert modal spec. !20706 (Jasper Maes)
- Fix Web IDE crashing on directories named 'blob'. !20712
- Fix accessing imported pipeline builds. !20713
- Fixed bug with invalid repository reference using the wiki search. !20722
- Resolve Copy diff file path as GFM is broken. !20725
- Chart versions for applications installed by one click install buttons should be version locked. !20765
- Fix misalignment of broadcast message on login page. !20794 (Robin Naundorf)
- Fix Vue datatype errors for markdownVersion parsing. !20800
- Fix authorization for interactive web terminals. !20811
- Increase width of Web IDE sidebar resize handles. !20818
- Fix new MR card styles. !20822
- Fix link color in markdown code brackets. !20841
- Rails5 update Gemfile.rails5.lock. !20858 (Jasper Maes)
- fix height of full-width Metrics charts on large screens. !20866
- Fix sorting by name on milestones page. !20881
- Permit concurrent loads in gpg keychain mutex. !20894 (Jasper Maes)
- Prevent editing and updating wiki pages with non UTF-8 encoding via web interface. !20906
- Retrieve merge request closing issues from database cache. !20911
- Fix LFS uploads not working with git-lfs 2.5.0. !20923
- Fix bug setting http headers in Files API. !20938
- Rails5: fix flaky spec. !20953 (Jasper Maes)
- Fixed list of projects not loading in group boards. !20955
- Fix autosave and ESC confirmation issues for MR discussions. !20968
- Fix navigation to First and Next discussion on MR Changes tab. !20968
- Fix rendering of the context lines in MR diffs page. !20968
- fix error caused when using the search bar while unauthenticated. !20970
- Fix GPG status badge loading regressions. !20987
- Ensure links in notifications footer are not escaped. !21000
- Rails5: update Rails5 lock for forgotten gem rouge. !21010 (Jasper Maes)
- Fix UI error whereby prometheus application status is updated. !21029
- Solves group dashboard line height is too tall for group names. !21033
- Fix rendering of pipeline failure view when directly navigationg to it. !21043
- Fix missing and duplicates on project milestone listing page. !21058
- Fix merge requests not showing any diff files for big patches. !21125
- Auto-DevOps.gitlab-ci.yml: Update glibc package signing key URL. !21182 (sgerrand)
- Fix issue stopping Instance Statistics javascript to be executed. !21211
- Fix broken JavaScript in IE11. !21214
- Improve JUnit test reports in merge request widgets. !49966
- Properly handle colons in URL passwords.
- Renders test reports for resolved failures and resets error state.
- Fix handling of annotated tags when Gitaly is not in use.
- Fix serialization of LegacyDiffNote.
- Escapes milestone and label's names on flash notice when promoting them.
- Allow to toggle notifications for issues due soon.
- Sanitize git URL in import errors. (Jamie Schembri)
- Add missing predefined variable and fix docs.
- Allow updating a project's avatar without other params. (Jamie Schembri)
- Fix the UI for listing system-level labels.
- Update hamlit to fix ruby 2.5 incompatibilities, fixes #42045. (Matthew Dawson)
- Fix updated_at if created_at is set for Note API.
- Fix search bar text input alignment.
### Changed (32 changes, 7 of them are from the community)
- Rack attack is now disabled by default. !16669
- Include full image URL in webhooks for uploaded images. !18109 (Satish Perala)
- Enable hashed storage for all newly created or renamed projects. !19747
- Support manually stopping any environment from the UI. !20077
- Close revert and cherry pick modal on escape keypress. !20341 (George Tsiolis)
- Adds with_projects optional parameter to GET /groups/:id API endpoint. !20494
- Improve feedback when a developer is unable to push to an empty repository. !20519
- Display GPG status on repository and blob pages. !20524
- Updated design of new entry dropdown in Web IDE. !20526
- UX improvements to top nav search bar. !20537
- Update issue closing pattern. !20554 (George Tsiolis)
- Add merge request header branch actions left margin. !20643 (George Tsiolis)
- Rubix, scikit-learn, tensorflow & other useful libraries pre-installed with JupyterHub. !20714 (Amit Rathi)
- Show decimal place up to single digit in Stacked Progress Bar. !20776
- Wrap job name on pipeline job sidebar. !20804 (George Tsiolis)
- Redesign Web IDE back button and context header. !20850
- Removes "show all" on reports and adds an actionButtons slot. !20855
- Put fallback reply-key address first in the References header. !20871
- Allow non-admins to view instance statistics (if permitted by the instance admins). !20874
- Adds the project and group name to the return type for project and group milestones. !20890
- Restyle status message input on profile settings. !20903
- Ensure installed Helm Tiller For GitLab Managed Apps Is protected by mutual auth. !20928
- Allow multiple JIRA transition ids. !20939
- Use Helm 2.7.2 for GitLab Managed Apps. !20956
- Create branch and MR picker for Web IDE. !20978
- Update commit message styles with monospace font and overflow-x. !20988
- Update to Rouge 3.2.0, including Terraform and Crystal lexer and bug fixes. !20991
- Update design of project templates. !21012
- Update to Rouge 3.2.1, which includes a critical fix to the Perl Lexer. !21263
- Add a 10 ms bucket for SQL timings.
- Show one digit after dot in commit_per_day value in charts page. (msdundar)
- Redesign GCP offer banner.
### Performance (30 changes, 10 of them are from the community)
- Stop dynamically creating project and namespace routes. !20313
- Tracking the number of repositories and wikis with a cached counter for site-wide statistics. !20413
- Optimize ProjectWiki#empty? check. !20573
- Delete UserActivities and related workers. !20597
- Enable frozen string in app/services/**/*.rb. !20656 (gfyoung)
- Enable more frozen string in app/services/**/*.rb. !20677 (gfyoung)
- Limit the TTL for anonymous sessions to 1 hour. !20700
- Enable even more frozen string in app/services/**/*.rb. !20702 (gfyoung)
- Enable frozen string in app/serializers/**/*.rb. !20726 (gfyoung)
- Enable frozen string in newly added files to previously processed directories. !20763 (gfyoung)
- Use limit parameter to retrieve Wikis from Gitaly. !20764
- Add Dangerfile for frozen_string_literal. !20767 (gfyoung)
- Remove method instrumentation for Banzai filters and reference parsers. !20770
- Enable frozen strings in lib/banzai/filter/*.rb. !20775
- Enable frozen strings in remaining lib/banzai/filter/*.rb files. !20777
- DNS prefetching if asset_host for CDN hosting is set. !20781
- Bump nokogiri to 1.8.4 and sanitize to 4.6.6 for performance. !20795
- Enable frozen string in app/presenters and app/policies. !20819 (gfyoung)
- Bump haml gem to 5.0.4. !20847
- Enable frozen string in app/models/*.rb. !20851 (gfyoung)
- Performing Commit GPG signature calculation in bulk. !20870
- Fix /admin/jobs failing to load due to statement timeout. !20909
- refactor pipeline job log animation to reduce CPU usage. !20915
- Improve performance when fetching collapsed diffs and commenting in merge requests. !20940
- Enable frozen string for app/models/**/*.rb. !21001 (gfyoung)
- Don't set gon variables in JSON requests. !21016 (Peter Leitzen)
- Improve performance and memory footprint of Changes tab of Merge Requests. !21028
- Avoid N+1 on MRs page when metrics merging date cannot be found. !21053
- Bump Gitaly to 0.117.0. !21055
- Access metadata directly from Object Storage.
### Added (41 changes, 18 of them are from the community)
- Show repository languages for projects. !19480
- Adds API endpoint /api/v4/(project/group)/:id/members/all to list also inherited members. !19748 (Jacopo Beschi @jacopo-beschi)
- Added live preview for JavaScript projects in the Web IDE. !19764
- Add support for SSH certificate authentication. !19911 (Ævar Arnfjörð Bjarmason)
- Add Hangouts Chat integration. !20290 (Kukovskii Vladimir)
- Add ability to import multiple repositories by uploading a manifest file. !20304
- Show Project ID on project home panel. !20305 (Tuğçe Nur Taş)
- Add an option to have a private profile on GitLab. !20387 (jxterry)
- Extend gitlab-ci.yml to request junit.xml test reports. !20390
- Add the first mutations for merge requests to GraphQL. !20443
- Add /-/health basic health check endpoint. !20456
- Add filter for minimal access level in groups and projects API. !20478 (Marko, Peter)
- Add download button for single file (including raw files) in repository. !20480 (Kia Mei Somabes)
- Gitaly Servers link into Admin > Overview navigation menu. !20550
- Adds foreign key to notification_settings.user_id. !20567 (Jacopo Beschi @jacopo-beschi)
- JUnit XML Test Summary In MR widget. !20576
- Cleans up display of Deploy Tokens to match Personal Access Tokens. !20578 (Marcel Amirault)
- Users can set a status message and emoji. !20614 (niedermyer & davamr)
- Add emails delivery Prometheus metrics. !20638
- Verify runner feature set. !20664
- Add more comprehensive metrics tracking authentication activity. !20668
- Add support for tar.gz AUTO_DEVOPS_CHART charts (#49324). !20691 (@kondi1)
- Adds Vuex store for reports section in MR widget. !20709
- Redirect commits to root if no ref is provided (31576). !20738 (Kia Mei Somabes)
- Search for labels by title or description on project labels page. !20749
- Add object storage logic to project import. !20773
- Enable renaming files and folders in Web IDE. !20835
- Warn user when reload IDE with staged changes. !20857
- Add local project uploads cleanup task. !20863
- Improve error message when adding invalid user to a project. !20885 (Jacopo Beschi @jacopo-beschi)
- Add link to homepage on static http status pages (404, 500, etc). !20898 (Jason Funk)
- Clean orphaned files in object storage. !20918
- Adds frontend support to render test reports on the MR widget. !20936
- Trigger system hooks when project is archived/unarchived. !20995
- Custom Wiki Sidebar Support Issue 14995. (Josh Sooter)
- Emails on push recipients now accepts formats like John Doe <johndoe@example.com>. (George Thomas)
- Add new model for tracking label events.
- Improve danger confirmation modals by focusing input field. (Jamie Schembri)
- Clicking CI icon in Web IDE now opens up pipelines panel.
- Enabled deletion of files in the Web IDE.
- Added button to regenerate 2FA codes. (Luke Picciau)
### Other (26 changes, 7 of them are from the community)
- Update specific runners help URL. !20213 (George Tsiolis)
- Enable frozen string in apps/uploaders/*.rb. !20401 (gfyoung)
- Update docs of Helm Tiller. !20515 (Takuya Noguchi)
- Persist 'Auto DevOps' banner dismissal globally. !20540
- Move xterm to a node dependency and remove it from vendor's folder. !20588
- Upgrade grape-path-helpers to 1.0.6. !20601
- Delete todos when user loses access to read the target. !20665
- Remove tooltips from commit author avatar and name in commit lists. !20674
- Allow cloning LFS repositories through DeployTokens. !20729
- Replace 'Sidekiq::Testing.inline!' with 'perform_enqueued_jobs'. !20768 (@blackst0ne)
- Replace author_link snake case in stylesheets, specs, and helpers. !20797 (George Tsiolis)
- Replace snake case in SCSS variables. !20799 (George Tsiolis)
- Add rbtrace to Gemfile. !20831
- Add support for searching users by confirmed e-mails. !20893
- Changes poll.js to keep polling on any 2xx http status code. !20904
- Remove todos of users without access to targets migration. !20927
- Improve and simplify Auto DevOps settings flow. !20946
- Keep admin settings sections open after submitting forms. !21040
- CE port of "List groups with developer maintainer access on project creation". !21051
- Update git rerere link in docs. !21060 (gfyoung)
- Add 'tabindex' attribute support on Icon component to show BS4 popover on trigger type 'focus'. !21066
- Add a Gitlab::Profiler.print_by_total_time convenience method for profiling from a Rails console.
- Automatically expand runner's settings block when linking to the runner's settings page.
- Increases title column on modal for reports.
- Disables toggle comments button if diff has no discussions.
- Moves help_popover component to a common location.
## 11.1.4 (2018-07-30)
- No changes.
## 11.1.3 (2018-07-27)
### Fixed (8 changes, 1 of them is from the community)
### Fixed (4 changes, 1 of them is from the community)
- Rework some projects table indexes around repository_storage field. !20377
- Fix navigation to First and Next discussion on MR Changes tab. !20434
- Fix showing outdated discussions on Changes tab. !20445
- Fix autosave and ESC confirmation issues for MR discussions. !20569
- Fix rendering of the context lines in MR diffs page. !20642
- Don't overflow project/group dropdown results. !20704 (gfyoung)
- Fixed IDE not opening JSON files. !20798
- Disable Gitaly timeouts when creating or restoring backups. !20810
### Performance (1 change)
- Reduces the client side memory footprint on merge requests. !20744
## 11.1.3 (2018-07-27)
- Not released.
## 11.1.2 (2018-07-26)
@ -331,6 +592,27 @@ entry.
- Use monospaced font for MR diff commit link ref on GFM.
## 11.0.5 (2018-07-26)
### Security (4 changes)
- Don't expose project names in various counters.
- Don't expose project names in GitHub counters.
- Adding CSRF protection to Hooks test action.
- Fixed XSS in branch name in Web IDE.
### Fixed (1 change)
- Escapes milestone and label's names on flash notice when promoting them.
## 11.0.4 (2018-07-17)
### Security (1 change)
- Fix symlink vulnerability in project import.
## 11.0.3 (2018-07-05)
### Fixed (14 changes, 1 of them is from the community)
@ -624,6 +906,14 @@ entry.
- Workhorse to send raw diff and patch for commits.
## 10.8.6 (2018-07-17)
### Security (2 changes)
- Fix symlink vulnerability in project import.
- Merge branch 'fix-mr-widget-border' into 'master'.
## 10.8.5 (2018-06-21)
### Security (5 changes)
@ -853,6 +1143,13 @@ entry.
- Gitaly handles repository forks by default.
## 10.7.7 (2018-07-17)
### Security (1 change)
- Fix symlink vulnerability in project import.
## 10.7.6 (2018-06-21)
### Security (6 changes)

View file

@ -50,6 +50,7 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Definition of done](#definition-of-done)
- [Style guides](#style-guides)
- [Code of conduct](#code-of-conduct)
- [Contribution Flow](#contribution-flow)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
@ -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.
- Team: ~"CI/CD", ~Plan, ~Manage, ~Quality, etc.
- Release Scoping: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Priority: ~P1, ~P2, ~P3, ~P4
- Severity: ~S1, ~S2, ~S3, ~S4
@ -186,16 +187,17 @@ The current team labels are:
- ~Configuration
- ~"CI/CD"
- ~Discussion
- ~Create
- ~Distribution
- ~Documentation
- ~Geo
- ~Gitaly
- ~Manage
- ~Monitoring
- ~Platform
- ~Plan
- ~Quality
- ~Release
- ~"Security Products"
- ~Secure
- ~UX
The descriptions on the [labels page][labels-page] explain what falls under the
@ -225,24 +227,24 @@ 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
### 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.
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.
| Label | Meaning | Estimate time to fix | Guidance |
|-------|-----------------|------------------------------------------------------------------|----------|
| ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com | |
| ~P2 | High Priority | The next release | |
| ~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 |
| Label | Meaning | Estimate time to fix |
|-------|-----------------|------------------------------------------------------------------|
| ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com |
| ~P2 | High Priority | The next release |
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) |
| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) |
### Bug Severity labels
### Severity labels
Severity labels help us clearly communicate the impact of a ~bug on users.
| Label | Meaning | Impact of the defect | Example |
| Label | Meaning | Impact on Functionality | Example |
|-------|-------------------|-------------------------------------------------------|---------|
| ~S1 | Blocker | Outage, broken feature with no workaround | Unable to create an issue. Data corruption/loss. Security breach. |
| ~S2 | Critical Severity | Broken Feature, workaround too complex & unacceptable | Can push commits, but only via the command line. |
@ -251,12 +253,14 @@ Severity labels help us clearly communicate the impact of a ~bug on users.
#### Severity impact guidance
| 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 |
| ~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 |
Severity levels can be applied further depending on the facet of the impact; e.g. Affected customers, GitLab.com availability, performance and etc. The below is a guideline.
| Severity | Affected Customers/Users | GitLab.com Availability | Performance Degradation |
|----------|---------------------------------------------------------------------|----------------------------------------------------|------------------------------|
| ~S1 | >50% users affected (possible company extinction level event) | Significant impact on all of GitLab.com | |
| ~S2 | Many users or multiple paid customers affected (but not apocalyptic)| Significant impact on large portions of GitLab.com | Degradation is guaranteed to occur in the near future |
| ~S3 | A few users or a single paid customer affected | Limited impact on important portions of GitLab.com | Degradation is likely to occur in the near future |
| ~S4 | No paid users/customer affected, or expected to in the near future | Minor impact on on GitLab.com | Degradation _may_ occur but it's not likely |
### Label for community contributors
@ -372,8 +376,15 @@ on those issues. Please select someone with relevant experience from the
[GitLab team][team]. If there is nobody mentioned with that expertise look in
the commit history for the affected files to find someone.
We also use [GitLab Triage] to automate some triaging policies. This is
currently setup as a [scheduled pipeline] running on [quality/triage-ops]
project.
[described in our handbook]: https://about.gitlab.com/handbook/engineering/issue-triage/
[issue bash events]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17815
[GitLab Triage]: https://gitlab.com/gitlab-org/gitlab-triage
[scheduled pipeline]: https://gitlab.com/gitlab-org/quality/triage-ops/pipeline_schedules/10512/edit
[quality/triage-ops]: https://gitlab.com/gitlab-org/quality/triage-ops
### Feature proposals
@ -729,6 +740,24 @@ reported by emailing `contact@gitlab.com`.
This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant], version 1.1.0,
available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/).
## Contribution Flow
When contributing to GitLab, your merge request is subject to review by merge request maintainers of a particular specialty.
When you submit code to GitLab, we really want it to get merged, but there will be times when it will not be merged.
When maintainers are reading through a merge request they may request guidance from other maintainers. If merge request maintainers conclude that the code should not be merged, our reasons will be fully disclosed. If it has been decided that the code quality is not up to GitLabs standards, the merge request maintainer will refer the author to our docs and code style guides, and provide some guidance.
Sometimes style guides will be followed but the code will lack structural integrity, or the maintainer will have reservations about the codes overall quality. When there is a reservation the maintainer will inform the author and provide some guidance. The author may then choose to update the merge request. Once the merge request has been updated and reassigned to the maintainer, they will review the code again. Once the code has been resubmitted any number of times, the maintainer may choose to close the merge request with a summary of why it will not be merged, as well as some guidance. If the merge request is closed the maintainer will be open to discussion as to how to improve the code so it can be approved in the future.
GitLab will do its best to review community contributions as quickly as possible. Specially appointed developers review community contributions daily. You may take a look at the [team page](https://about.gitlab.com/team/) for the merge request coach who specializes in the type of code you have written and mention them in the merge request. For example, if you have written some JavaScript in your code then you should mention the frontend merge request coach. If your code has multiple disciplines you may mention multiple merge request coaches.
GitLab receives a lot of community contributions, so if your code has not been reviewed within 4 days of its initial submission feel free to re-mention the appropriate merge request coach.
When submitting code to GitLab, you may feel that your contribution requires the aid of an external library. If your code includes an external library please provide a link to the library, as well as reasons for including it.
When your code contains more than 500 changes, any major breaking changes, or an external library, `@mention` a maintainer in the merge request. If you are not sure who to mention, the reviewer will add one early in the merge request process.
[core team]: https://about.gitlab.com/core-team/
[team]: https://about.gitlab.com/team/
[getting-help]: https://about.gitlab.com/getting-help/

7
Dangerfile Normal file
View file

@ -0,0 +1,7 @@
danger.import_dangerfile(path: 'danger/metadata')
danger.import_dangerfile(path: 'danger/changes_size')
danger.import_dangerfile(path: 'danger/changelog')
danger.import_dangerfile(path: 'danger/specs')
danger.import_dangerfile(path: 'danger/gemfile')
danger.import_dangerfile(path: 'danger/database')
danger.import_dangerfile(path: 'danger/frozen_string')

View file

@ -1 +1 @@
0.111.4
0.117.3

View file

@ -1 +1 @@
7.1.4
8.1.1

View file

@ -1 +1 @@
5.0.0
5.1.0

17
Gemfile
View file

@ -104,7 +104,7 @@ gem 'hashie-forbidden_attributes'
gem 'kaminari', '~> 1.0'
# HAML
gem 'hamlit', '~> 2.6.1'
gem 'hamlit', '~> 2.8.8'
# Files attachments
gem 'carrierwave', '~> 1.2'
@ -220,6 +220,9 @@ gem 'gemnasium-gitlab-service', '~> 0.2'
# Slack integration
gem 'slack-notifier', '~> 1.5.1'
# Hangouts Chat integration
gem 'hangouts-chat', '~> 0.0.5'
# Asana integration
gem 'asana', '~> 0.6.0'
@ -230,7 +233,7 @@ gem 'ruby-fogbugz', '~> 0.2.1'
gem 'kubeclient', '~> 3.1.0'
# Sanitize user input
gem 'sanitize', '~> 4.6.5'
gem 'sanitize', '~> 4.6'
gem 'babosa', '~> 1.0.2'
# Sanitizes SVG input
@ -303,7 +306,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~> 0.9.3'
gem 'prometheus-client-mmap', '~> 0.9.4'
gem 'raindrops', '~> 0.18'
end
@ -323,6 +326,7 @@ group :development do
end
group :development, :test do
gem 'bootsnap', '~> 1.3'
gem 'bullet', '~> 5.5.0', require: !!ENV['ENABLE_BULLET']
gem 'pry-byebug', '~> 3.4.1', platform: :mri
gem 'pry-rails', '~> 0.3.4'
@ -351,9 +355,9 @@ group :development, :test do
gem 'spring', '~> 2.0.0'
gem 'spring-commands-rspec', '~> 1.0.4'
gem 'gitlab-styles', '~> 2.3', require: false
gem 'gitlab-styles', '~> 2.4', require: false
# Pin these dependencies, otherwise a new rule could break the CI pipelines
gem 'rubocop', '~> 0.52.1'
gem 'rubocop', '~> 0.54.0'
gem 'rubocop-rspec', '~> 1.22.1'
gem 'scss_lint', '~> 0.56.0', require: false
@ -396,6 +400,7 @@ gem 'email_reply_trimmer', '~> 0.1'
gem 'html2text'
gem 'ruby-prof', '~> 0.17.0'
gem 'rbtrace', '~> 0.4', require: false
# OAuth
gem 'oauth2', '~> 1.4'
@ -418,7 +423,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.105.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.113.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed

View file

@ -87,6 +87,8 @@ GEM
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
blankslate (2.1.2.4)
bootsnap (1.3.1)
msgpack (~> 1.0)
bootstrap_form (2.7.0)
brakeman (4.2.1)
browser (2.2.0)
@ -282,7 +284,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gitaly-proto (0.105.0)
gitaly-proto (0.113.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
@ -312,8 +314,8 @@ GEM
mime-types (>= 1.16)
posix-spawn (~> 0.3)
gitlab-markup (1.6.4)
gitlab-styles (2.3.2)
rubocop (~> 0.51)
gitlab-styles (2.4.1)
rubocop (~> 0.54.0)
rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19)
gitlab_omniauth-ldap (2.0.4)
@ -359,7 +361,7 @@ GEM
grape-entity (0.7.1)
activesupport (>= 4.0)
multi_json (>= 1.3.2)
grape-path-helpers (1.0.5)
grape-path-helpers (1.0.6)
activesupport (>= 4, < 5.1)
grape (~> 1.0)
rake (~> 12)
@ -373,7 +375,8 @@ GEM
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7)
haml (4.0.7)
haml (5.0.4)
temple (>= 0.8.0)
tilt
haml_lint (0.26.0)
haml (>= 4.0, < 5.1)
@ -381,10 +384,11 @@ GEM
rake (>= 10, < 13)
rubocop (>= 0.49.0)
sysexits (~> 1.1)
hamlit (2.6.1)
temple (~> 0.7.6)
hamlit (2.8.8)
temple (>= 0.8.0)
thor
tilt
hangouts-chat (0.0.5)
hashdiff (0.3.4)
hashie (3.5.7)
hashie-forbidden_attributes (0.1.1)
@ -394,7 +398,7 @@ GEM
hipchat (1.5.2)
httparty
mimemagic
html-pipeline (2.8.3)
html-pipeline (2.8.4)
activesupport (>= 2)
nokogiri (>= 1.4)
html2text (0.2.0)
@ -500,6 +504,7 @@ GEM
mini_portile2 (2.3.0)
minitest (5.7.0)
mousetrap-rails (1.4.6)
msgpack (1.2.4)
multi_json (1.13.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
@ -510,7 +515,7 @@ GEM
net-ldap (0.16.0)
net-ssh (5.0.1)
netrc (0.11.0)
nokogiri (1.8.3)
nokogiri (1.8.4)
mini_portile2 (~> 2.3.0)
nokogumbo (1.5.0)
nokogiri
@ -630,7 +635,7 @@ GEM
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.9.3)
prometheus-client-mmap (0.9.4)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
@ -696,6 +701,10 @@ GEM
ffi (>= 0.5.0, < 2)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
rbtrace (0.4.10)
ffi (>= 1.0.6)
msgpack (>= 0.4.3)
trollop (>= 1.16.2)
rdoc (6.0.4)
re2 (1.1.1)
recaptcha (3.0.0)
@ -736,7 +745,7 @@ GEM
retriable (3.1.1)
rinku (2.0.0)
rotp (2.1.2)
rouge (3.1.1)
rouge (3.2.1)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
@ -776,16 +785,16 @@ GEM
pg
rails
sqlite3
rubocop (0.52.1)
rubocop (0.54.0)
parallel (~> 1.10)
parser (>= 2.4.0.2, < 3.0)
parser (>= 2.5)
powerpack (~> 0.1)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-gitlab-security (0.1.1)
rubocop (>= 0.51)
rubocop-rspec (1.22.1)
rubocop-rspec (1.22.2)
rubocop (>= 0.52.1)
ruby-enum (0.7.2)
i18n
@ -802,9 +811,9 @@ GEM
rubyzip (1.2.1)
rufus-scheduler (3.4.0)
et-orbi (~> 1.0)
rugged (0.27.2)
rugged (0.27.4)
safe_yaml (1.0.4)
sanitize (4.6.5)
sanitize (4.6.6)
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4)
@ -889,7 +898,7 @@ GEM
sys-filesystem (1.1.6)
ffi
sysexits (1.2.0)
temple (0.7.7)
temple (0.8.0)
test-prof (0.2.5)
test_after_commit (1.1.0)
activerecord (>= 3.2)
@ -900,13 +909,14 @@ GEM
rack (>= 1, < 3)
thor (0.19.4)
thread_safe (0.3.6)
tilt (2.0.6)
tilt (2.0.8)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
toml (0.1.2)
parslet (~> 1.5.0)
toml-rb (1.0.0)
citrus (~> 3.0, > 3.0)
trollop (2.1.3)
truncato (0.7.10)
htmlentities (~> 4.3.1)
nokogiri (~> 1.8.0, >= 1.7.0)
@ -986,6 +996,7 @@ DEPENDENCIES
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2)
bootsnap (~> 1.3)
bootstrap_form (~> 2.7.0)
brakeman (~> 4.2)
browser (~> 2.2)
@ -1037,13 +1048,13 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.105.0)
gitaly-proto (~> 0.113.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
gitlab-gollum-rugged_adapter (~> 0.4.4)
gitlab-markup (~> 1.6.4)
gitlab-styles (~> 2.3)
gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2)
google-api-client (~> 0.19.8)
@ -1057,7 +1068,8 @@ DEPENDENCIES
graphql (~> 1.8.0)
grpc (~> 1.11.0)
haml_lint (~> 0.26.0)
hamlit (~> 2.6.1)
hamlit (~> 2.8.8)
hangouts-chat (~> 0.0.5)
hashie-forbidden_attributes
health_check (~> 2.6.0)
hipchat (~> 1.5.0)
@ -1114,7 +1126,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.9.3)
prometheus-client-mmap (~> 0.9.4)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
@ -1127,6 +1139,7 @@ DEPENDENCIES
rainbow (~> 2.2)
raindrops (~> 0.18)
rblineprof (~> 0.3.6)
rbtrace (~> 0.4)
rdoc (~> 6.0)
re2 (~> 1.1.1)
recaptcha (~> 3.0)
@ -1143,7 +1156,7 @@ DEPENDENCIES
rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5)
rubocop (~> 0.52.1)
rubocop (~> 0.54.0)
rubocop-rspec (~> 1.22.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.17.0)
@ -1151,7 +1164,7 @@ DEPENDENCIES
ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
rugged (~> 0.27)
sanitize (~> 4.6.5)
sanitize (~> 4.6)
sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7)

View file

@ -79,7 +79,7 @@ GEM
babosa (1.0.2)
base32 (0.3.2)
batch-loader (1.2.1)
bcrypt (3.1.11)
bcrypt (3.1.12)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
better_errors (2.1.1)
@ -90,6 +90,8 @@ GEM
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
blankslate (2.1.2.4)
bootsnap (1.3.1)
msgpack (~> 1.0)
bootstrap_form (2.7.0)
brakeman (4.2.1)
browser (2.2.0)
@ -111,7 +113,7 @@ GEM
capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
carrierwave (1.2.1)
carrierwave (1.2.3)
activemodel (>= 4.0.0)
activesupport (>= 4.0.0)
mime-types (>= 1.16)
@ -285,7 +287,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gitaly-proto (0.105.0)
gitaly-proto (0.113.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
@ -315,8 +317,8 @@ GEM
mime-types (>= 1.16)
posix-spawn (~> 0.3)
gitlab-markup (1.6.4)
gitlab-styles (2.3.2)
rubocop (~> 0.51)
gitlab-styles (2.4.1)
rubocop (~> 0.54.0)
rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19)
gitlab_omniauth-ldap (2.0.4)
@ -362,7 +364,7 @@ GEM
grape-entity (0.7.1)
activesupport (>= 4.0)
multi_json (>= 1.3.2)
grape-path-helpers (1.0.5)
grape-path-helpers (1.0.6)
activesupport (>= 4, < 5.1)
grape (~> 1.0)
rake (~> 12)
@ -376,7 +378,8 @@ GEM
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7)
haml (4.0.7)
haml (5.0.4)
temple (>= 0.8.0)
tilt
haml_lint (0.26.0)
haml (>= 4.0, < 5.1)
@ -384,10 +387,11 @@ GEM
rake (>= 10, < 13)
rubocop (>= 0.49.0)
sysexits (~> 1.1)
hamlit (2.6.1)
temple (~> 0.7.6)
hamlit (2.8.8)
temple (>= 0.8.0)
thor
tilt
hangouts-chat (0.0.5)
hashdiff (0.3.4)
hashie (3.5.7)
hashie-forbidden_attributes (0.1.1)
@ -397,7 +401,7 @@ GEM
hipchat (1.5.2)
httparty
mimemagic
html-pipeline (2.8.3)
html-pipeline (2.8.4)
activesupport (>= 2)
nokogiri (>= 1.4)
html2text (0.2.0)
@ -503,6 +507,7 @@ GEM
mini_portile2 (2.3.0)
minitest (5.7.0)
mousetrap-rails (1.4.6)
msgpack (1.2.4)
multi_json (1.13.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
@ -514,7 +519,7 @@ GEM
net-ssh (5.0.1)
netrc (0.11.0)
nio4r (2.3.1)
nokogiri (1.8.2)
nokogiri (1.8.4)
mini_portile2 (~> 2.3.0)
nokogumbo (1.5.0)
nokogiri
@ -634,7 +639,7 @@ GEM
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.9.3)
prometheus-client-mmap (0.9.4)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
@ -705,6 +710,10 @@ GEM
ffi (>= 0.5.0, < 2)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
rbtrace (0.4.10)
ffi (>= 1.0.6)
msgpack (>= 0.4.3)
trollop (>= 1.16.2)
rdoc (6.0.4)
re2 (1.1.1)
recaptcha (3.0.0)
@ -745,7 +754,7 @@ GEM
retriable (3.1.1)
rinku (2.0.0)
rotp (2.1.2)
rouge (3.1.1)
rouge (3.2.0)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
@ -785,16 +794,16 @@ GEM
pg
rails
sqlite3
rubocop (0.52.1)
rubocop (0.54.0)
parallel (~> 1.10)
parser (>= 2.4.0.2, < 3.0)
parser (>= 2.5)
powerpack (~> 0.1)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-gitlab-security (0.1.1)
rubocop (>= 0.51)
rubocop-rspec (1.22.1)
rubocop-rspec (1.22.2)
rubocop (>= 0.52.1)
ruby-enum (0.7.2)
i18n
@ -811,9 +820,9 @@ 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 (4.6.5)
sanitize (4.6.6)
crass (~> 1.0.2)
nokogiri (>= 1.4.4)
nokogumbo (~> 1.4)
@ -877,7 +886,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)
@ -898,7 +907,7 @@ GEM
sys-filesystem (1.1.6)
ffi
sysexits (1.2.0)
temple (0.7.7)
temple (0.8.0)
test-prof (0.2.5)
text (1.3.1)
thin (1.7.0)
@ -907,13 +916,14 @@ GEM
rack (>= 1, < 3)
thor (0.19.4)
thread_safe (0.3.6)
tilt (2.0.6)
tilt (2.0.8)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
toml (0.1.2)
parslet (~> 1.5.0)
toml-rb (1.0.0)
citrus (~> 3.0, > 3.0)
trollop (2.1.3)
truncato (0.7.10)
htmlentities (~> 4.3.1)
nokogiri (~> 1.8.0, >= 1.7.0)
@ -996,6 +1006,7 @@ DEPENDENCIES
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2)
bootsnap (~> 1.3)
bootstrap_form (~> 2.7.0)
brakeman (~> 4.2)
browser (~> 2.2)
@ -1047,13 +1058,13 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.105.0)
gitaly-proto (~> 0.113.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
gitlab-gollum-rugged_adapter (~> 0.4.4)
gitlab-markup (~> 1.6.4)
gitlab-styles (~> 2.3)
gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2)
google-api-client (~> 0.19.8)
@ -1067,7 +1078,8 @@ DEPENDENCIES
graphql (~> 1.8.0)
grpc (~> 1.11.0)
haml_lint (~> 0.26.0)
hamlit (~> 2.6.1)
hamlit (~> 2.8.8)
hangouts-chat (~> 0.0.5)
hashie-forbidden_attributes
health_check (~> 2.6.0)
hipchat (~> 1.5.0)
@ -1124,7 +1136,7 @@ DEPENDENCIES
peek-sidekiq (~> 1.0.3)
pg (~> 0.18.2)
premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.9.3)
prometheus-client-mmap (~> 0.9.4)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
@ -1138,6 +1150,7 @@ DEPENDENCIES
rainbow (~> 2.2)
raindrops (~> 0.18)
rblineprof (~> 0.3.6)
rbtrace (~> 0.4)
rdoc (~> 6.0)
re2 (~> 1.1.1)
recaptcha (~> 3.0)
@ -1154,7 +1167,7 @@ DEPENDENCIES
rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5)
rubocop (~> 0.52.1)
rubocop (~> 0.54.0)
rubocop-rspec (~> 1.22.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.17.0)
@ -1162,7 +1175,7 @@ DEPENDENCIES
ruby_parser (~> 3.8)
rufus-scheduler (~> 3.4)
rugged (~> 0.27)
sanitize (~> 4.6.5)
sanitize (~> 4.6)
sass-rails (~> 5.0.6)
scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7)
@ -1204,4 +1217,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
1.16.2
1.16.3

View file

@ -1,11 +1,5 @@
# GitLab
[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
[![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
## Test coverage
- [![Ruby coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) Ruby

View file

@ -2,9 +2,9 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require File.expand_path('../config/application', __FILE__)
require File.expand_path('config/application', __dir__)
relative_url_conf = File.expand_path('../config/initializers/relative_url', __FILE__)
relative_url_conf = File.expand_path('config/initializers/relative_url', __dir__)
require relative_url_conf if File.exist?("#{relative_url_conf}.rb")
Gitlab::Application.load_tasks

View file

@ -1 +1 @@
11.1.8
11.2.8

View file

@ -244,6 +244,18 @@ const Api = {
});
},
branches(id, query = '', options = {}) {
const url = Api.buildUrl(this.createBranchPath).replace(':id', encodeURIComponent(id));
return axios.get(url, {
params: {
search: query,
per_page: 20,
...options,
},
});
},
createBranch(id, { ref, branch }) {
const url = Api.buildUrl(this.createBranchPath).replace(':id', encodeURIComponent(id));

View file

@ -53,4 +53,8 @@ export default class Autosave {
return window.localStorage.removeItem(this.key);
}
dispose() {
this.field.off('input');
}
}

View file

@ -33,19 +33,24 @@ const categoryLabelMap = {
const IS_VISIBLE = 'is-visible';
const IS_RENDERED = 'is-rendered';
class AwardsHandler {
export class AwardsHandler {
constructor(emoji) {
this.emoji = emoji;
this.eventListeners = [];
this.toggleButtonSelector = '.js-add-award';
this.menuClass = 'js-award-emoji-menu';
}
bindEvents() {
// If the user shows intent let's pre-build the menu
this.registerEventListener(
'one',
$(document),
'mouseenter focus',
'.js-add-award',
this.toggleButtonSelector,
'mouseenter focus',
() => {
const $menu = $('.emoji-menu');
const $menu = $(`.${this.menuClass}`);
if ($menu.length === 0) {
requestAnimationFrame(() => {
this.createEmojiMenu();
@ -53,7 +58,7 @@ class AwardsHandler {
}
},
);
this.registerEventListener('on', $(document), 'click', '.js-add-award', e => {
this.registerEventListener('on', $(document), 'click', this.toggleButtonSelector, e => {
e.stopPropagation();
e.preventDefault();
this.showEmojiMenu($(e.currentTarget));
@ -61,15 +66,17 @@ class AwardsHandler {
this.registerEventListener('on', $('html'), 'click', e => {
const $target = $(e.target);
if (!$target.closest('.emoji-menu').length) {
if (!$target.closest(`.${this.menuClass}`).length) {
$('.js-awards-block.current').removeClass('current');
if ($('.emoji-menu').is(':visible')) {
$('.js-add-award.is-active').removeClass('is-active');
this.hideMenuElement($('.emoji-menu'));
if ($(`.${this.menuClass}`).is(':visible')) {
$(`${this.toggleButtonSelector}.is-active`).removeClass('is-active');
this.hideMenuElement($(`.${this.menuClass}`));
}
}
});
this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', e => {
const emojiButtonSelector = `.js-awards-block .js-emoji-btn, .${this.menuClass} .js-emoji-btn`;
this.registerEventListener('on', $(document), 'click', emojiButtonSelector, e => {
e.preventDefault();
const $target = $(e.currentTarget);
const $glEmojiElement = $target.find('gl-emoji');
@ -101,7 +108,7 @@ class AwardsHandler {
$addBtn.closest('.js-awards-block').addClass('current');
}
const $menu = $('.emoji-menu');
const $menu = $(`.${this.menuClass}`);
const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent();
const $userAuthored = this.isUserAuthored($addBtn);
if ($menu.length) {
@ -118,7 +125,7 @@ class AwardsHandler {
} else {
$addBtn.addClass('is-loading is-active');
this.createEmojiMenu(() => {
const $createdMenu = $('.emoji-menu');
const $createdMenu = $(`.${this.menuClass}`);
$addBtn.removeClass('is-loading');
this.positionMenu($createdMenu, $addBtn);
return setTimeout(() => {
@ -156,7 +163,7 @@ class AwardsHandler {
}
const emojiMenuMarkup = `
<div class="emoji-menu">
<div class="emoji-menu ${this.menuClass}">
<input type="text" name="emoji-menu-search" value="" class="js-emoji-menu-search emoji-search search-input form-control" placeholder="Search emoji" />
<div class="emoji-menu-content">
@ -185,7 +192,7 @@ class AwardsHandler {
// Avoid the jank and render the remaining categories separately
// This will take more time, but makes UI more responsive
const menu = document.querySelector('.emoji-menu');
const menu = document.querySelector(`.${this.menuClass}`);
const emojiContentElement = menu.querySelector('.emoji-menu-content');
const remainingCategories = Object.keys(categoryMap).slice(1);
const allCategoriesAddedPromise = remainingCategories.reduce(
@ -270,9 +277,9 @@ class AwardsHandler {
if (isInVueNoteablePage() && !isMainAwardsBlock) {
const id = votesBlock.attr('id').replace('note_', '');
this.hideMenuElement($('.emoji-menu'));
this.hideMenuElement($(`.${this.menuClass}`));
$('.js-add-award.is-active').removeClass('is-active');
$(`${this.toggleButtonSelector}.is-active`).removeClass('is-active');
const toggleAwardEvent = new CustomEvent('toggleAward', {
detail: {
awardName: emoji,
@ -291,9 +298,9 @@ class AwardsHandler {
return typeof callback === 'function' ? callback() : undefined;
});
this.hideMenuElement($('.emoji-menu'));
this.hideMenuElement($(`.${this.menuClass}`));
return $('.js-add-award.is-active').removeClass('is-active');
return $(`${this.toggleButtonSelector}.is-active`).removeClass('is-active');
}
addAwardToEmojiBar(votesBlock, emoji, checkForMutuality) {
@ -321,7 +328,7 @@ class AwardsHandler {
getVotesBlock() {
if (isInVueNoteablePage()) {
const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
const $el = $(`${this.toggleButtonSelector}.is-active`).closest('.note.timeline-entry');
if ($el.length) {
return $el;
@ -458,7 +465,7 @@ class AwardsHandler {
}
createEmoji(votesBlock, emoji) {
if ($('.emoji-menu').length) {
if ($(`.${this.menuClass}`).length) {
this.createAwardButtonForVotesBlock(votesBlock, emoji);
}
this.createEmojiMenu(() => {
@ -538,7 +545,7 @@ class AwardsHandler {
this.searchEmojis(term);
});
const $menu = $('.emoji-menu');
const $menu = $(`.${this.menuClass}`);
this.registerEventListener('on', $menu, transitionEndEventString, e => {
if (e.target === e.currentTarget) {
// Clear the search
@ -608,7 +615,7 @@ class AwardsHandler {
this.eventListeners.forEach(entry => {
entry.element.off.call(entry.element, ...entry.args);
});
$('.emoji-menu').remove();
$(`.${this.menuClass}`).remove();
}
}
@ -616,7 +623,11 @@ let awardsHandlerPromise = null;
export default function loadAwardsHandler(reload = false) {
if (!awardsHandlerPromise || reload) {
awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(
Emoji => new AwardsHandler(Emoji),
Emoji => {
const awardsHandler = new AwardsHandler(Emoji);
awardsHandler.bindEvents();
return awardsHandler;
},
);
}
return awardsHandlerPromise;

View file

@ -93,7 +93,7 @@ export default {
<icon
:size="16"
class="prepend-left-8 append-right-8"
name="doc_image"
name="doc-image"
aria-hidden="true"
/>
</div>

View file

@ -2,6 +2,9 @@
import Sortable from 'sortablejs';
import Vue from 'vue';
import { n__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list.vue';
import BoardBlankState from './board_blank_state.vue';
@ -17,6 +20,10 @@ gl.issueBoards.Board = Vue.extend({
boardList,
'board-delete': gl.issueBoards.BoardDelete,
BoardBlankState,
Icon,
},
directives: {
Tooltip,
},
props: {
list: {
@ -46,6 +53,12 @@ gl.issueBoards.Board = Vue.extend({
filter: Store.filter,
};
},
computed: {
counterTooltip() {
const { issuesSize } = this.list;
return `${n__('%d issue', '%d issues', issuesSize)}`;
},
},
watch: {
filter: {
handler() {

View file

@ -112,12 +112,20 @@ export default {
if (e.target) {
const containerEl = e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list');
const toBoardType = containerEl.dataset.boardType;
const cloneActions = {
label: ['milestone', 'assignee'],
assignee: ['milestone', 'label'],
milestone: ['label', 'assignee'],
};
if (toBoardType) {
const fromBoardType = this.list.type;
// For each list we check if the destination list is
// a the list were we should clone the issue
const shouldClone = Object.entries(cloneActions).some(entry => (
fromBoardType === entry[0] && entry[1].includes(toBoardType)));
if ((fromBoardType === 'assignee' && toBoardType === 'label') ||
(fromBoardType === 'label' && toBoardType === 'assignee')) {
if (shouldClone) {
return 'clone';
}
}
@ -145,7 +153,8 @@ export default {
});
},
onUpdate: (e) => {
const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
const sortedArray = this.sortable.toArray()
.filter(id => id !== '-1');
gl.issueBoards.BoardsStore
.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
},

View file

@ -105,7 +105,7 @@ export default {
</div>
<label
:for="list.id + '-title'"
class="label-light"
class="label-bold"
>
Title
</label>
@ -115,6 +115,7 @@ export default {
:id="list.id + '-title'"
class="form-control"
type="text"
name="issue_title"
autocomplete="off"
/>
<project-select

View file

@ -51,6 +51,16 @@ gl.issueBoards.BoardSidebar = Vue.extend({
canRemove() {
return !this.list.preset;
},
hasLabels() {
return this.issue.labels && this.issue.labels.length;
},
labelDropdownTitle() {
return this.hasLabels ?
`${this.issue.labels[0].title} ${this.issue.labels.length - 1}+ more` : 'Label';
},
selectedLabels() {
return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : '';
}
},
watch: {
detail: {

View file

@ -46,7 +46,7 @@ export default {
selectable: true,
data: (term, callback) => {
this.loading = true;
return Api.groupProjects(this.groupId, term, projects => {
return Api.groupProjects(this.groupId, term, {}, projects => {
this.loading = false;
callback(projects);
});
@ -68,7 +68,7 @@ export default {
<template>
<div>
<label class="label-light prepend-top-10">
<label class="label-bold prepend-top-10">
Project
</label>
<div

View file

@ -1,5 +1,3 @@
/* eslint-disable quote-props, comma-dangle */
import $ from 'jquery';
import _ from 'underscore';
import Vue from 'vue';
@ -47,7 +45,7 @@ export default () => {
gl.IssueBoardsApp = new Vue({
el: $boardApp,
components: {
'board': gl.issueBoards.Board,
board: gl.issueBoards.Board,
'board-sidebar': gl.issueBoards.BoardSidebar,
BoardAddIssuesModal,
},
@ -65,11 +63,11 @@ export default () => {
defaultAvatar: $boardApp.dataset.defaultAvatar,
},
computed: {
detailIssueVisible () {
detailIssueVisible() {
return Object.keys(this.detailIssue.issue).length;
},
},
created () {
created() {
gl.boardService = new BoardService({
boardsEndpoint: this.boardsEndpoint,
listsEndpoint: this.listsEndpoint,
@ -89,15 +87,16 @@ export default () => {
eventHub.$off('clearDetailIssue', this.clearDetailIssue);
sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
},
mounted () {
mounted() {
this.filterManager = new FilteredSearchBoards(Store.filter, true, Store.cantEdit);
this.filterManager.setup();
Store.disabled = this.disabled;
gl.boardService.all()
gl.boardService
.all()
.then(res => res.data)
.then((data) => {
data.forEach((board) => {
.then(data => {
data.forEach(board => {
const list = Store.addList(board, this.defaultAvatar);
if (list.type === 'closed') {
@ -126,7 +125,7 @@ export default () => {
newIssue.setFetchingState('subscriptions', true);
BoardService.getIssueInfo(sidebarInfoEndpoint)
.then(res => res.data)
.then((data) => {
.then(data => {
newIssue.setFetchingState('subscriptions', false);
newIssue.updateData({
subscribed: data.subscribed,
@ -159,7 +158,7 @@ export default () => {
Flash(__('An error occurred when toggling the notification subscription'));
});
}
}
},
},
});
@ -168,77 +167,81 @@ export default () => {
data: {
filters: Store.state.filters,
},
mounted () {
mounted() {
gl.issueBoards.newListDropdownInit();
},
});
gl.IssueBoardsModalAddBtn = new Vue({
el: document.getElementById('js-add-issues-btn'),
mixins: [modalMixin],
data() {
return {
modal: ModalStore.store,
store: Store.state,
canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
};
},
computed: {
disabled() {
if (!this.store) {
return true;
}
return !this.store.lists.filter(list => !list.preset).length;
},
tooltipTitle() {
if (this.disabled) {
return 'Please add a list to your board first';
}
const issueBoardsModal = document.getElementById('js-add-issues-btn');
return '';
if (issueBoardsModal) {
gl.IssueBoardsModalAddBtn = new Vue({
el: issueBoardsModal,
mixins: [modalMixin],
data() {
return {
modal: ModalStore.store,
store: Store.state,
canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
};
},
},
watch: {
disabled() {
computed: {
disabled() {
if (!this.store) {
return true;
}
return !this.store.lists.filter(list => !list.preset).length;
},
tooltipTitle() {
if (this.disabled) {
return 'Please add a list to your board first';
}
return '';
},
},
watch: {
disabled() {
this.updateTooltip();
},
},
mounted() {
this.updateTooltip();
},
},
mounted() {
this.updateTooltip();
},
methods: {
updateTooltip() {
const $tooltip = $(this.$refs.addIssuesButton);
methods: {
updateTooltip() {
const $tooltip = $(this.$refs.addIssuesButton);
this.$nextTick(() => {
if (this.disabled) {
$tooltip.tooltip();
} else {
$tooltip.tooltip('dispose');
this.$nextTick(() => {
if (this.disabled) {
$tooltip.tooltip();
} else {
$tooltip.tooltip('dispose');
}
});
},
openModal() {
if (!this.disabled) {
this.toggleModal(true);
}
});
},
},
openModal() {
if (!this.disabled) {
this.toggleModal(true);
}
},
},
template: `
<div class="board-extra-actions">
<button
class="btn btn-create prepend-left-10"
type="button"
data-placement="bottom"
ref="addIssuesButton"
:class="{ 'disabled': disabled }"
:title="tooltipTitle"
:aria-disabled="disabled"
v-if="canAdminList"
@click="openModal">
Add issues
</button>
</div>
`,
});
template: `
<div class="board-extra-actions">
<button
class="btn btn-create prepend-left-10"
type="button"
data-placement="bottom"
ref="addIssuesButton"
:class="{ 'disabled': disabled }"
:title="tooltipTitle"
:aria-disabled="disabled"
v-if="canAdminList"
@click="openModal">
Add issues
</button>
</div>
`,
});
}
};

View file

@ -136,6 +136,8 @@ class List {
}
this.createIssues(data.issues);
return data;
});
}

View file

@ -108,6 +108,16 @@ gl.issueBoards.BoardsStore = {
issue.findAssignee(listTo.assignee)) {
const targetIssue = listTo.findIssue(issue.id);
targetIssue.removeAssignee(listFrom.assignee);
} else if (listTo.type === 'milestone') {
const currentMilestone = issue.milestone;
const currentLists = this.state.lists
.filter(list => (list.type === 'milestone' && list.id !== listTo.id))
.filter(list => list.issues.some(listIssue => issue.id === listIssue.id));
issue.removeMilestone(currentMilestone);
issue.addMilestone(listTo.milestone);
currentLists.forEach(currentList => currentList.removeIssue(issue));
listTo.addIssue(issue, listFrom, newIndex);
} else {
// Add to new lists issues if it doesn't already exist
listTo.addIssue(issue, listFrom, newIndex);
@ -125,11 +135,20 @@ gl.issueBoards.BoardsStore = {
} else if (listTo.type === 'backlog' && listFrom.type === 'assignee') {
issue.removeAssignee(listFrom.assignee);
listFrom.removeIssue(issue);
} else if ((listTo.type !== 'label' && listFrom.type === 'assignee') ||
(listTo.type !== 'assignee' && listFrom.type === 'label')) {
} else if (listTo.type === 'backlog' && listFrom.type === 'milestone') {
issue.removeMilestone(listFrom.milestone);
listFrom.removeIssue(issue);
} else if (this.shouldRemoveIssue(listFrom, listTo)) {
listFrom.removeIssue(issue);
}
},
shouldRemoveIssue(listFrom, listTo) {
return (
(listTo.type !== 'label' && listFrom.type === 'assignee') ||
(listTo.type !== 'assignee' && listFrom.type === 'label') ||
(listFrom.type === 'backlog')
);
},
moveIssueInList (list, issue, oldIndex, newIndex, idArray) {
const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
const afterId = parseInt(idArray[newIndex + 1], 10) || null;
@ -138,7 +157,7 @@ gl.issueBoards.BoardsStore = {
},
findList (key, val, type = 'label') {
const filteredList = this.state.lists.filter((list) => {
const byType = type ? (list.type === type) || (list.type === 'assignee') : true;
const byType = type ? (list.type === type) || (list.type === 'assignee') || (list.type === 'milestone') : true;
return list[key] === val && byType;
});

View file

@ -6,7 +6,7 @@ import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub';
import {
APPLICATION_INSTALLED,
APPLICATION_STATUS,
REQUEST_LOADING,
REQUEST_SUCCESS,
REQUEST_FAILURE,
@ -162,8 +162,10 @@ export default class Clusters {
if (type === 'password') {
this.tokenField.setAttribute('type', 'text');
this.showTokenButton.textContent = s__('ClusterIntegration|Hide');
} else {
this.tokenField.setAttribute('type', 'password');
this.showTokenButton.textContent = s__('ClusterIntegration|Show');
}
}
@ -175,8 +177,8 @@ export default class Clusters {
checkForNewInstalls(prevApplicationMap, newApplicationMap) {
const appTitles = Object.keys(newApplicationMap)
.filter(appId => newApplicationMap[appId].status === APPLICATION_INSTALLED &&
prevApplicationMap[appId].status !== APPLICATION_INSTALLED &&
.filter(appId => newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED &&
prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED &&
prevApplicationMap[appId].status !== null)
.map(appId => newApplicationMap[appId].title);

View file

@ -4,12 +4,7 @@
import eventHub from '../event_hub';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import {
APPLICATION_NOT_INSTALLABLE,
APPLICATION_SCHEDULED,
APPLICATION_INSTALLABLE,
APPLICATION_INSTALLING,
APPLICATION_INSTALLED,
APPLICATION_ERROR,
APPLICATION_STATUS,
REQUEST_LOADING,
REQUEST_SUCCESS,
REQUEST_FAILURE,
@ -59,49 +54,57 @@
},
},
computed: {
isUnknownStatus() {
return !this.isKnownStatus && this.status !== null;
},
isKnownStatus() {
return Object.values(APPLICATION_STATUS).includes(this.status);
},
rowJsClass() {
return `js-cluster-application-row-${this.id}`;
},
installButtonLoading() {
return !this.status ||
this.status === APPLICATION_SCHEDULED ||
this.status === APPLICATION_INSTALLING ||
this.status === APPLICATION_STATUS.SCHEDULED ||
this.status === APPLICATION_STATUS.INSTALLING ||
this.requestStatus === REQUEST_LOADING;
},
installButtonDisabled() {
// Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but
// Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but
// we already made a request to install and are just waiting for the real-time
// to sync up.
return (this.status !== APPLICATION_INSTALLABLE
&& this.status !== APPLICATION_ERROR) ||
return ((this.status !== APPLICATION_STATUS.INSTALLABLE
&& this.status !== APPLICATION_STATUS.ERROR) ||
this.requestStatus === REQUEST_LOADING ||
this.requestStatus === REQUEST_SUCCESS;
this.requestStatus === REQUEST_SUCCESS) && this.isKnownStatus;
},
installButtonLabel() {
let label;
if (
this.status === APPLICATION_NOT_INSTALLABLE ||
this.status === APPLICATION_INSTALLABLE ||
this.status === APPLICATION_ERROR
this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
this.status === APPLICATION_STATUS.INSTALLABLE ||
this.status === APPLICATION_STATUS.ERROR ||
this.isUnknownStatus
) {
label = s__('ClusterIntegration|Install');
} else if (this.status === APPLICATION_SCHEDULED ||
this.status === APPLICATION_INSTALLING) {
} else if (this.status === APPLICATION_STATUS.SCHEDULED ||
this.status === APPLICATION_STATUS.INSTALLING) {
label = s__('ClusterIntegration|Installing');
} else if (this.status === APPLICATION_INSTALLED) {
} else if (this.status === APPLICATION_STATUS.INSTALLED ||
this.status === APPLICATION_STATUS.UPDATED) {
label = s__('ClusterIntegration|Installed');
}
return label;
},
showManageButton() {
return this.manageLink && this.status === APPLICATION_INSTALLED;
return this.manageLink && this.status === APPLICATION_STATUS.INSTALLED;
},
manageButtonLabel() {
return s__('ClusterIntegration|Manage');
},
hasError() {
return this.status === APPLICATION_ERROR ||
return this.status === APPLICATION_STATUS.ERROR ||
this.requestStatus === REQUEST_FAILURE;
},
generalErrorDescription() {
@ -182,7 +185,7 @@
</div>
</div>
<div
v-if="hasError"
v-if="hasError || isUnknownStatus"
class="gl-responsive-table-row-layout"
role="row"
>

View file

@ -3,7 +3,7 @@ import _ from 'underscore';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { APPLICATION_INSTALLED, INGRESS } from '../constants';
import { APPLICATION_STATUS, INGRESS } from '../constants';
export default {
components: {
@ -58,7 +58,7 @@ export default {
return INGRESS;
},
ingressInstalled() {
return this.applications.ingress.status === APPLICATION_INSTALLED;
return this.applications.ingress.status === APPLICATION_STATUS.INSTALLED;
},
ingressExternalIp() {
return this.applications.ingress.externalIp;
@ -122,7 +122,7 @@ export default {
);
},
jupyterInstalled() {
return this.applications.jupyter.status === APPLICATION_INSTALLED;
return this.applications.jupyter.status === APPLICATION_STATUS.INSTALLED;
},
jupyterHostname() {
return this.applications.jupyter.hostname;

View file

@ -1,10 +1,13 @@
// These need to match what is returned from the server
export const APPLICATION_NOT_INSTALLABLE = 'not_installable';
export const APPLICATION_INSTALLABLE = 'installable';
export const APPLICATION_SCHEDULED = 'scheduled';
export const APPLICATION_INSTALLING = 'installing';
export const APPLICATION_INSTALLED = 'installed';
export const APPLICATION_ERROR = 'errored';
export const APPLICATION_STATUS = {
NOT_INSTALLABLE: 'not_installable',
INSTALLABLE: 'installable',
SCHEDULED: 'scheduled',
INSTALLING: 'installing',
INSTALLED: 'installed',
UPDATED: 'updated',
ERROR: 'errored',
};
// These are only used client-side
export const REQUEST_LOADING = 'request-loading';

View file

@ -0,0 +1,4 @@
import Vue from 'vue';
import progressBar from '@gitlab-org/gitlab-ui/dist/base/progress_bar';
Vue.component('gl-progress-bar', progressBar);

View file

@ -3,4 +3,5 @@ import './polyfills';
import './jquery';
import './bootstrap';
import './vue';
import './gitlab_ui';
import '../lib/utils/axios_utils';

View file

@ -8,13 +8,16 @@ import 'core-js/fn/object/assign';
import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at';
import 'core-js/fn/string/from-code-point';
import 'core-js/fn/string/includes';
import 'core-js/fn/symbol';
import 'core-js/es6/map';
import 'core-js/es6/weak-map';
// Browser polyfills
import 'classlist-polyfill';
import 'formdata-polyfill';
import './polyfills/custom_event';
import './polyfills/element';
import './polyfills/event';
import './polyfills/nodelist';
import './polyfills/request_idle_callback';

View file

@ -0,0 +1,17 @@
window.requestIdleCallback =
window.requestIdleCallback ||
function requestShim(cb) {
const start = Date.now();
return setTimeout(() => {
cb({
didTimeout: false,
timeRemaining: () => Math.max(0, 50 - (Date.now() - start)),
});
}, 1);
};
window.cancelIdleCallback =
window.cancelIdleCallback ||
function cancelShim(id) {
clearTimeout(id);
};

View file

@ -2,13 +2,16 @@ import $ from 'jquery';
import { rstrip } from './lib/utils/common_utils';
function openConfirmDangerModal($form, text) {
const $input = $('.js-confirm-danger-input');
$input.val('');
$('.js-confirm-text').text(text || '');
$('.js-confirm-danger-input').val('');
$('#modal-confirm-danger').modal('show');
const confirmTextMatch = $('.js-confirm-danger-match').text();
const $submit = $('.js-confirm-danger-submit');
$submit.disable();
$input.focus();
$('.js-confirm-danger-input').off('input').on('input', function handleInput() {
const confirmText = rstrip($(this).val());

View file

@ -12,6 +12,7 @@ export default class CreateItemDropdown {
this.fieldName = options.fieldName;
this.onSelect = options.onSelect || (() => {});
this.getDataOption = options.getData;
this.getDataRemote = !!options.filterRemote;
this.createNewItemFromValueOption = options.createNewItemFromValue;
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
@ -29,7 +30,7 @@ export default class CreateItemDropdown {
this.$dropdown.glDropdown({
data: this.getData.bind(this),
filterable: true,
remote: false,
filterRemote: this.getDataRemote,
search: {
fields: ['text'],
},

View file

@ -30,6 +30,7 @@ export default {
:render-header="false"
:render-diff-file="false"
:always-expanded="true"
:discussions-by-diff-order="true"
/>
</ul>
</div>

View file

@ -50,7 +50,7 @@ export default {
};
},
computed: {
...mapGetters('diffs', ['diffHasExpandedDiscussions']),
...mapGetters('diffs', ['diffHasExpandedDiscussions', 'diffHasDiscussions']),
hasExpandedDiscussions() {
return this.diffHasExpandedDiscussions(this.diffFile);
},
@ -108,6 +108,9 @@ export default {
false,
);
},
gfmCopyText() {
return `\`${this.diffFile.filePath}\``;
},
},
methods: {
...mapActions('diffs', ['toggleFileDiscussions']),
@ -191,6 +194,7 @@ export default {
<clipboard-button
:title="__('Copy file path to clipboard')"
:text="diffFile.filePath"
:gfm="gfmCopyText"
css-class="btn-default btn-transparent btn-clipboard"
/>
@ -217,6 +221,7 @@ export default {
v-if="diffFile.blob && diffFile.blob.readableText"
>
<button
:disabled="!diffHasDiscussions(diffFile)"
:class="{ active: hasExpandedDiscussions }"
:title="s__('MergeRequests|Toggle comments for this file')"
class="js-btn-vue-toggle-comments btn"

View file

@ -71,13 +71,23 @@ export default {
required: false,
default: false,
},
isHover: {
type: Boolean,
required: false,
default: false,
},
discussions: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
...mapState({
diffViewType: state => state.diffs.diffViewType,
diffFiles: state => state.diffs.diffFiles,
}),
...mapGetters(['isLoggedIn', 'discussionsByLineCode']),
...mapGetters(['isLoggedIn']),
lineHref() {
return this.lineCode ? `#${this.lineCode}` : '#';
},
@ -85,26 +95,22 @@ export default {
return (
this.isLoggedIn &&
this.showCommentButton &&
this.isHover &&
!this.isMatchLine &&
!this.isContextLine &&
!this.hasDiscussions &&
!this.isMetaLine
!this.isMetaLine &&
!this.hasDiscussions
);
},
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 false;
}
return render;
return this.showCommentButton && this.hasDiscussions;
},
},
methods: {
@ -176,7 +182,7 @@ export default {
v-else
>
<button
v-show="shouldShowCommentButton"
v-if="shouldShowCommentButton"
type="button"
class="add-diff-note js-add-diff-note-button"
title="Add a comment to this line"
@ -189,7 +195,6 @@ export default {
</button>
<a
v-if="lineNumber"
v-once
:data-linenumber="lineNumber"
:href="lineHref"
>

View file

@ -1,17 +1,17 @@
<script>
import $ from 'jquery';
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';
import Autosave from '../../autosave';
import { DIFF_NOTE_TYPE, NOTE_TYPE } from '../constants';
import autosave from '../../notes/mixins/autosave';
import { DIFF_NOTE_TYPE } from '../constants';
export default {
components: {
noteForm,
},
mixins: [autosave],
props: {
diffFileHash: {
type: String,
@ -41,28 +41,35 @@ export default {
},
mounted() {
if (this.isLoggedIn) {
const noteableData = this.getNoteableData;
const keys = [
NOTE_TYPE,
this.noteableType,
noteableData.id,
noteableData.diff_head_sha,
this.noteableData.diff_head_sha,
DIFF_NOTE_TYPE,
noteableData.source_project_id,
this.noteableData.source_project_id,
this.line.lineCode,
];
this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), keys);
this.initAutoSave(this.noteableData, keys);
}
},
methods: {
...mapActions('diffs', ['cancelCommentForm']),
...mapActions(['saveNote', 'refetchDiscussionById']),
handleCancelCommentForm() {
this.autosave.reset();
handleCancelCommentForm(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
const msg = s__('Notes|Are you sure you want to cancel creating this comment?');
// eslint-disable-next-line no-alert
if (!window.confirm(msg)) {
return;
}
}
this.cancelCommentForm({
lineCode: this.line.lineCode,
});
this.$nextTick(() => {
this.resetAutoSave();
});
},
handleSaveNote(note) {
const selectedDiffFile = this.getDiffFileByHash(this.diffFileHash);

View file

@ -67,6 +67,11 @@ export default {
required: false,
default: false,
},
discussions: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
...mapGetters(['isLoggedIn']),
@ -132,10 +137,12 @@ export default {
:line-number="lineNumber"
:meta-data="normalizedLine.metaData"
:show-comment-button="showCommentButton"
:is-hover="isHover"
:is-bottom="isBottom"
:is-match-line="isMatchLine"
:is-context-line="isContentLine"
:is-meta-line="isMetaLine"
:discussions="discussions"
/>
</td>
</template>

View file

@ -1,5 +1,5 @@
<script>
import { mapState, mapGetters } from 'vuex';
import { mapState } from 'vuex';
import diffDiscussions from './diff_discussions.vue';
import diffLineNoteForm from './diff_line_note_form.vue';
@ -21,15 +21,16 @@ export default {
type: Number,
required: true,
},
discussions: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
...mapGetters(['discussionsByLineCode']),
discussions() {
return this.discussionsByLineCode[this.line.lineCode] || [];
},
className() {
return this.discussions.length ? '' : 'js-temp-notes-holder';
},

View file

@ -33,6 +33,11 @@ export default {
required: false,
default: false,
},
discussions: {
type: Array,
required: false,
default: () => [],
},
},
data() {
return {
@ -89,6 +94,7 @@ export default {
:is-bottom="isBottom"
:is-hover="isHover"
:show-comment-button="true"
:discussions="discussions"
class="diff-line-num old_line"
/>
<diff-table-cell
@ -98,10 +104,10 @@ export default {
:line-type="newLineType"
:is-bottom="isBottom"
:is-hover="isHover"
:discussions="discussions"
class="diff-line-num new_line"
/>
<td
v-once
:class="line.type"
class="line_content"
v-html="line.richText"

View file

@ -20,8 +20,11 @@ export default {
},
},
computed: {
...mapGetters('diffs', ['commitId']),
...mapGetters(['discussionsByLineCode']),
...mapGetters('diffs', [
'commitId',
'shouldRenderInlineCommentRow',
'singleDiscussionByLineCode',
]),
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
@ -36,15 +39,8 @@ export default {
},
},
methods: {
shouldRenderCommentRow(line) {
if (this.diffLineCommentForms[line.lineCode]) return true;
const lineDiscussions = this.discussionsByLineCode[line.lineCode];
if (lineDiscussions === undefined) {
return false;
}
return lineDiscussions.every(discussion => discussion.expanded);
discussionsList(line) {
return line.lineCode !== undefined ? this.singleDiscussionByLineCode(line.lineCode) : [];
},
},
};
@ -65,13 +61,15 @@ export default {
:line="line"
:is-bottom="index + 1 === diffLinesLength"
:key="line.lineCode"
:discussions="discussionsList(line)"
/>
<inline-diff-comment-row
v-if="shouldRenderCommentRow(line)"
v-if="shouldRenderInlineCommentRow(line)"
:diff-file-hash="diffFile.fileHash"
:line="line"
:line-index="index"
:key="index"
:discussions="discussionsList(line)"
/>
</template>
</tbody>

View file

@ -1,5 +1,5 @@
<script>
import { mapState, mapGetters } from 'vuex';
import { mapState } from 'vuex';
import diffDiscussions from './diff_discussions.vue';
import diffLineNoteForm from './diff_line_note_form.vue';
@ -21,30 +21,34 @@ export default {
type: Number,
required: true,
},
leftDiscussions: {
type: Array,
required: false,
default: () => [],
},
rightDiscussions: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
...mapGetters(['discussionsByLineCode']),
leftLineCode() {
return this.line.left.lineCode;
},
rightLineCode() {
return this.line.right.lineCode;
},
hasDiscussion() {
const discussions = this.discussionsByLineCode;
return discussions[this.leftLineCode] || discussions[this.rightLineCode];
},
hasExpandedDiscussionOnLeft() {
const discussions = this.discussionsByLineCode[this.leftLineCode];
const discussions = this.leftDiscussions;
return discussions ? discussions.every(discussion => discussion.expanded) : false;
},
hasExpandedDiscussionOnRight() {
const discussions = this.discussionsByLineCode[this.rightLineCode];
const discussions = this.rightDiscussions;
return discussions ? discussions.every(discussion => discussion.expanded) : false;
},
@ -52,17 +56,18 @@ export default {
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
},
shouldRenderDiscussionsOnLeft() {
return this.discussionsByLineCode[this.leftLineCode] && this.hasExpandedDiscussionOnLeft;
return this.leftDiscussions && this.hasExpandedDiscussionOnLeft;
},
shouldRenderDiscussionsOnRight() {
return (
this.discussionsByLineCode[this.rightLineCode] &&
this.hasExpandedDiscussionOnRight &&
this.line.right.type
);
return this.rightDiscussions && this.hasExpandedDiscussionOnRight && this.line.right.type;
},
showRightSideCommentForm() {
return this.line.right.type && this.diffLineCommentForms[this.rightLineCode];
},
className() {
return this.hasDiscussion ? '' : 'js-temp-notes-holder';
return this.leftDiscussions.length > 0 || this.rightDiscussions.length > 0
? ''
: 'js-temp-notes-holder';
},
},
};
@ -80,13 +85,12 @@ export default {
class="content"
>
<diff-discussions
v-if="discussionsByLineCode[leftLineCode].length"
:discussions="discussionsByLineCode[leftLineCode]"
v-if="leftDiscussions.length"
:discussions="leftDiscussions"
/>
</div>
<diff-line-note-form
v-if="diffLineCommentForms[leftLineCode] &&
diffLineCommentForms[leftLineCode]"
v-if="diffLineCommentForms[leftLineCode]"
:diff-file-hash="diffFileHash"
:line="line.left"
:note-target-line="line.left"
@ -100,13 +104,12 @@ export default {
class="content"
>
<diff-discussions
v-if="discussionsByLineCode[rightLineCode].length"
:discussions="discussionsByLineCode[rightLineCode]"
v-if="rightDiscussions.length"
:discussions="rightDiscussions"
/>
</div>
<diff-line-note-form
v-if="diffLineCommentForms[rightLineCode] &&
diffLineCommentForms[rightLineCode] && line.right.type"
v-if="showRightSideCommentForm"
:diff-file-hash="diffFileHash"
:line="line.right"
:note-target-line="line.right"

View file

@ -36,6 +36,16 @@ export default {
required: false,
default: false,
},
leftDiscussions: {
type: Array,
required: false,
default: () => [],
},
rightDiscussions: {
type: Array,
required: false,
default: () => [],
},
},
data() {
return {
@ -116,10 +126,10 @@ export default {
:is-hover="isLeftHover"
:show-comment-button="true"
:diff-view-type="parallelDiffViewType"
:discussions="leftDiscussions"
class="diff-line-num old_line"
/>
<td
v-once
:id="line.left.lineCode"
:class="parallelViewLeftLineType"
class="line_content parallel left-side"
@ -137,10 +147,10 @@ export default {
:is-hover="isRightHover"
:show-comment-button="true"
:diff-view-type="parallelDiffViewType"
:discussions="rightDiscussions"
class="diff-line-num new_line"
/>
<td
v-once
:id="line.right.lineCode"
:class="line.right.type"
class="line_content parallel right-side"

View file

@ -21,8 +21,11 @@ export default {
},
},
computed: {
...mapGetters('diffs', ['commitId']),
...mapGetters(['discussionsByLineCode']),
...mapGetters('diffs', [
'commitId',
'singleDiscussionByLineCode',
'shouldRenderParallelCommentRow',
]),
...mapState({
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
@ -53,29 +56,9 @@ export default {
},
},
methods: {
shouldRenderCommentRow(line) {
const leftLineCode = line.left.lineCode;
const rightLineCode = line.right.lineCode;
const discussions = this.discussionsByLineCode;
const leftDiscussions = discussions[leftLineCode];
const rightDiscussions = discussions[rightLineCode];
const hasDiscussion = leftDiscussions || rightDiscussions;
const hasExpandedDiscussionOnLeft = leftDiscussions
? leftDiscussions.every(discussion => discussion.expanded)
: false;
const hasExpandedDiscussionOnRight = rightDiscussions
? rightDiscussions.every(discussion => discussion.expanded)
: false;
if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
return true;
}
const hasCommentFormOnLeft = this.diffLineCommentForms[leftLineCode];
const hasCommentFormOnRight = this.diffLineCommentForms[rightLineCode];
return hasCommentFormOnLeft || hasCommentFormOnRight;
discussionsByLine(line, leftOrRight) {
return line[leftOrRight] && line[leftOrRight].lineCode !== undefined ?
this.singleDiscussionByLineCode(line[leftOrRight].lineCode) : [];
},
},
};
@ -98,13 +81,17 @@ export default {
:line="line"
:is-bottom="index + 1 === diffLinesLength"
:key="index"
:left-discussions="discussionsByLine(line, 'left')"
:right-discussions="discussionsByLine(line, 'right')"
/>
<parallel-diff-comment-row
v-if="shouldRenderCommentRow(line)"
v-if="shouldRenderParallelCommentRow(line)"
:key="`dcr-${index}`"
:line="line"
:diff-file-hash="diffFile.fileHash"
:line-index="index"
:left-discussions="discussionsByLine(line, 'left')"
:right-discussions="discussionsByLine(line, 'right')"
/>
</template>
</tbody>

View file

@ -45,6 +45,14 @@ export const diffHasExpandedDiscussions = (state, getters) => diff => {
);
};
/**
* Checks if the diff has any discussion
* @param {Boolean} diff
* @returns {Boolean}
*/
export const diffHasDiscussions = (state, getters) => diff =>
getters.getDiffFileDiscussions(diff).length > 0;
/**
* Returns an array with the discussions of the given diff
* @param {Object} diff
@ -56,6 +64,47 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) =
discussion.diff_discussion && _.isEqual(discussion.diff_file.file_hash, diff.fileHash),
) || [];
export const singleDiscussionByLineCode = (state, getters, rootState, rootGetters) => lineCode => {
if (!lineCode || lineCode === undefined) return [];
const discussions = rootGetters.discussionsByLineCode;
return discussions[lineCode] || [];
};
export const shouldRenderParallelCommentRow = (state, getters) => line => {
const leftLineCode = line.left.lineCode;
const rightLineCode = line.right.lineCode;
const leftDiscussions = getters.singleDiscussionByLineCode(leftLineCode);
const rightDiscussions = getters.singleDiscussionByLineCode(rightLineCode);
const hasDiscussion = leftDiscussions.length || rightDiscussions.length;
const hasExpandedDiscussionOnLeft = leftDiscussions.length
? leftDiscussions.every(discussion => discussion.expanded)
: false;
const hasExpandedDiscussionOnRight = rightDiscussions.length
? rightDiscussions.every(discussion => discussion.expanded)
: false;
if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
return true;
}
const hasCommentFormOnLeft = state.diffLineCommentForms[leftLineCode];
const hasCommentFormOnRight = state.diffLineCommentForms[rightLineCode];
return hasCommentFormOnLeft || hasCommentFormOnRight;
};
export const shouldRenderInlineCommentRow = (state, getters) => line => {
if (state.diffLineCommentForms[line.lineCode]) return true;
const lineDiscussions = getters.singleDiscussionByLineCode(line.lineCode);
if (lineDiscussions.length === 0) {
return false;
}
return lineDiscussions.every(discussion => discussion.expanded);
};
// prevent babel-plugin-rewire from generating an invalid default during karma∂ tests
export const getDiffFileByHash = state => fileHash =>
state.diffFiles.find(file => file.fileHash === fileHash);

View file

@ -173,3 +173,24 @@ export function trimFirstCharOfLineContent(line = {}) {
return parsedLine;
}
export function getDiffRefsByLineCode(diffFiles) {
return diffFiles.reduce((acc, diffFile) => {
const { baseSha, headSha, startSha } = diffFile.diffRefs;
const { newPath, oldPath } = diffFile;
// We can only use highlightedDiffLines to create the map of diff lines because
// highlightedDiffLines will also include every parallel diff line in it.
if (diffFile.highlightedDiffLines) {
diffFile.highlightedDiffLines.forEach(line => {
const { lineCode, oldLine, newLine } = line;
if (lineCode) {
acc[lineCode] = { baseSha, headSha, startSha, newPath, oldPath, oldLine, newLine };
}
});
}
return acc;
}, {});
}

View file

@ -171,6 +171,8 @@ export default class DueDateSelectors {
initMilestoneDatePicker() {
$('.datepicker').each(function initPikadayMilestone() {
const $datePicker = $(this);
const datePickerVal = $datePicker.val();
const calendar = new Pikaday({
field: $datePicker.get(0),
theme: 'gitlab-theme animate-picker',
@ -183,7 +185,7 @@ export default class DueDateSelectors {
},
});
calendar.setDate(parsePikadayDate($datePicker.val()));
calendar.setDate(parsePikadayDate(datePickerVal));
$datePicker.data('pikaday', calendar);
});

View file

@ -1,50 +1,50 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
export default {
directives: {
tooltip,
},
components: {
loadingIcon,
Icon,
},
props: {
actions: {
type: Array,
required: false,
default: () => [],
},
components: {
loadingIcon,
Icon,
},
data() {
return {
isLoading: false,
};
},
computed: {
title() {
return 'Deploy to...';
},
props: {
actions: {
type: Array,
required: false,
default: () => [],
},
},
data() {
return {
isLoading: false,
};
},
computed: {
title() {
return 'Deploy to...';
},
},
methods: {
onClickAction(endpoint) {
this.isLoading = true;
},
methods: {
onClickAction(endpoint) {
this.isLoading = true;
eventHub.$emit('postAction', endpoint);
},
isActionDisabled(action) {
if (action.playable === undefined) {
return false;
}
return !action.playable;
},
eventHub.$emit('postAction', { endpoint });
},
};
isActionDisabled(action) {
if (action.playable === undefined) {
return false;
}
return !action.playable;
},
},
};
</script>
<template>
<div
@ -61,10 +61,7 @@
data-toggle="dropdown"
>
<span>
<icon
:size="12"
name="play"
/>
<icon name="play" />
<i
class="fa fa-caret-down"
aria-hidden="true"
@ -85,10 +82,6 @@
class="js-manual-action-link no-btn btn"
@click="onClickAction(action.play_path)"
>
<icon
:size="12"
name="play"
/>
<span>
{{ action.name }}
</span>

View file

@ -1,30 +1,30 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale';
/**
* Renders the external url link in environments table.
*/
export default {
components: {
Icon,
/**
* Renders the external url link in environments table.
*/
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
externalUrl: {
type: String,
required: true,
},
directives: {
tooltip,
},
computed: {
title() {
return s__('Environments|Open live environment');
},
props: {
externalUrl: {
type: String,
required: true,
},
},
computed: {
title() {
return s__('Environments|Open');
},
},
};
},
};
</script>
<template>
<a
@ -37,9 +37,6 @@
target="_blank"
rel="noopener noreferrer nofollow"
>
<icon
:size="12"
name="external-link"
/>
<icon name="external-link" />
</a>
</template>

View file

@ -1,429 +1,461 @@
<script>
import Timeago from 'timeago.js';
import _ from 'underscore';
import tooltip from '~/vue_shared/directives/tooltip';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { humanize } from '~/lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue';
import RollbackComponent from './environment_rollback.vue';
import TerminalButtonComponent from './environment_terminal_button.vue';
import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub';
import Timeago from 'timeago.js';
import _ from 'underscore';
import tooltip from '~/vue_shared/directives/tooltip';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { humanize } from '~/lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue';
import RollbackComponent from './environment_rollback.vue';
import TerminalButtonComponent from './environment_terminal_button.vue';
import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit.vue';
import eventHub from '../event_hub';
/**
* Envrionment Item Component
*
* Renders a table row for each environment.
*/
const timeagoInstance = new Timeago();
/**
* Envrionment Item Component
*
* Renders a table row for each environment.
*/
const timeagoInstance = new Timeago();
export default {
components: {
UserAvatarLink,
CommitComponent,
ActionsComponent,
ExternalUrlComponent,
StopComponent,
RollbackComponent,
TerminalButtonComponent,
MonitoringButtonComponent,
export default {
components: {
UserAvatarLink,
CommitComponent,
ActionsComponent,
ExternalUrlComponent,
StopComponent,
RollbackComponent,
TerminalButtonComponent,
MonitoringButtonComponent,
},
directives: {
tooltip,
},
props: {
model: {
type: Object,
required: true,
default: () => ({}),
},
directives: {
tooltip,
canCreateDeployment: {
type: Boolean,
required: false,
default: false,
},
props: {
model: {
type: Object,
required: true,
default: () => ({}),
},
canReadEnvironment: {
type: Boolean,
required: false,
default: false,
},
},
canCreateDeployment: {
type: Boolean,
required: false,
default: false,
},
canReadEnvironment: {
type: Boolean,
required: false,
default: false,
},
computed: {
/**
* Verifies if `last_deployment` key exists in the current Envrionment.
* This key is required to render most of the html - this method works has
* an helper.
*
* @returns {Boolean}
*/
hasLastDeploymentKey() {
if (this.model && this.model.last_deployment && !_.isEmpty(this.model.last_deployment)) {
return true;
}
return false;
},
computed: {
/**
* Verifies if `last_deployment` key exists in the current Envrionment.
* This key is required to render most of the html - this method works has
* an helper.
*
* @returns {Boolean}
*/
hasLastDeploymentKey() {
if (this.model &&
this.model.last_deployment &&
!_.isEmpty(this.model.last_deployment)) {
return true;
}
return false;
},
/**
* Verifies is the given environment has manual actions.
* Used to verify if we should render them or nor.
*
* @returns {Boolean|Undefined}
*/
hasManualActions() {
return this.model &&
this.model.last_deployment &&
this.model.last_deployment.manual_actions &&
this.model.last_deployment.manual_actions.length > 0;
},
/**
* Returns the value of the `stop_action?` key provided in the response.
*
* @returns {Boolean}
*/
hasStopAction() {
return this.model && this.model['stop_action?'];
},
/**
* Verifies if the `deployable` key is present in `last_deployment` key.
* Used to verify whether we should or not render the rollback partial.
*
* @returns {Boolean|Undefined}
*/
canRetry() {
return this.model &&
this.hasLastDeploymentKey &&
this.model.last_deployment &&
this.model.last_deployment.deployable;
},
/**
* Verifies if the date to be shown is present.
*
* @returns {Boolean|Undefined}
*/
canShowDate() {
return this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable !== undefined;
},
/**
* Human readable date.
*
* @returns {String}
*/
createdDate() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.created_at) {
return timeagoInstance.format(this.model.last_deployment.deployable.created_at);
}
return '';
},
/**
* Returns the manual actions with the name parsed.
*
* @returns {Array.<Object>|Undefined}
*/
manualActions() {
if (this.hasManualActions) {
return this.model.last_deployment.manual_actions.map((action) => {
const parsedAction = {
name: humanize(action.name),
play_path: action.play_path,
playable: action.playable,
};
return parsedAction;
});
}
return [];
},
/**
* Builds the string used in the user image alt attribute.
*
* @returns {String}
*/
userImageAltDescription() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.user &&
this.model.last_deployment.user.username) {
return `${this.model.last_deployment.user.username}'s avatar'`;
}
return '';
},
/**
* If provided, returns the commit tag.
*
* @returns {String|Undefined}
*/
commitTag() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.tag) {
return this.model.last_deployment.tag;
}
return undefined;
},
/**
* If provided, returns the commit ref.
*
* @returns {Object|Undefined}
*/
commitRef() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.ref) {
return this.model.last_deployment.ref;
}
return undefined;
},
/**
* If provided, returns the commit url.
*
* @returns {String|Undefined}
*/
commitUrl() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.commit_path) {
return this.model.last_deployment.commit.commit_path;
}
return undefined;
},
/**
* If provided, returns the commit short sha.
*
* @returns {String|Undefined}
*/
commitShortSha() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.short_id) {
return this.model.last_deployment.commit.short_id;
}
return undefined;
},
/**
* If provided, returns the commit title.
*
* @returns {String|Undefined}
*/
commitTitle() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.title) {
return this.model.last_deployment.commit.title;
}
return undefined;
},
/**
* If provided, returns the commit tag.
*
* @returns {Object|Undefined}
*/
commitAuthor() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.author) {
return this.model.last_deployment.commit.author;
}
return undefined;
},
/**
* Verifies if the `retry_path` key is present and returns its value.
*
* @returns {String|Undefined}
*/
retryUrl() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.retry_path) {
return this.model.last_deployment.deployable.retry_path;
}
return undefined;
},
/**
* Verifies if the `last?` key is present and returns its value.
*
* @returns {Boolean|Undefined}
*/
isLastDeployment() {
return this.model && this.model.last_deployment &&
this.model.last_deployment['last?'];
},
/**
* Builds the name of the builds needed to display both the name and the id.
*
* @returns {String}
*/
buildName() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable) {
const { deployable } = this.model.last_deployment;
return `${deployable.name} #${deployable.id}`;
}
return '';
},
/**
* Builds the needed string to show the internal id.
*
* @returns {String}
*/
deploymentInternalId() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.iid) {
return `#${this.model.last_deployment.iid}`;
}
return '';
},
/**
* Verifies if the user object is present under last_deployment object.
*
* @returns {Boolean}
*/
deploymentHasUser() {
return this.model &&
!_.isEmpty(this.model.last_deployment) &&
!_.isEmpty(this.model.last_deployment.user);
},
/**
* Returns the user object nested with the last_deployment object.
* Used to render the template.
*
* @returns {Object}
*/
deploymentUser() {
if (this.model &&
!_.isEmpty(this.model.last_deployment) &&
!_.isEmpty(this.model.last_deployment.user)) {
return this.model.last_deployment.user;
}
return {};
},
/**
* Verifies if the build name column should be rendered by verifing
* if all the information needed is present
* and if the environment is not a folder.
*
* @returns {Boolean}
*/
shouldRenderBuildName() {
return !this.model.isFolder &&
!_.isEmpty(this.model.last_deployment) &&
!_.isEmpty(this.model.last_deployment.deployable);
},
/**
* Verifies the presence of all the keys needed to render the buil_path.
*
* @return {String}
*/
buildPath() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.build_path) {
return this.model.last_deployment.deployable.build_path;
}
return '';
},
/**
* Verifies the presence of all the keys needed to render the external_url.
*
* @return {String}
*/
externalURL() {
if (this.model && this.model.external_url) {
return this.model.external_url;
}
return '';
},
/**
* Verifies if deplyment internal ID should be rendered by verifing
* if all the information needed is present
* and if the environment is not a folder.
*
* @returns {Boolean}
*/
shouldRenderDeploymentID() {
return !this.model.isFolder &&
!_.isEmpty(this.model.last_deployment) &&
this.model.last_deployment.iid !== undefined;
},
environmentPath() {
if (this.model && this.model.environment_path) {
return this.model.environment_path;
}
return '';
},
monitoringUrl() {
if (this.model && this.model.metrics_path) {
return this.model.metrics_path;
}
return '';
},
displayEnvironmentActions() {
return this.hasManualActions ||
this.externalURL ||
this.monitoringUrl ||
this.hasStopAction ||
this.canRetry;
},
/**
* Verifies is the given environment has manual actions.
* Used to verify if we should render them or nor.
*
* @returns {Boolean|Undefined}
*/
hasManualActions() {
return (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.manual_actions &&
this.model.last_deployment.manual_actions.length > 0
);
},
methods: {
onClickFolder() {
eventHub.$emit('toggleFolder', this.model);
},
/**
* Checkes whether the environment is protected.
* (`is_protected` currently only set in EE)
*
* @returns {Boolean}
*/
isProtected() {
return this.model && this.model.is_protected;
},
};
/**
* Returns whether the environment can be stopped.
*
* @returns {Boolean}
*/
canStopEnvironment() {
return this.model && this.model.can_stop;
},
/**
* Verifies if the `deployable` key is present in `last_deployment` key.
* Used to verify whether we should or not render the rollback partial.
*
* @returns {Boolean|Undefined}
*/
canRetry() {
return (
this.model &&
this.hasLastDeploymentKey &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.retry_path
);
},
/**
* Verifies if the date to be shown is present.
*
* @returns {Boolean|Undefined}
*/
canShowDate() {
return (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable !== undefined
);
},
/**
* Human readable date.
*
* @returns {String}
*/
createdDate() {
if (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.created_at
) {
return timeagoInstance.format(this.model.last_deployment.deployable.created_at);
}
return '';
},
/**
* Returns the manual actions with the name parsed.
*
* @returns {Array.<Object>|Undefined}
*/
manualActions() {
if (this.hasManualActions) {
return this.model.last_deployment.manual_actions.map(action => {
const parsedAction = {
name: humanize(action.name),
play_path: action.play_path,
playable: action.playable,
};
return parsedAction;
});
}
return [];
},
/**
* Builds the string used in the user image alt attribute.
*
* @returns {String}
*/
userImageAltDescription() {
if (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.user &&
this.model.last_deployment.user.username
) {
return `${this.model.last_deployment.user.username}'s avatar'`;
}
return '';
},
/**
* If provided, returns the commit tag.
*
* @returns {String|Undefined}
*/
commitTag() {
if (this.model && this.model.last_deployment && this.model.last_deployment.tag) {
return this.model.last_deployment.tag;
}
return undefined;
},
/**
* If provided, returns the commit ref.
*
* @returns {Object|Undefined}
*/
commitRef() {
if (this.model && this.model.last_deployment && this.model.last_deployment.ref) {
return this.model.last_deployment.ref;
}
return undefined;
},
/**
* If provided, returns the commit url.
*
* @returns {String|Undefined}
*/
commitUrl() {
if (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.commit_path
) {
return this.model.last_deployment.commit.commit_path;
}
return undefined;
},
/**
* If provided, returns the commit short sha.
*
* @returns {String|Undefined}
*/
commitShortSha() {
if (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.short_id
) {
return this.model.last_deployment.commit.short_id;
}
return undefined;
},
/**
* If provided, returns the commit title.
*
* @returns {String|Undefined}
*/
commitTitle() {
if (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.title
) {
return this.model.last_deployment.commit.title;
}
return undefined;
},
/**
* If provided, returns the commit tag.
*
* @returns {Object|Undefined}
*/
commitAuthor() {
if (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.author
) {
return this.model.last_deployment.commit.author;
}
return undefined;
},
/**
* Verifies if the `retry_path` key is present and returns its value.
*
* @returns {String|Undefined}
*/
retryUrl() {
if (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.retry_path
) {
return this.model.last_deployment.deployable.retry_path;
}
return undefined;
},
/**
* Verifies if the `last?` key is present and returns its value.
*
* @returns {Boolean|Undefined}
*/
isLastDeployment() {
return this.model && this.model.last_deployment && this.model.last_deployment['last?'];
},
/**
* Builds the name of the builds needed to display both the name and the id.
*
* @returns {String}
*/
buildName() {
if (this.model && this.model.last_deployment && this.model.last_deployment.deployable) {
const { deployable } = this.model.last_deployment;
return `${deployable.name} #${deployable.id}`;
}
return '';
},
/**
* Builds the needed string to show the internal id.
*
* @returns {String}
*/
deploymentInternalId() {
if (this.model && this.model.last_deployment && this.model.last_deployment.iid) {
return `#${this.model.last_deployment.iid}`;
}
return '';
},
/**
* Verifies if the user object is present under last_deployment object.
*
* @returns {Boolean}
*/
deploymentHasUser() {
return (
this.model &&
!_.isEmpty(this.model.last_deployment) &&
!_.isEmpty(this.model.last_deployment.user)
);
},
/**
* Returns the user object nested with the last_deployment object.
* Used to render the template.
*
* @returns {Object}
*/
deploymentUser() {
if (
this.model &&
!_.isEmpty(this.model.last_deployment) &&
!_.isEmpty(this.model.last_deployment.user)
) {
return this.model.last_deployment.user;
}
return {};
},
/**
* Verifies if the build name column should be rendered by verifing
* if all the information needed is present
* and if the environment is not a folder.
*
* @returns {Boolean}
*/
shouldRenderBuildName() {
return (
!this.model.isFolder &&
!_.isEmpty(this.model.last_deployment) &&
!_.isEmpty(this.model.last_deployment.deployable)
);
},
/**
* Verifies the presence of all the keys needed to render the buil_path.
*
* @return {String}
*/
buildPath() {
if (
this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.build_path
) {
return this.model.last_deployment.deployable.build_path;
}
return '';
},
/**
* Verifies the presence of all the keys needed to render the external_url.
*
* @return {String}
*/
externalURL() {
if (this.model && this.model.external_url) {
return this.model.external_url;
}
return '';
},
/**
* Verifies if deplyment internal ID should be rendered by verifing
* if all the information needed is present
* and if the environment is not a folder.
*
* @returns {Boolean}
*/
shouldRenderDeploymentID() {
return (
!this.model.isFolder &&
!_.isEmpty(this.model.last_deployment) &&
this.model.last_deployment.iid !== undefined
);
},
environmentPath() {
if (this.model && this.model.environment_path) {
return this.model.environment_path;
}
return '';
},
monitoringUrl() {
if (this.model && this.model.metrics_path) {
return this.model.metrics_path;
}
return '';
},
displayEnvironmentActions() {
return (
this.hasManualActions ||
this.externalURL ||
this.monitoringUrl ||
this.canStopEnvironment ||
this.canRetry
);
},
},
methods: {
onClickFolder() {
eventHub.$emit('toggleFolder', this.model);
},
},
};
</script>
<template>
<div
@ -434,7 +466,7 @@
class="gl-responsive-table-row"
role="row">
<div
class="table-section section-10"
class="table-section section-wrap section-15"
role="gridcell"
>
<div
@ -444,16 +476,17 @@
>
{{ s__("Environments|Environment") }}
</div>
<a
<span
v-if="!model.isFolder"
:href="environmentPath"
class="environment-name flex-truncate-parent table-mobile-content">
<span
class="environment-name table-mobile-content">
<a
v-tooltip
:href="environmentPath"
:title="model.name"
class="flex-truncate-child"
>{{ model.name }}</span>
</a>
>
{{ model.name }}
</a>
</span>
<span
v-else
class="folder-name"
@ -527,7 +560,7 @@
<div
v-if="!model.isFolder"
class="table-section section-25"
class="table-section section-20"
role="gridcell"
>
<div
@ -580,11 +613,6 @@
class="btn-group table-action-buttons"
role="group">
<actions-component
v-if="hasManualActions && canCreateDeployment"
:actions="manualActions"
/>
<external-url-component
v-if="externalURL && canReadEnvironment"
:external-url="externalURL"
@ -595,21 +623,26 @@
:monitoring-url="monitoringUrl"
/>
<actions-component
v-if="hasManualActions && canCreateDeployment"
:actions="manualActions"
/>
<terminal-button-component
v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"
/>
<stop-component
v-if="hasStopAction && canCreateDeployment"
:stop-url="model.stop_path"
/>
<rollback-component
v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl"
/>
<stop-component
v-if="canStopEnvironment"
:environment="model"
/>
</div>
</div>
</div>

View file

@ -1,29 +1,29 @@
<script>
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
monitoringUrl: {
type: String,
required: true,
},
directives: {
tooltip,
},
computed: {
title() {
return 'Monitoring';
},
props: {
monitoringUrl: {
type: String,
required: true,
},
},
computed: {
title() {
return 'Monitoring';
},
},
};
},
};
</script>
<template>
<a
@ -35,9 +35,6 @@
data-container="body"
rel="noopener noreferrer nofollow"
>
<icon
:size="12"
name="chart"
/>
<icon name="chart" />
</a>
</template>

View file

@ -1,56 +1,74 @@
<script>
/**
* Renders Rollback or Re deploy button in environments table depending
* of the provided property `isLastDeployment`.
*
* Makes a post request when the button is clicked.
*/
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
/**
* Renders Rollback or Re deploy button in environments table depending
* of the provided property `isLastDeployment`.
*
* Makes a post request when the button is clicked.
*/
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../event_hub';
import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
components: {
loadingIcon,
},
props: {
retryUrl: {
type: String,
default: '',
},
export default {
components: {
Icon,
LoadingIcon,
},
isLastDeployment: {
type: Boolean,
default: true,
},
},
data() {
return {
isLoading: false,
};
},
methods: {
onClick() {
this.isLoading = true;
directives: {
tooltip,
},
eventHub.$emit('postAction', this.retryUrl);
},
props: {
retryUrl: {
type: String,
default: '',
},
};
isLastDeployment: {
type: Boolean,
default: true,
},
},
data() {
return {
isLoading: false,
};
},
computed: {
title() {
return this.isLastDeployment ? s__('Environments|Re-deploy to environment') : s__('Environments|Rollback environment');
},
},
methods: {
onClick() {
this.isLoading = true;
eventHub.$emit('postAction', { endpoint: this.retryUrl });
},
},
};
</script>
<template>
<button
v-tooltip
:disabled="isLoading"
:title="title"
type="button"
class="btn d-none d-sm-none d-md-block"
@click="onClick"
>
<span v-if="isLastDeployment">
{{ s__("Environments|Re-deploy") }}
</span>
<span v-else>
{{ s__("Environments|Rollback") }}
</span>
<icon
v-if="isLastDeployment"
name="repeat" />
<icon
v-else
name="redo"/>
<loading-icon v-if="isLoading" />
</button>

View file

@ -1,72 +1,78 @@
<script>
/**
* Renders the stop "button" that allows stop an environment.
* Used in environments table.
*/
/**
* Renders the stop "button" that allows stop an environment.
* Used in environments table.
*/
import $ from 'jquery';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import $ from 'jquery';
import Icon from '~/vue_shared/components/icon.vue';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
loadingIcon,
export default {
components: {
Icon,
LoadingButton,
},
directives: {
tooltip,
},
props: {
environment: {
type: Object,
required: true,
},
},
directives: {
tooltip,
data() {
return {
isLoading: false,
};
},
computed: {
title() {
return s__('Environments|Stop environment');
},
},
props: {
stopUrl: {
type: String,
default: '',
},
mounted() {
eventHub.$on('stopEnvironment', this.onStopEnvironment);
},
beforeDestroy() {
eventHub.$off('stopEnvironment', this.onStopEnvironment);
},
methods: {
onClick() {
$(this.$el).tooltip('dispose');
eventHub.$emit('requestStopEnvironment', this.environment);
},
data() {
return {
isLoading: false,
};
onStopEnvironment(environment) {
if (this.environment.id === environment.id) {
this.isLoading = true;
}
},
computed: {
title() {
return 'Stop';
},
},
methods: {
onClick() {
// eslint-disable-next-line no-alert
if (window.confirm('Are you sure you want to stop this environment?')) {
this.isLoading = true;
$(this.$el).tooltip('dispose');
eventHub.$emit('postAction', this.stopUrl);
}
},
},
};
},
};
</script>
<template>
<button
<loading-button
v-tooltip
:disabled="isLoading"
:loading="isLoading"
:title="title"
:aria-label="title"
type="button"
class="btn stop-env-link d-none d-sm-none d-md-block"
container-class="btn btn-danger d-none d-sm-none d-md-block"
data-container="body"
data-toggle="modal"
data-target="#stop-environment-modal"
@click="onClick"
>
<i
class="fa fa-stop stop-env-icon"
aria-hidden="true"
>
</i>
<loading-icon v-if="isLoading" />
</button>
<icon name="stop"/>
</loading-button>
</template>

View file

@ -1,31 +1,31 @@
<script>
/**
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
/**
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
components: {
Icon,
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
terminalPath: {
type: String,
required: false,
default: '',
},
directives: {
tooltip,
},
computed: {
title() {
return 'Terminal';
},
props: {
terminalPath: {
type: String,
required: false,
default: '',
},
},
computed: {
title() {
return 'Terminal';
},
},
};
},
};
</script>
<template>
<a
@ -36,9 +36,6 @@
class="btn terminal-button d-none d-sm-none d-md-block"
data-container="body"
>
<icon
:size="12"
name="terminal"
/>
<icon name="terminal" />
</a>
</template>

View file

@ -5,10 +5,12 @@
import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from './stop_environment_modal.vue';
export default {
components: {
emptyState,
StopEnvironmentModal,
},
mixins: [
@ -90,6 +92,8 @@
</script>
<template>
<div :class="cssContainerClass">
<stop-environment-modal :environment="environmentInStopModal" />
<div class="top-area">
<tabs
:tabs="tabs"

View file

@ -15,7 +15,7 @@ export default {
environments: {
type: Array,
required: true,
default: () => ([]),
default: () => [],
},
canReadEnvironment: {
@ -35,10 +35,7 @@ export default {
return `${window.location.pathname}/folders/${model.folderName}`;
},
shouldRenderFolderContent(env) {
return env.isFolder &&
env.isOpen &&
env.children &&
env.children.length > 0;
return env.isFolder && env.isOpen && env.children && env.children.length > 0;
},
},
};
@ -53,7 +50,7 @@ export default {
role="row"
>
<div
class="table-section section-10 environments-name"
class="table-section section-15 environments-name"
role="columnheader"
>
{{ s__("Environments|Environment") }}
@ -71,7 +68,7 @@ export default {
{{ s__("Environments|Job") }}
</div>
<div
class="table-section section-25 environments-commit"
class="table-section section-20 environments-commit"
role="columnheader"
>
{{ s__("Environments|Commit") }}
@ -91,7 +88,7 @@ export default {
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:key="i"
:key="`environment-item-${i}`"
/>
<template

View file

@ -0,0 +1,92 @@
<script>
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '../event_hub';
export default {
id: 'stop-environment-modal',
name: 'StopEnvironmentModal',
components: {
GlModal,
LoadingButton,
},
directives: {
tooltip,
},
props: {
environment: {
type: Object,
required: true,
},
},
computed: {
noStopActionMessage() {
return sprintf(
s__(
`Environments|Note that this action will stop the environment,
but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment
due to no stop environment action being defined
in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file.`,
),
{
emphasisStart: '<strong>',
emphasisEnd: '</strong>',
ciConfigLinkStart:
'<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">',
ciConfigLinkEnd: '</a>',
},
false,
);
},
},
methods: {
onSubmit() {
eventHub.$emit('stopEnvironment', this.environment);
},
},
};
</script>
<template>
<gl-modal
:id="$options.id"
:footer-primary-button-text="s__('Environments|Stop environment')"
footer-primary-button-variant="danger"
@submit="onSubmit"
>
<template slot="header">
<h4
class="modal-title d-flex mw-100"
>
Stopping
<span
v-tooltip
:title="environment.name"
class="text-truncate ml-1 mr-1 flex-fill"
>{{ environment.name }}</span>
?
</h4>
</template>
<p>{{ s__('Environments|Are you sure you want to stop this environment?') }}</p>
<div
v-if="!environment.has_stop_action"
class="warning_message"
>
<p v-html="noStopActionMessage"></p>
<a
href="https://docs.gitlab.com/ee/ci/environments.html#stopping-an-environment"
target="_blank"
rel="noopener noreferrer"
>{{ s__('Environments|Learn more about stopping environments') }}</a>
</div>
</gl-modal>
</template>

View file

@ -1,12 +1,18 @@
<script>
import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from '../components/stop_environment_modal.vue';
export default {
components: {
StopEnvironmentModal,
},
mixins: [
environmentsMixin,
CIPaginationMixin,
],
props: {
endpoint: {
type: String,
@ -38,6 +44,8 @@
</script>
<template>
<div :class="cssContainerClass">
<stop-environment-modal :environment="environmentInStopModal" />
<div
v-if="!isLoading"
class="top-area"

View file

@ -40,6 +40,7 @@ export default {
scope: getParameterByName('scope') || 'available',
page: getParameterByName('page') || '1',
requestData: {},
environmentInStopModal: {},
};
},
@ -85,7 +86,7 @@ export default {
Flash(s__('Environments|An error occurred while fetching the environments.'));
},
postAction(endpoint) {
postAction({ endpoint, errorMessage }) {
if (!this.isMakingRequest) {
this.isLoading = true;
@ -93,7 +94,7 @@ export default {
.then(() => this.fetchEnvironments())
.catch(() => {
this.isLoading = false;
Flash(s__('Environments|An error occurred while making the request.'));
Flash(errorMessage || s__('Environments|An error occurred while making the request.'));
});
}
},
@ -106,6 +107,15 @@ export default {
.catch(this.errorCallback);
},
updateStopModal(environment) {
this.environmentInStopModal = environment;
},
stopEnvironment(environment) {
const endpoint = environment.stop_path;
const errorMessage = s__('Environments|An error occurred while stopping the environment, please try again');
this.postAction({ endpoint, errorMessage });
},
},
computed: {
@ -162,9 +172,13 @@ export default {
});
eventHub.$on('postAction', this.postAction);
eventHub.$on('requestStopEnvironment', this.updateStopModal);
eventHub.$on('stopEnvironment', this.stopEnvironment);
},
beforeDestroyed() {
eventHub.$off('postAction');
beforeDestroy() {
eventHub.$off('postAction', this.postAction);
eventHub.$off('requestStopEnvironment', this.updateStopModal);
eventHub.$off('stopEnvironment', this.stopEnvironment);
},
};

View file

@ -13,7 +13,7 @@ export default class EnvironmentsService {
// eslint-disable-next-line class-methods-use-this
postAction(endpoint) {
return axios.post(endpoint, {}, { emulateJSON: true });
return axios.post(endpoint, {});
}
getFolderContent(folderUrl) {

View file

@ -199,7 +199,7 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
tmpl = GfmAutoComplete.Issues.template;
tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title);
}
return tmpl;
},
@ -265,7 +265,7 @@ class GfmAutoComplete {
displayTpl(value) {
let tmpl = GfmAutoComplete.Loading.template;
if (value.title != null) {
tmpl = GfmAutoComplete.Issues.template;
tmpl = GfmAutoComplete.Issues.templateFunction(value.id, value.title);
}
return tmpl;
},
@ -521,8 +521,9 @@ GfmAutoComplete.Labels = {
};
// Issues and MergeRequests
GfmAutoComplete.Issues = {
// eslint-disable-next-line no-template-curly-in-string
template: '<li><small>${id}</small> ${title}</li>',
templateFunction(id, title) {
return `<li><small>${id}</small> ${_.escape(title)}</li>`;
},
};
// Milestones
GfmAutoComplete.Milestones = {

View file

@ -1,4 +1,4 @@
/* 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 */
/* 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, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func */
/* global fuzzaldrinPlus */
import $ from 'jquery';
@ -19,32 +19,42 @@ GitLabDropdownInput = (function() {
this.fieldName = this.options.fieldName || 'field-name';
$inputContainer = this.input.parent();
$clearButton = $inputContainer.find('.js-dropdown-input-clear');
$clearButton.on('click', (function(_this) {
// Clear click
return function(e) {
e.preventDefault();
e.stopPropagation();
return _this.input.val('').trigger('input').focus();
};
})(this));
$clearButton.on(
'click',
(function(_this) {
// Clear click
return function(e) {
e.preventDefault();
e.stopPropagation();
return _this.input
.val('')
.trigger('input')
.focus();
};
})(this),
);
this.input
.on('keydown', function (e) {
var keyCode = e.which;
if (keyCode === 13 && !options.elIsInput) {
e.preventDefault();
}
})
.on('input', function(e) {
var val = e.currentTarget.value || _this.options.inputFieldName;
val = val.split(' ').join('-') // replaces space with dash
.replace(/[^a-zA-Z0-9 -]/g, '').toLowerCase() // replace non alphanumeric
.replace(/(-)\1+/g, '-'); // replace repeated dashes
_this.cb(_this.options.fieldName, val, {}, true);
_this.input.closest('.dropdown')
.find('.dropdown-toggle-text')
.text(val);
});
.on('keydown', function(e) {
var keyCode = e.which;
if (keyCode === 13 && !options.elIsInput) {
e.preventDefault();
}
})
.on('input', function(e) {
var val = e.currentTarget.value || _this.options.inputFieldName;
val = val
.split(' ')
.join('-') // replaces space with dash
.replace(/[^a-zA-Z0-9 -]/g, '')
.toLowerCase() // replace non alphanumeric
.replace(/(-)\1+/g, '-'); // replace repeated dashes
_this.cb(_this.options.fieldName, val, {}, true);
_this.input
.closest('.dropdown')
.find('.dropdown-toggle-text')
.text(val);
});
}
GitLabDropdownInput.prototype.onInput = function(cb) {
@ -61,7 +71,7 @@ GitLabDropdownFilter = (function() {
ARROW_KEY_CODES = [38, 40];
HAS_VALUE_CLASS = "has-value";
HAS_VALUE_CLASS = 'has-value';
function GitLabDropdownFilter(input, options) {
var $clearButton, $inputContainer, ref, timeout;
@ -70,44 +80,59 @@ GitLabDropdownFilter = (function() {
this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
$inputContainer = this.input.parent();
$clearButton = $inputContainer.find('.js-dropdown-input-clear');
$clearButton.on('click', (function(_this) {
// Clear click
return function(e) {
e.preventDefault();
e.stopPropagation();
return _this.input.val('').trigger('input').focus();
};
})(this));
$clearButton.on(
'click',
(function(_this) {
// Clear click
return function(e) {
e.preventDefault();
e.stopPropagation();
return _this.input
.val('')
.trigger('input')
.focus();
};
})(this),
);
// Key events
timeout = "";
timeout = '';
this.input
.on('keydown', function (e) {
.on('keydown', function(e) {
var keyCode = e.which;
if (keyCode === 13 && !options.elIsInput) {
e.preventDefault();
}
})
.on('input', function() {
if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
$inputContainer.addClass(HAS_VALUE_CLASS);
} else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
$inputContainer.removeClass(HAS_VALUE_CLASS);
}
// Only filter asynchronously only if option remote is set
if (this.options.remote) {
clearTimeout(timeout);
return timeout = setTimeout(function() {
$inputContainer.parent().addClass('is-loading');
.on(
'input',
function() {
if (this.input.val() !== '' && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
$inputContainer.addClass(HAS_VALUE_CLASS);
} else if (this.input.val() === '' && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
$inputContainer.removeClass(HAS_VALUE_CLASS);
}
// Only filter asynchronously only if option remote is set
if (this.options.remote) {
clearTimeout(timeout);
return (timeout = setTimeout(
function() {
$inputContainer.parent().addClass('is-loading');
return this.options.query(this.input.val(), function(data) {
$inputContainer.parent().removeClass('is-loading');
return this.options.callback(data);
}.bind(this));
}.bind(this), 250);
} else {
return this.filter(this.input.val());
}
}.bind(this));
return this.options.query(
this.input.val(),
function(data) {
$inputContainer.parent().removeClass('is-loading');
return this.options.callback(data);
}.bind(this),
);
}.bind(this),
250,
));
} else {
return this.filter(this.input.val());
}
}.bind(this),
);
}
GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) {
@ -120,7 +145,7 @@ GitLabDropdownFilter = (function() {
this.options.onFilter(search_text);
}
data = this.options.data();
if ((data != null) && !this.options.filterByText) {
if (data != null && !this.options.filterByText) {
results = data;
if (search_text !== '') {
// When data is an array of objects therefore [object Array] e.g.
@ -130,7 +155,7 @@ GitLabDropdownFilter = (function() {
// ]
if (_.isArray(data)) {
results = fuzzaldrinPlus.filter(data, search_text, {
key: this.options.keys
key: this.options.keys,
});
} else {
// If data is grouped therefore an [object Object]. e.g.
@ -149,7 +174,7 @@ GitLabDropdownFilter = (function() {
for (key in data) {
group = data[key];
tmp = fuzzaldrinPlus.filter(group, search_text, {
key: this.options.keys
key: this.options.keys,
});
if (tmp.length) {
results[key] = tmp.map(function(item) {
@ -180,7 +205,10 @@ GitLabDropdownFilter = (function() {
elements.show().removeClass('option-hidden');
}
elements.parent().find('.dropdown-menu-empty-item').toggleClass('hidden', elements.is(':visible'));
elements
.parent()
.find('.dropdown-menu-empty-item')
.toggleClass('hidden', elements.is(':visible'));
}
};
@ -194,23 +222,26 @@ GitLabDropdownRemote = (function() {
}
GitLabDropdownRemote.prototype.execute = function() {
if (typeof this.dataEndpoint === "string") {
if (typeof this.dataEndpoint === 'string') {
return this.fetchData();
} else if (typeof this.dataEndpoint === "function") {
} else if (typeof this.dataEndpoint === 'function') {
if (this.options.beforeSend) {
this.options.beforeSend();
}
return this.dataEndpoint("", (function(_this) {
// Fetch the data by calling the data funcfion
return function(data) {
if (_this.options.success) {
_this.options.success(data);
}
if (_this.options.beforeSend) {
return _this.options.beforeSend();
}
};
})(this));
return this.dataEndpoint(
'',
(function(_this) {
// Fetch the data by calling the data funcfion
return function(data) {
if (_this.options.success) {
_this.options.success(data);
}
if (_this.options.beforeSend) {
return _this.options.beforeSend();
}
};
})(this),
);
}
};
@ -220,33 +251,41 @@ GitLabDropdownRemote = (function() {
}
// Fetch the data through ajax if the data is a string
return axios.get(this.dataEndpoint)
.then(({ data }) => {
if (this.options.success) {
return this.options.success(data);
}
});
return axios.get(this.dataEndpoint).then(({ data }) => {
if (this.options.success) {
return this.options.success(data);
}
});
};
return GitLabDropdownRemote;
})();
GitLabDropdown = (function() {
var ACTIVE_CLASS, FILTER_INPUT, NO_FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex;
var ACTIVE_CLASS,
FILTER_INPUT,
NO_FILTER_INPUT,
INDETERMINATE_CLASS,
LOADING_CLASS,
PAGE_TWO_CLASS,
NON_SELECTABLE_CLASSES,
SELECTABLE_CLASSES,
CURSOR_SELECT_SCROLL_PADDING,
currentIndex;
LOADING_CLASS = "is-loading";
LOADING_CLASS = 'is-loading';
PAGE_TWO_CLASS = "is-page-two";
PAGE_TWO_CLASS = 'is-page-two';
ACTIVE_CLASS = "is-active";
ACTIVE_CLASS = 'is-active';
INDETERMINATE_CLASS = "is-indeterminate";
INDETERMINATE_CLASS = 'is-indeterminate';
currentIndex = -1;
NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-item';
SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)";
SELECTABLE_CLASSES = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ', .option-hidden)';
CURSOR_SELECT_SCROLL_PADDING = 5;
@ -263,15 +302,15 @@ GitLabDropdown = (function() {
this.opened = this.opened.bind(this);
this.shouldPropagate = this.shouldPropagate.bind(this);
self = this;
selector = $(this.el).data("target");
selector = $(this.el).data('target');
this.dropdown = selector != null ? $(selector) : $(this.el).parent();
// Set Defaults
this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT);
this.highlight = !!this.options.highlight;
this.filterInputBlur = this.options.filterInputBlur != null
? this.options.filterInputBlur
: true;
this.icon = !!this.options.icon;
this.filterInputBlur =
this.options.filterInputBlur != null ? this.options.filterInputBlur : true;
// If no input is passed create a default one
self = this;
// If selector was passed
@ -296,11 +335,17 @@ GitLabDropdown = (function() {
_this.fullData = data;
_this.parseData(_this.fullData);
_this.focusTextInput();
if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
if (
_this.options.filterable &&
_this.filter &&
_this.filter.input &&
_this.filter.input.val() &&
_this.filter.input.val().trim() !== ''
) {
return _this.filter.input.trigger('input');
}
};
// Remote data
// Remote data
})(this),
instance: this,
});
@ -325,7 +370,7 @@ GitLabDropdown = (function() {
return function() {
selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
selector = '.dropdown-page-one ' + selector;
}
return $(selector, this.instance.dropdown);
};
@ -341,80 +386,97 @@ GitLabDropdown = (function() {
if (_this.filterInput.val() !== '') {
selector = SELECTABLE_CLASSES;
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
selector = '.dropdown-page-one ' + selector;
}
if ($(_this.el).is('input')) {
currentIndex = -1;
} else {
$(selector, _this.dropdown).first().find('a').addClass('is-focused');
$(selector, _this.dropdown)
.first()
.find('a')
.addClass('is-focused');
currentIndex = 0;
}
}
};
})(this)
})(this),
});
}
// Event listeners
this.dropdown.on("shown.bs.dropdown", this.opened);
this.dropdown.on("hidden.bs.dropdown", this.hidden);
$(this.el).on("update.label", this.updateLabel);
this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate);
this.dropdown.on('keyup', (function(_this) {
return function(e) {
// Escape key
if (e.which === 27) {
return $('.dropdown-menu-close', _this.dropdown).trigger('click');
}
};
})(this));
this.dropdown.on('blur', 'a', (function(_this) {
return function(e) {
var $dropdownMenu, $relatedTarget;
if (e.relatedTarget != null) {
$relatedTarget = $(e.relatedTarget);
$dropdownMenu = $relatedTarget.closest('.dropdown-menu');
if ($dropdownMenu.length === 0) {
return _this.dropdown.removeClass('show');
}
}
};
})(this));
if (this.dropdown.find(".dropdown-toggle-page").length) {
this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_this) {
this.dropdown.on('shown.bs.dropdown', this.opened);
this.dropdown.on('hidden.bs.dropdown', this.hidden);
$(this.el).on('update.label', this.updateLabel);
this.dropdown.on('click', '.dropdown-menu, .dropdown-menu-close', this.shouldPropagate);
this.dropdown.on(
'keyup',
(function(_this) {
return function(e) {
e.preventDefault();
e.stopPropagation();
return _this.togglePage();
// Escape key
if (e.which === 27) {
return $('.dropdown-menu-close', _this.dropdown).trigger('click');
}
};
})(this));
})(this),
);
this.dropdown.on(
'blur',
'a',
(function(_this) {
return function(e) {
var $dropdownMenu, $relatedTarget;
if (e.relatedTarget != null) {
$relatedTarget = $(e.relatedTarget);
$dropdownMenu = $relatedTarget.closest('.dropdown-menu');
if ($dropdownMenu.length === 0) {
return _this.dropdown.removeClass('show');
}
}
};
})(this),
);
if (this.dropdown.find('.dropdown-toggle-page').length) {
this.dropdown.find('.dropdown-toggle-page, .dropdown-menu-back').on(
'click',
(function(_this) {
return function(e) {
e.preventDefault();
e.stopPropagation();
return _this.togglePage();
};
})(this),
);
}
if (this.options.selectable) {
selector = ".dropdown-content a";
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one .dropdown-content a";
selector = '.dropdown-content a';
if (this.dropdown.find('.dropdown-toggle-page').length) {
selector = '.dropdown-page-one .dropdown-content a';
}
this.dropdown.on("click", selector, function(e) {
var $el, selected, selectedObj, isMarking;
$el = $(e.currentTarget);
selected = self.rowClicked($el);
selectedObj = selected ? selected[0] : null;
isMarking = selected ? selected[1] : null;
if (this.options.clicked) {
this.options.clicked.call(this, {
selectedObj,
$el,
e,
isMarking,
});
}
this.dropdown.on(
'click',
selector,
function(e) {
var $el, selected, selectedObj, isMarking;
$el = $(e.currentTarget);
selected = self.rowClicked($el);
selectedObj = selected ? selected[0] : null;
isMarking = selected ? selected[1] : null;
if (this.options.clicked) {
this.options.clicked.call(this, {
selectedObj,
$el,
e,
isMarking,
});
}
// Update label right after all modifications in dropdown has been done
if (this.options.toggleLabel) {
this.updateLabel(selectedObj, $el, this);
}
// Update label right after all modifications in dropdown has been done
if (this.options.toggleLabel) {
this.updateLabel(selectedObj, $el, this);
}
$el.trigger('blur');
}.bind(this));
$el.trigger('blur');
}.bind(this),
);
}
}
@ -452,10 +514,15 @@ GitLabDropdown = (function() {
html = [];
for (name in data) {
groupData = data[name];
html.push(this.renderItem({
header: name
// Add header for each group
}, name));
html.push(
this.renderItem(
{
header: name,
// Add header for each group
},
name,
),
);
this.renderData(groupData, name).map(function(item) {
return html.push(item);
});
@ -474,20 +541,25 @@ GitLabDropdown = (function() {
if (group == null) {
group = false;
}
return data.map((function(_this) {
return function(obj, index) {
return _this.renderItem(obj, group, index);
};
})(this));
return data.map(
(function(_this) {
return function(obj, index) {
return _this.renderItem(obj, group, index);
};
})(this),
);
};
GitLabDropdown.prototype.shouldPropagate = function(e) {
var $target;
if (this.options.multiSelect || this.options.shouldPropagate === false) {
$target = $(e.target);
if ($target && !$target.hasClass('dropdown-menu-close') &&
!$target.hasClass('dropdown-menu-close-icon') &&
!$target.data('isLink')) {
if (
$target &&
!$target.hasClass('dropdown-menu-close') &&
!$target.hasClass('dropdown-menu-close-icon') &&
!$target.data('isLink')
) {
e.stopPropagation();
return false;
} else {
@ -497,9 +569,11 @@ GitLabDropdown = (function() {
};
GitLabDropdown.prototype.filteredFullData = function() {
return this.fullData.filter(r => typeof r === 'object'
&& !Object.prototype.hasOwnProperty.call(r, 'beforeDivider')
&& !Object.prototype.hasOwnProperty.call(r, 'header')
return this.fullData.filter(
r =>
typeof r === 'object' &&
!Object.prototype.hasOwnProperty.call(r, 'beforeDivider') &&
!Object.prototype.hasOwnProperty.call(r, 'header'),
);
};
@ -522,11 +596,16 @@ GitLabDropdown = (function() {
// matches the correct layout
const inputValue = this.filterInput.val();
if (this.fullData && hasMultiSelect && this.options.processData && inputValue.length === 0) {
this.options.processData.call(this.options, inputValue, this.filteredFullData(), this.parseData.bind(this));
this.options.processData.call(
this.options,
inputValue,
this.filteredFullData(),
this.parseData.bind(this),
);
}
contentHtml = $('.dropdown-content', this.dropdown).html();
if (this.remote && contentHtml === "") {
if (this.remote && contentHtml === '') {
this.remote.execute();
} else {
this.focusTextInput();
@ -537,7 +616,11 @@ GitLabDropdown = (function() {
}
if (this.options.opened) {
this.options.opened.call(this, e);
if (this.options.preserveContext) {
this.options.opened(e);
} else {
this.options.opened.call(this, e);
}
}
return this.dropdown.trigger('shown.gl.dropdown');
@ -555,11 +638,11 @@ GitLabDropdown = (function() {
var $input;
this.resetRows();
this.removeArrayKeyEvent();
$input = this.dropdown.find(".dropdown-input-field");
$input = this.dropdown.find('.dropdown-input-field');
if (this.options.filterable) {
$input.blur();
}
if (this.dropdown.find(".dropdown-toggle-page").length) {
if (this.dropdown.find('.dropdown-toggle-page').length) {
$('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
}
if (this.options.hidden) {
@ -601,7 +684,7 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.clearMenu = function() {
var selector;
selector = '.dropdown-content';
if (this.dropdown.find(".dropdown-toggle-page").length) {
if (this.dropdown.find('.dropdown-toggle-page').length) {
if (this.options.containerSelector) {
selector = this.options.containerSelector;
} else {
@ -619,7 +702,7 @@ GitLabDropdown = (function() {
value = this.options.id ? this.options.id(data) : data.id;
if (value) {
value = value.toString().replace(/'/g, '\\\'');
value = value.toString().replace(/'/g, "\\'");
}
}
@ -676,21 +759,27 @@ GitLabDropdown = (function() {
text = data.text != null ? data.text : '';
}
if (this.highlight) {
text = this.highlightTextMatches(text, this.filterInput.val());
text = data.template
? this.highlightTemplate(text, data.template)
: this.highlightTextMatches(text, this.filterInput.val());
}
// Create the list item & the link
var link = document.createElement('a');
link.href = url;
if (this.highlight) {
if (this.icon) {
text = `<span>${text}</span>`;
link.classList.add('d-flex', 'align-items-center');
link.innerHTML = data.icon ? data.icon + text : text;
} else if (this.highlight) {
link.innerHTML = text;
} else {
link.textContent = text;
}
if (selected) {
link.className = 'is-active';
link.classList.add('is-active');
}
if (group) {
@ -703,17 +792,24 @@ GitLabDropdown = (function() {
return html;
};
GitLabDropdown.prototype.highlightTemplate = function(text, template) {
return `"<b>${_.escape(text)}</b>" ${template}`;
};
GitLabDropdown.prototype.highlightTextMatches = function(text, term) {
const occurrences = fuzzaldrinPlus.match(text, term);
const { indexOf } = [];
return text.split('').map(function(character, i) {
if (indexOf.call(occurrences, i) !== -1) {
return "<b>" + character + "</b>";
} else {
return character;
}
}).join('');
return text
.split('')
.map(function(character, i) {
if (indexOf.call(occurrences, i) !== -1) {
return '<b>' + character + '</b>';
} else {
return character;
}
})
.join('');
};
GitLabDropdown.prototype.noResults = function() {
@ -748,13 +844,15 @@ GitLabDropdown = (function() {
}
field = [];
value = this.options.id
? this.options.id(selectedObject, el)
: selectedObject.id;
value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
if (isInput) {
field = $(this.el);
} else if (value != null) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
field = this.dropdown
.parent()
.find(
"input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, "\\'") + "']",
);
}
if (this.options.isSelectable && !this.options.isSelectable(selectedObject, el)) {
@ -780,9 +878,12 @@ GitLabDropdown = (function() {
} else {
isMarking = true;
if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
this.dropdown.find('.' + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
if (!isInput) {
this.dropdown.parent().find("input[name='" + fieldName + "']").remove();
this.dropdown
.parent()
.find("input[name='" + fieldName + "']")
.remove();
}
}
if (field && field.length && value == null) {
@ -823,13 +924,16 @@ GitLabDropdown = (function() {
$('input[name="' + fieldName + '"]').remove();
}
$input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
$input = $('<input>')
.attr('type', 'hidden')
.attr('name', fieldName)
.val(value);
if (this.options.inputId != null) {
$input.attr('id', this.options.inputId);
}
if (this.options.multiSelect) {
Object.keys(selectedObject).forEach((attribute) => {
Object.keys(selectedObject).forEach(attribute => {
$input.attr(`data-${attribute}`, selectedObject[attribute]);
});
}
@ -844,13 +948,13 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.selectRowAtIndex = function(index) {
var $el, selector;
// If we pass an option index
if (typeof index !== "undefined") {
selector = SELECTABLE_CLASSES + ":eq(" + index + ") a";
if (typeof index !== 'undefined') {
selector = SELECTABLE_CLASSES + ':eq(' + index + ') a';
} else {
selector = ".dropdown-content .is-focused";
selector = '.dropdown-content .is-focused';
}
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one " + selector;
if (this.dropdown.find('.dropdown-toggle-page').length) {
selector = '.dropdown-page-one ' + selector;
}
// simulate a click on the first link
$el = $(selector, this.dropdown);
@ -867,44 +971,47 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.addArrowKeyEvent = function() {
var $input, ARROW_KEY_CODES, selector;
ARROW_KEY_CODES = [38, 40];
$input = this.dropdown.find(".dropdown-input-field");
$input = this.dropdown.find('.dropdown-input-field');
selector = SELECTABLE_CLASSES;
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one " + selector;
if (this.dropdown.find('.dropdown-toggle-page').length) {
selector = '.dropdown-page-one ' + selector;
}
return $('body').on('keydown', (function(_this) {
return function(e) {
var $listItems, PREV_INDEX, currentKeyCode;
currentKeyCode = e.which;
if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
e.preventDefault();
e.stopImmediatePropagation();
PREV_INDEX = currentIndex;
$listItems = $(selector, _this.dropdown);
// if @options.filterable
// $input.blur()
if (currentKeyCode === 40) {
// Move down
if (currentIndex < ($listItems.length - 1)) {
currentIndex += 1;
return $('body').on(
'keydown',
(function(_this) {
return function(e) {
var $listItems, PREV_INDEX, currentKeyCode;
currentKeyCode = e.which;
if (ARROW_KEY_CODES.indexOf(currentKeyCode) !== -1) {
e.preventDefault();
e.stopImmediatePropagation();
PREV_INDEX = currentIndex;
$listItems = $(selector, _this.dropdown);
// if @options.filterable
// $input.blur()
if (currentKeyCode === 40) {
// Move down
if (currentIndex < $listItems.length - 1) {
currentIndex += 1;
}
} else if (currentKeyCode === 38) {
// Move up
if (currentIndex > 0) {
currentIndex -= 1;
}
}
} else if (currentKeyCode === 38) {
// Move up
if (currentIndex > 0) {
currentIndex -= 1;
if (currentIndex !== PREV_INDEX) {
_this.highlightRowAtIndex($listItems, currentIndex);
}
return false;
}
if (currentIndex !== PREV_INDEX) {
_this.highlightRowAtIndex($listItems, currentIndex);
if (currentKeyCode === 13 && currentIndex !== -1) {
e.preventDefault();
_this.selectRowAtIndex();
}
return false;
}
if (currentKeyCode === 13 && currentIndex !== -1) {
e.preventDefault();
_this.selectRowAtIndex();
}
};
})(this));
};
})(this),
);
};
GitLabDropdown.prototype.removeArrayKeyEvent = function() {
@ -917,12 +1024,25 @@ GitLabDropdown = (function() {
};
GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop;
var $dropdownContent,
$listItem,
dropdownContentBottom,
dropdownContentHeight,
dropdownContentTop,
dropdownScrollTop,
listItemBottom,
listItemHeight,
listItemTop;
if (!$listItems) {
$listItems = $(SELECTABLE_CLASSES, this.dropdown);
}
// Remove the class for the previously focused row
$('.is-focused', this.dropdown).removeClass('is-focused');
// Update the class for the row at the specific index
$listItem = $listItems.eq(index);
$listItem.find('a:first-child').addClass("is-focused");
$listItem.find('a:first-child').addClass('is-focused');
// Dropdown content scroll area
$dropdownContent = $listItem.closest('.dropdown-content');
dropdownScrollTop = $dropdownContent.scrollTop();
@ -936,15 +1056,19 @@ GitLabDropdown = (function() {
if (!index) {
// Scroll the dropdown content to the top
$dropdownContent.scrollTop(0);
} else if (index === ($listItems.length - 1)) {
} else if (index === $listItems.length - 1) {
// Scroll the dropdown content to the bottom
$dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
} else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
} else if (listItemBottom > dropdownContentBottom + dropdownScrollTop) {
// Scroll the dropdown content down
$dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
} else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
$dropdownContent.scrollTop(
listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING,
);
} else if (listItemTop < dropdownContentTop + dropdownScrollTop) {
// Scroll the dropdown content up
return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING);
return $dropdownContent.scrollTop(
listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING,
);
}
};
@ -965,7 +1089,9 @@ GitLabDropdown = (function() {
toggleText = this.options.updateLabel;
}
return $(this.el).find(".dropdown-toggle-text").text(toggleText);
return $(this.el)
.find('.dropdown-toggle-text')
.text(toggleText);
};
GitLabDropdown.prototype.clearField = function(field, isInput) {

View file

@ -1,23 +1,36 @@
import $ from 'jquery';
import { parseQueryStringIntoObject } from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import createFlash from '~/flash';
import { __ } from '~/locale';
export default class GpgBadges {
static fetch() {
const badges = $('.js-loading-gpg-badge');
const tag = $('.js-signature-container');
if (tag.length === 0) {
return Promise.resolve();
}
const badges = $('.js-loading-gpg-badge');
badges.html('<i class="fa fa-spinner fa-spin"></i>');
const displayError = () => createFlash(__('An error occurred while loading commit signatures'));
const endpoint = tag.data('signaturesPath');
if (!endpoint) {
displayError();
return Promise.reject(new Error('Missing commit signatures endpoint!'));
}
const params = parseQueryStringIntoObject(tag.serialize());
return axios.get(tag.data('signaturesPath'), { params })
.then(({ data }) => {
data.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
});
})
.catch(() => flash(__('An error occurred while loading commits')));
return axios
.get(endpoint, { params })
.then(({ data }) => {
data.signatures.forEach(signature => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
});
})
.catch(displayError);
}
}

View file

@ -78,17 +78,10 @@ export default {
>
<div
:class="{ 'project-row-contents': !isGroup }"
class="group-row-contents">
<item-actions
v-if="isGroup"
:group="group"
:parent-group="parentGroup"
/>
<item-stats
:item="group"
/>
class="group-row-contents d-flex justify-content-end align-items-center"
>
<div
class="folder-toggle-wrap"
class="folder-toggle-wrap append-right-4 d-flex align-items-center"
>
<item-caret
:is-group-open="group.isOpen"
@ -100,7 +93,7 @@ export default {
</div>
<div
:class="{ 'content-loading': group.isChildrenLoading }"
class="avatar-container prepend-top-8 prepend-left-5 s24 d-none d-sm-block"
class="avatar-container s24 d-none d-sm-block"
>
<a
:href="group.relativePath"
@ -120,32 +113,46 @@ export default {
</a>
</div>
<div
class="title namespace-title"
class="group-text flex-grow"
>
<a
v-tooltip
:href="group.relativePath"
:title="group.fullName"
class="no-expand"
data-placement="bottom"
>{{
// ending bracket must be by closing tag to prevent
// link hover text-decoration from over-extending
group.name
}}</a>
<span
v-if="group.permission"
class="user-access-role"
<div
class="title namespace-title append-right-8"
>
{{ group.permission }}
</span>
</div>
<div
v-if="group.description"
class="description">
<span v-html="group.description">
</span>
<a
v-tooltip
:href="group.relativePath"
:title="group.fullName"
class="no-expand"
data-placement="bottom"
>{{
// ending bracket must be by closing tag to prevent
// link hover text-decoration from over-extending
group.name
}}</a>
<span
v-if="group.permission"
class="user-access-role"
>
{{ group.permission }}
</span>
</div>
<div
v-if="group.description"
class="description"
>
<span v-html="group.description">
</span>
</div>
</div>
<item-stats
:item="group"
class="group-stats prepend-top-2"
/>
<item-actions
v-if="isGroup"
:group="group"
:parent-group="parentGroup"
/>
</div>
<group-folder
v-if="group.isOpen && hasChildren"

View file

@ -12,6 +12,7 @@ export default function groupsSelect() {
const skipGroups = $select.data('skipGroups') || [];
$select.select2({
placeholder: 'Search for a group',
allowClear: $select.hasClass('allowClear'),
multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
ajax: {

View file

@ -0,0 +1,33 @@
import _ from 'underscore';
import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
export const DEFAULT_SIZE_CLASS = 's40';
export const IDENTICON_BG_COUNT = 7;
export function getIdenticonBackgroundClass(entityId) {
const type = (entityId % IDENTICON_BG_COUNT) + 1;
return `bg${type}`;
}
export function getIdenticonTitle(entityName) {
return getFirstCharacterCapitalized(entityName) || ' ';
}
export function renderIdenticon(entity, options = {}) {
const { sizeClass = DEFAULT_SIZE_CLASS } = options;
const bgClass = getIdenticonBackgroundClass(entity.id);
const title = getIdenticonTitle(entity.name);
return `<div class="avatar identicon ${_.escape(sizeClass)} ${_.escape(bgClass)}">${_.escape(title)}</div>`;
}
export function renderAvatar(entity, options = {}) {
if (!entity.avatar_url) {
return renderIdenticon(entity, options);
}
const { sizeClass = DEFAULT_SIZE_CLASS } = options;
return `<img src="${_.escape(entity.avatar_url)}" class="avatar ${_.escape(sizeClass)}" />`;
}

View file

@ -13,11 +13,8 @@ export default {
tooltip,
},
computed: {
...mapGetters(['currentProject', 'hasChanges']),
...mapGetters(['hasChanges']),
...mapState(['currentActivityView']),
goBackUrl() {
return document.referrer || this.currentProject.web_url;
},
},
methods: {
...mapActions(['updateActivityBarView']),
@ -36,22 +33,6 @@ export default {
<template>
<nav class="ide-activity-bar">
<ul class="list-unstyled">
<li v-once>
<a
v-tooltip
:href="goBackUrl"
:title="s__('IDE|Go back')"
:aria-label="s__('IDE|Go back')"
data-container="body"
data-placement="right"
class="ide-sidebar-link"
>
<icon
:size="16"
name="go-back"
/>
</a>
</li>
<li>
<button
v-tooltip

View file

@ -0,0 +1,60 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
import router from '../../ide_router';
export default {
components: {
Icon,
Timeago,
},
props: {
item: {
type: Object,
required: true,
},
projectId: {
type: String,
required: true,
},
isActive: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
branchHref() {
return router.resolve(`/project/${this.projectId}/edit/${this.item.name}`).href;
},
},
};
</script>
<template>
<a
:href="branchHref"
class="btn-link d-flex align-items-center"
>
<span class="d-flex append-right-default ide-search-list-current-icon">
<icon
v-if="isActive"
:size="18"
name="mobile-issue-close"
/>
</span>
<span>
<strong>
{{ item.name }}
</strong>
<span
class="ide-merge-request-project-path d-block mt-1"
>
Updated
<timeago
:time="item.committedDate || ''"
/>
</span>
</span>
</a>
</template>

View file

@ -0,0 +1,111 @@
<script>
import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import Item from './item.vue';
export default {
components: {
LoadingIcon,
Item,
Icon,
},
data() {
return {
search: '',
};
},
computed: {
...mapState('branches', ['branches', 'isLoading']),
...mapState(['currentBranchId', 'currentProjectId']),
hasBranches() {
return this.branches.length !== 0;
},
hasNoSearchResults() {
return this.search !== '' && !this.hasBranches;
},
},
watch: {
isLoading: {
handler: 'focusSearch',
},
},
mounted() {
this.loadBranches();
},
methods: {
...mapActions('branches', ['fetchBranches']),
loadBranches() {
this.fetchBranches({ search: this.search });
},
searchBranches: _.debounce(function debounceSearch() {
this.loadBranches();
}, 250),
focusSearch() {
if (!this.isLoading) {
this.$nextTick(() => {
this.$refs.searchInput.focus();
});
}
},
isActiveBranch(item) {
return item.name === this.currentBranchId;
},
},
};
</script>
<template>
<div>
<div class="dropdown-input mt-3 pb-3 mb-0 border-bottom">
<div class="position-relative">
<input
ref="searchInput"
:placeholder="__('Search branches')"
v-model="search"
type="search"
class="form-control dropdown-input-field"
@input="searchBranches"
/>
<icon
:size="18"
name="search"
class="input-icon"
/>
</div>
</div>
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<loading-icon
v-if="isLoading"
class="mt-3 mb-3 align-self-center ml-auto mr-auto"
size="2"
/>
<ul
v-else
class="mb-3 w-100"
>
<template v-if="hasBranches">
<li
v-for="item in branches"
:key="item.name"
>
<item
:item="item"
:project-id="currentProjectId"
:is-active="isActiveBranch(item)"
/>
</li>
</template>
<li
v-else
class="ide-search-list-empty d-flex align-items-center justify-content-center"
>
<template v-if="hasNoSearchResults">
{{ __('No branches found') }}
</template>
</li>
</ul>
</div>
</div>
</template>

View file

@ -3,6 +3,7 @@ import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
import { getCommitIconMap } from '../utils';
export default {
components: {
@ -34,16 +35,14 @@ export default {
},
computed: {
changedIcon() {
const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : '';
return this.file.tempFile && !this.forceModifiedIcon
? `file-addition${suffix}`
: `file-modified${suffix}`;
},
stagedIcon() {
return `${this.changedIcon}-solid`;
const suffix = !this.file.changed && this.file.staged && !this.showStagedIcon ? '-solid' : '';
if (this.forceModifiedIcon) return `file-modified${suffix}`;
return `${getCommitIconMap(this.file).icon}${suffix}`;
},
changedIconClass() {
return `multi-${this.changedIcon} float-left`;
return `ide-${this.changedIcon} float-left`;
},
tooltipTitle() {
if (!this.showTooltip) return undefined;
@ -66,6 +65,9 @@ export default {
return undefined;
},
showIcon() {
return this.file.changed || this.file.tempFile || this.file.staged || this.file.deleted;
},
},
};
</script>
@ -79,7 +81,7 @@ export default {
class="ide-file-changed-icon"
>
<icon
v-if="file.changed || file.tempFile || file.staged"
v-if="showIcon"
:name="changedIcon"
:size="12"
:css-classes="changedIconClass"

View file

@ -5,6 +5,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue';
import { viewerTypes } from '../../constants';
import { getCommitIconMap } from '../../utils';
export default {
components: {
@ -42,11 +43,12 @@ export default {
},
computed: {
iconName() {
const prefix = this.stagedList ? '-solid' : '';
return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`;
const suffix = this.stagedList ? '-solid' : '';
return `${getCommitIconMap(this.file).icon}${suffix}`;
},
iconClass() {
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
return `${getCommitIconMap(this.file).class} append-right-8`;
},
fullKey() {
return `${this.keyPrefix}-${this.file.key}`;
@ -67,6 +69,8 @@ export default {
'stageChange',
]),
openFileInEditor() {
if (this.file.type === 'tree') return null;
return this.openPendingTab({
file: this.file,
keyPrefix: this.keyPrefix,

View file

@ -56,7 +56,7 @@ export default {
>
<icon
:size="12"
name="more"
name="ellipsis_h"
/>
</button>
<div class="dropdown-menu dropdown-menu-right">

View file

@ -1,6 +1,8 @@
<script>
import Mousetrap from 'mousetrap';
import { mapActions, mapState, mapGetters } from 'vuex';
import { __ } from '~/locale';
import NewModal from './new_dropdown/modal.vue';
import IdeSidebar from './ide_side_bar.vue';
import RepoTabs from './repo_tabs.vue';
import IdeStatusBar from './ide_status_bar.vue';
@ -13,6 +15,7 @@ const originalStopCallback = Mousetrap.stopCallback;
export default {
components: {
NewModal,
IdeSidebar,
RepoTabs,
IdeStatusBar,
@ -23,7 +26,6 @@ export default {
},
computed: {
...mapState([
'changedFiles',
'openFiles',
'viewer',
'currentMergeRequestId',
@ -32,18 +34,10 @@ export default {
'currentProjectId',
'errorMessage',
]),
...mapGetters(['activeFile', 'hasChanges']),
...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges']),
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined;
Object.assign(e, {
returnValue,
});
return returnValue;
};
window.onbeforeunload = e => this.onBeforeUnload(e);
Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
if (e.preventDefault) {
@ -57,6 +51,16 @@ export default {
},
methods: {
...mapActions(['toggleFileFinder']),
onBeforeUnload(e = {}) {
const returnValue = __('Are you sure you want to lose unsaved changes?');
if (!this.someUncommitedChanges) return undefined;
Object.assign(e, {
returnValue,
});
return returnValue;
},
mousetrapStopCallback(e, el, combo) {
if (
(combo === 't' && el.classList.contains('dropdown-input-field')) ||
@ -137,5 +141,6 @@ export default {
/>
</div>
<ide-status-bar :file="activeFile"/>
<new-modal />
</article>
</template>

View file

@ -0,0 +1,37 @@
<script>
import ProjectAvatarDefault from '~/vue_shared/components/project_avatar/default.vue';
export default {
components: {
ProjectAvatarDefault,
},
props: {
project: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="context-header ide-context-header">
<a
:href="project.web_url"
:title="s__('IDE|Go to project')"
>
<project-avatar-default
:project="project"
:size="48"
/>
<span class="ide-sidebar-project-title">
<span class="sidebar-context-title">
{{ project.name }}
</span>
<span class="sidebar-context-title text-secondary">
{{ project.path_with_namespace }}
</span>
</span>
</a>
</div>
</template>

View file

@ -10,7 +10,7 @@ export default {
EditorModeDropdown,
},
computed: {
...mapGetters(['currentMergeRequest']),
...mapGetters(['currentMergeRequest', 'activeFile']),
...mapState(['viewer', 'currentMergeRequestId']),
showLatestChangesText() {
return !this.currentMergeRequestId || this.viewer === viewerTypes.diff;
@ -23,12 +23,20 @@ export default {
},
},
mounted() {
if (this.activeFile && this.activeFile.pending && !this.activeFile.deleted) {
this.$router.push(`/project${this.activeFile.url}`, () => {
this.updateViewer('editor');
});
} else if (this.activeFile && this.activeFile.deleted) {
this.resetOpenFiles();
}
this.$nextTick(() => {
this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff);
});
},
methods: {
...mapActions(['updateViewer']),
...mapActions(['updateViewer', 'resetOpenFiles']),
},
};
</script>
@ -36,7 +44,6 @@ export default {
<template>
<ide-tree-list
:viewer-type="viewer"
:disable-action-dropdown="true"
header-class="ide-review-header"
>
<template

View file

@ -1,12 +1,6 @@
<script>
import $ from 'jquery';
import { mapState, mapGetters } from 'vuex';
import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import Identicon from '../../vue_shared/components/identicon.vue';
import IdeTree from './ide_tree.vue';
import ResizablePanel from './resizable_panel.vue';
import ActivityBar from './activity_bar.vue';
@ -14,43 +8,28 @@ import CommitSection from './repo_commit_section.vue';
import CommitForm from './commit_sidebar/form.vue';
import IdeReview from './ide_review.vue';
import SuccessMessage from './commit_sidebar/success_message.vue';
import MergeRequestDropdown from './merge_requests/dropdown.vue';
import IdeProjectHeader from './ide_project_header.vue';
import { activityBarViews } from '../constants';
export default {
directives: {
tooltip,
},
components: {
Icon,
PanelResizer,
SkeletonLoadingContainer,
ResizablePanel,
ActivityBar,
ProjectAvatarImage,
Identicon,
CommitSection,
IdeTree,
CommitForm,
IdeReview,
SuccessMessage,
MergeRequestDropdown,
},
data() {
return {
showTooltip: false,
showMergeRequestsDropdown: false,
};
IdeProjectHeader,
},
computed: {
...mapState([
'loading',
'currentBranchId',
'currentActivityView',
'changedFiles',
'stagedFiles',
'lastCommitMsg',
'currentMergeRequestId',
]),
...mapGetters(['currentProject', 'someUncommitedChanges']),
showSuccessMessage() {
@ -59,46 +38,6 @@ export default {
(this.lastCommitMsg && !this.someUncommitedChanges)
);
},
branchTooltipTitle() {
return this.showTooltip ? this.currentBranchId : undefined;
},
},
watch: {
currentBranchId() {
this.$nextTick(() => {
if (!this.$refs.branchId) return;
this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth;
});
},
loading() {
this.$nextTick(() => {
this.addDropdownListeners();
});
},
},
mounted() {
this.addDropdownListeners();
},
beforeDestroy() {
$(this.$refs.mergeRequestDropdown)
.off('show.bs.dropdown')
.off('hide.bs.dropdown');
},
methods: {
addDropdownListeners() {
if (!this.$refs.mergeRequestDropdown) return;
$(this.$refs.mergeRequestDropdown)
.on('show.bs.dropdown', () => {
this.toggleMergeRequestDropdown();
}).on('hide.bs.dropdown', () => {
this.toggleMergeRequestDropdown();
});
},
toggleMergeRequestDropdown() {
this.showMergeRequestsDropdown = !this.showMergeRequestsDropdown;
},
},
};
</script>
@ -108,12 +47,10 @@ export default {
:collapsible="false"
:initial-width="340"
side="left"
class="flex-column"
>
<activity-bar
v-if="!loading"
/>
<div class="multi-file-commit-panel-inner">
<template v-if="loading">
<template v-if="loading">
<div class="multi-file-commit-panel-inner">
<div
v-for="n in 3"
:key="n"
@ -121,81 +58,23 @@ export default {
>
<skeleton-loading-container />
</div>
</template>
<template v-else>
<div
ref="mergeRequestDropdown"
class="context-header ide-context-header dropdown"
>
<button
type="button"
data-toggle="dropdown"
>
<div
v-if="currentProject.avatar_url"
class="avatar-container s40 project-avatar"
>
<project-avatar-image
:link-href="currentProject.path"
:img-src="currentProject.avatar_url"
:img-alt="currentProject.name"
:img-size="40"
class="avatar-container project-avatar"
/>
</div>
<identicon
v-else
:entity-id="currentProject.id"
:entity-name="currentProject.name"
size-class="s40"
</div>
</template>
<template v-else>
<ide-project-header
:project="currentProject"
/>
<div class="ide-context-body d-flex flex-fill">
<activity-bar />
<div class="multi-file-commit-panel-inner">
<div class="multi-file-commit-panel-inner-content">
<component
:is="currentActivityView"
/>
<div class="ide-sidebar-project-title">
<div class="sidebar-context-title">
{{ currentProject.name }}
</div>
<div class="d-flex">
<div
v-tooltip
v-if="currentBranchId"
ref="branchId"
:title="branchTooltipTitle"
class="sidebar-context-title ide-sidebar-branch-title"
>
<icon
name="branch"
css-classes="append-right-5"
/>{{ currentBranchId }}
</div>
<div
v-if="currentMergeRequestId"
:class="{
'prepend-left-8': currentBranchId
}"
class="sidebar-context-title ide-sidebar-branch-title"
>
<icon
name="git-merge"
css-classes="append-right-5"
/>!{{ currentMergeRequestId }}
</div>
</div>
</div>
<icon
class="ml-auto"
name="chevron-down"
/>
</button>
<merge-request-dropdown
:show="showMergeRequestsDropdown"
/>
</div>
<commit-form />
</div>
<div class="multi-file-commit-panel-inner-scroll">
<component
:is="currentActivityView"
/>
</div>
<commit-form />
</template>
</div>
</div>
</template>
</resizable-panel>
</template>

View file

@ -5,6 +5,7 @@ import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import CiIcon from '../../vue_shared/components/ci_icon.vue';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import { rightSidebarViews } from '../constants';
export default {
components: {
@ -49,6 +50,7 @@ export default {
this.stopPipelinePolling();
},
methods: {
...mapActions(['setRightPane']),
...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']),
startTimer() {
this.intervalId = setInterval(() => {
@ -69,24 +71,31 @@ export default {
return `${this.currentProject.web_url}/commit/${shortSha}`;
},
},
rightSidebarViews,
};
</script>
<template>
<footer class="ide-status-bar">
<div
v-if="lastCommit && lastCommitFormatedAge"
v-if="lastCommit"
class="ide-status-branch"
>
<span
v-if="latestPipeline && latestPipeline.details"
class="ide-status-pipeline"
>
<ci-icon
v-tooltip
:status="latestPipeline.details.status"
:title="latestPipeline.details.status.text"
/>
<button
type="button"
class="p-0 border-0 h-50"
@click="setRightPane($options.rightSidebarViews.pipelines)"
>
<ci-icon
v-tooltip
:status="latestPipeline.details.status"
:title="latestPipeline.details.status.text"
/>
</button>
Pipeline
<a
:href="latestPipeline.details.status.details_path"

View file

@ -1,26 +1,34 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import NewDropdown from './new_dropdown/index.vue';
import Icon from '~/vue_shared/components/icon.vue';
import IdeTreeList from './ide_tree_list.vue';
import Upload from './new_dropdown/upload.vue';
import NewEntryButton from './new_dropdown/button.vue';
export default {
components: {
NewDropdown,
Icon,
Upload,
IdeTreeList,
NewEntryButton,
},
computed: {
...mapState(['currentBranchId']),
...mapGetters(['currentProject', 'currentTree', 'activeFile']),
},
mounted() {
if (this.activeFile && this.activeFile.pending) {
if (!this.activeFile) return;
if (this.activeFile.pending && !this.activeFile.deleted) {
this.$router.push(`/project${this.activeFile.url}`, () => {
this.updateViewer('editor');
});
} else if (this.activeFile.deleted) {
this.resetOpenFiles();
}
},
methods: {
...mapActions(['updateViewer']),
...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry', 'resetOpenFiles']),
},
};
</script>
@ -33,10 +41,28 @@ export default {
slot="header"
>
{{ __('Edit') }}
<new-dropdown
:project-id="currentProject.name_with_namespace"
:branch="currentBranchId"
/>
<div class="ide-tree-actions ml-auto d-flex">
<new-entry-button
:label="__('New file')"
:show-label="false"
class="d-flex border-0 p-0 mr-3"
icon="doc-new"
@click="openNewEntryModal({ type: 'blob' })"
/>
<upload
:show-label="false"
class="d-flex mr-3"
button-css-classes="border-0 p-0"
@create="createTempEntry"
/>
<new-entry-button
:label="__('New directory')"
:show-label="false"
class="d-flex border-0 p-0"
icon="folder-new"
@click="openNewEntryModal({ type: 'tree' })"
/>
</div>
</template>
</ide-tree-list>
</template>

View file

@ -3,14 +3,14 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import RepoFile from './repo_file.vue';
import NewDropdown from './new_dropdown/index.vue';
import NavDropdown from './nav_dropdown.vue';
export default {
components: {
Icon,
RepoFile,
SkeletonLoadingContainer,
NewDropdown,
NavDropdown,
},
props: {
viewerType: {
@ -22,11 +22,6 @@ export default {
required: false,
default: null,
},
disableActionDropdown: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState(['currentBranchId']),
@ -62,15 +57,19 @@ export default {
:class="headerClass"
class="ide-tree-header"
>
<nav-dropdown />
<slot name="header"></slot>
</header>
<repo-file
v-for="file in currentTree.tree"
:key="file.key"
:file="file"
:level="0"
:disable-action-dropdown="disableActionDropdown"
/>
<div
class="ide-tree-body h-100"
>
<repo-file
v-for="file in currentTree.tree"
:key="file.key"
:file="file"
:level="0"
/>
</div>
</template>
</div>
</template>

View file

@ -131,6 +131,9 @@ export default {
v-show="detailJob.isLoading"
class="build-loader-animation"
>
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
</pre>
</div>

View file

@ -1,63 +0,0 @@
<script>
import { mapGetters } from 'vuex';
import Tabs from '../../../vue_shared/components/tabs/tabs';
import Tab from '../../../vue_shared/components/tabs/tab.vue';
import List from './list.vue';
export default {
components: {
Tabs,
Tab,
List,
},
props: {
show: {
type: Boolean,
required: true,
},
},
computed: {
...mapGetters('mergeRequests', ['assignedData', 'createdData']),
createdMergeRequestLength() {
return this.createdData.mergeRequests.length;
},
assignedMergeRequestLength() {
return this.assignedData.mergeRequests.length;
},
},
};
</script>
<template>
<div class="dropdown-menu ide-merge-requests-dropdown p-0">
<tabs
v-if="show"
stop-propagation
>
<tab active>
<template slot="title">
{{ __('Created by me') }}
<span class="badge badge-pill">
{{ createdMergeRequestLength }}
</span>
</template>
<list
:empty-text="__('You have not created any merge requests')"
type="created"
/>
</tab>
<tab>
<template slot="title">
{{ __('Assigned to me') }}
<span class="badge badge-pill">
{{ assignedMergeRequestLength }}
</span>
</template>
<list
:empty-text="__('You do not have any assigned merge requests')"
type="assigned"
/>
</tab>
</tabs>
</div>
</template>

View file

@ -1,5 +1,6 @@
<script>
import Icon from '../../../vue_shared/components/icon.vue';
import router from '../../ide_router';
export default {
components: {
@ -29,22 +30,21 @@ export default {
pathWithID() {
return `${this.item.projectPathWithNamespace}!${this.item.iid}`;
},
},
methods: {
clickItem() {
this.$emit('click', this.item);
mergeRequestHref() {
const path = `/project/${this.item.projectPathWithNamespace}/merge_requests/${this.item.iid}`;
return router.resolve(path).href;
},
},
};
</script>
<template>
<button
type="button"
<a
:href="mergeRequestHref"
class="btn-link d-flex align-items-center"
@click="clickItem"
>
<span class="d-flex append-right-default ide-merge-request-current-icon">
<span class="d-flex append-right-default ide-search-list-current-icon">
<icon
v-if="isActive"
:size="18"
@ -59,5 +59,5 @@ export default {
{{ pathWithID }}
</span>
</span>
</button>
</a>
</template>

View file

@ -1,96 +1,101 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import Item from './item.vue';
import TokenedInput from '../shared/tokened_input.vue';
const SEARCH_TYPES = [
{ type: 'created', label: __('Created by me') },
{ type: 'assigned', label: __('Assigned to me') },
];
export default {
components: {
LoadingIcon,
TokenedInput,
Item,
},
props: {
type: {
type: String,
required: true,
},
emptyText: {
type: String,
required: true,
},
Icon,
},
data() {
return {
search: '',
currentSearchType: null,
hasSearchFocus: false,
};
},
computed: {
...mapGetters('mergeRequests', ['getData']),
...mapState('mergeRequests', ['mergeRequests', 'isLoading']),
...mapState(['currentMergeRequestId', 'currentProjectId']),
data() {
return this.getData(this.type);
},
isLoading() {
return this.data.isLoading;
},
mergeRequests() {
return this.data.mergeRequests;
},
hasMergeRequests() {
return this.mergeRequests.length !== 0;
},
hasNoSearchResults() {
return this.search !== '' && !this.hasMergeRequests;
},
showSearchTypes() {
return this.hasSearchFocus && !this.search && !this.currentSearchType;
},
type() {
return this.currentSearchType
? this.currentSearchType.type
: '';
},
searchTokens() {
return this.currentSearchType
? [this.currentSearchType]
: [];
},
},
watch: {
isLoading: {
handler: 'focusSearch',
search() {
// When the search is updated, let's turn off this flag to hide the search types
this.hasSearchFocus = false;
},
},
mounted() {
this.loadMergeRequests();
},
methods: {
...mapActions('mergeRequests', ['fetchMergeRequests', 'openMergeRequest']),
...mapActions('mergeRequests', ['fetchMergeRequests']),
loadMergeRequests() {
this.fetchMergeRequests({ type: this.type, search: this.search });
},
viewMergeRequest(item) {
this.openMergeRequest({
projectPath: item.projectPathWithNamespace,
id: item.iid,
});
},
searchMergeRequests: _.debounce(function debounceSearch() {
this.loadMergeRequests();
}, 250),
focusSearch() {
if (!this.isLoading) {
this.$nextTick(() => {
this.$refs.searchInput.focus();
});
}
onSearchFocus() {
this.hasSearchFocus = true;
},
setSearchType(searchType) {
this.currentSearchType = searchType;
this.loadMergeRequests();
},
},
searchTypes: SEARCH_TYPES,
};
</script>
<template>
<div>
<div class="dropdown-input mt-3 pb-3 mb-0 border-bottom">
<input
ref="searchInput"
:placeholder="__('Search merge requests')"
v-model="search"
type="search"
class="dropdown-input-field"
@input="searchMergeRequests"
/>
<i
aria-hidden="true"
class="fa fa-search dropdown-input-search"
></i>
<div class="position-relative">
<tokened-input
v-model="search"
:tokens="searchTokens"
:placeholder="__('Search merge requests')"
@focus="onSearchFocus"
@input="searchMergeRequests"
@removeToken="setSearchType(null)"
/>
<icon
:size="18"
name="search"
class="input-icon"
/>
</div>
</div>
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<loading-icon
@ -98,35 +103,52 @@ export default {
class="mt-3 mb-3 align-self-center ml-auto mr-auto"
size="2"
/>
<ul
v-else
class="mb-3 w-100"
>
<template v-if="hasMergeRequests">
<li
v-for="item in mergeRequests"
:key="item.id"
>
<item
:item="item"
:current-id="currentMergeRequestId"
:current-project-id="currentProjectId"
@click="viewMergeRequest"
/>
</li>
</template>
<li
v-else
class="ide-merge-requests-empty d-flex align-items-center justify-content-center"
<template v-else>
<ul
class="mb-3 w-100"
>
<template v-if="hasNoSearchResults">
<template v-if="showSearchTypes">
<li
v-for="searchType in $options.searchTypes"
:key="searchType.type"
>
<button
type="button"
class="btn-link d-flex align-items-center"
@click.stop="setSearchType(searchType)"
>
<span class="d-flex append-right-default ide-search-list-current-icon">
<icon
:size="18"
name="search"
/>
</span>
<span>
{{ searchType.label }}
</span>
</button>
</li>
</template>
<template v-else-if="hasMergeRequests">
<li
v-for="item in mergeRequests"
:key="item.id"
>
<item
:item="item"
:current-id="currentMergeRequestId"
:current-project-id="currentProjectId"
/>
</li>
</template>
<li
v-else
class="ide-search-list-empty d-flex align-items-center justify-content-center"
>
{{ __('No merge requests found') }}
</template>
<template v-else>
{{ emptyText }}
</template>
</li>
</ul>
</li>
</ul>
</template>
</div>
</div>
</template>

View file

@ -0,0 +1,59 @@
<script>
import $ from 'jquery';
import Icon from '~/vue_shared/components/icon.vue';
import NavForm from './nav_form.vue';
import NavDropdownButton from './nav_dropdown_button.vue';
export default {
components: {
Icon,
NavDropdownButton,
NavForm,
},
data() {
return {
isVisibleDropdown: false,
};
},
mounted() {
this.addDropdownListeners();
},
beforeDestroy() {
this.removeDropdownListeners();
},
methods: {
addDropdownListeners() {
$(this.$refs.dropdown)
.on('show.bs.dropdown', () => this.showDropdown())
.on('hide.bs.dropdown', () => this.hideDropdown());
},
removeDropdownListeners() {
$(this.$refs.dropdown)
.off('show.bs.dropdown')
.off('hide.bs.dropdown');
},
showDropdown() {
this.isVisibleDropdown = true;
},
hideDropdown() {
this.isVisibleDropdown = false;
},
},
};
</script>
<template>
<div
ref="dropdown"
class="btn-group ide-nav-dropdown dropdown"
>
<nav-dropdown-button />
<div
class="dropdown-menu dropdown-menu-left p-0"
>
<nav-form
v-if="isVisibleDropdown"
/>
</div>
</div>
</template>

View file

@ -0,0 +1,54 @@
<script>
import { mapState } from 'vuex';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
const EMPTY_LABEL = '-';
export default {
components: {
Icon,
DropdownButton,
},
computed: {
...mapState(['currentBranchId', 'currentMergeRequestId']),
mergeRequestLabel() {
return this.currentMergeRequestId
? `!${this.currentMergeRequestId}`
: EMPTY_LABEL;
},
branchLabel() {
return this.currentBranchId || EMPTY_LABEL;
},
},
};
</script>
<template>
<dropdown-button>
<span
class="row"
>
<span
class="col-7 text-truncate"
>
<icon
:size="16"
:aria-label="__('Current Branch')"
name="branch"
/>
{{ branchLabel }}
</span>
<span
class="col-5 pl-0 text-truncate"
>
<icon
:size="16"
:aria-label="__('Merge Request')"
name="merge-request"
/>
{{ mergeRequestLabel }}
</span>
</span>
</dropdown-button>
</template>

View file

@ -0,0 +1,40 @@
<script>
import Tabs from '~/vue_shared/components/tabs/tabs';
import Tab from '~/vue_shared/components/tabs/tab.vue';
import BranchesSearchList from './branches/search_list.vue';
import MergeRequestSearchList from './merge_requests/list.vue';
export default {
components: {
Tabs,
Tab,
BranchesSearchList,
MergeRequestSearchList,
},
};
</script>
<template>
<div
class="ide-nav-form p-0"
>
<tabs
stop-propagation
>
<tab
active
>
<template slot="title">
{{ __('Merge Requests') }}
</template>
<merge-request-search-list />
</tab>
<tab>
<template slot="title">
{{ __('Branches') }}
</template>
<branches-search-list />
</tab>
</tabs>
</div>
</template>

View file

@ -0,0 +1,52 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
},
props: {
label: {
type: String,
required: false,
default: null,
},
icon: {
type: String,
required: true,
},
iconClasses: {
type: String,
required: false,
default: null,
},
showLabel: {
type: Boolean,
required: false,
default: true,
},
},
methods: {
clicked() {
this.$emit('click');
},
},
};
</script>
<template>
<button
:aria-label="label"
type="button"
class="btn-blank"
@click.stop.prevent="clicked"
>
<icon
:name="icon"
:css-classes="iconClasses"
/>
<template v-if="showLabel">
{{ label }}
</template>
</button>
</template>

View file

@ -3,15 +3,18 @@ import { mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import newModal from './modal.vue';
import upload from './upload.vue';
import ItemButton from './button.vue';
import { modalTypes } from '../../constants';
export default {
components: {
icon,
newModal,
upload,
ItemButton,
},
props: {
branch: {
type: {
type: String,
required: true,
},
@ -20,35 +23,41 @@ export default {
required: false,
default: '',
},
mouseOver: {
type: Boolean,
required: true,
},
},
data() {
return {
openModal: false,
modalType: '',
dropdownOpen: false,
};
},
watch: {
dropdownOpen() {
this.$nextTick(() => {
this.$refs.dropdownMenu.scrollIntoView();
this.$refs.dropdownMenu.scrollIntoView({
block: 'nearest',
});
});
},
mouseOver() {
if (!this.mouseOver) {
this.dropdownOpen = false;
}
},
},
methods: {
...mapActions(['createTempEntry']),
...mapActions(['createTempEntry', 'openNewEntryModal', 'deleteEntry']),
createNewItem(type) {
this.modalType = type;
this.openModal = true;
this.openNewEntryModal({ type, path: this.path });
this.dropdownOpen = false;
},
hideModal() {
this.openModal = false;
},
openDropdown() {
this.dropdownOpen = !this.dropdownOpen;
},
},
modalTypes,
};
</script>
@ -58,63 +67,71 @@ export default {
:class="{
show: dropdownOpen,
}"
class="dropdown"
class="dropdown d-flex"
>
<button
:aria-label="__('Create new file or directory')"
type="button"
class="btn btn-sm btn-default dropdown-toggle add-to-tree"
aria-label="Create new file or directory"
class="rounded border-0 d-flex ide-entry-dropdown-toggle"
@click.stop="openDropdown()"
>
<icon
:size="12"
name="plus"
css-classes="float-left"
name="ellipsis_v"
/>
<icon
:size="12"
name="arrow-down"
css-classes="float-left"
/>
</button>
<ul
ref="dropdownMenu"
class="dropdown-menu dropdown-menu-right"
>
<template v-if="type === 'tree'">
<li>
<item-button
:label="__('New file')"
class="d-flex"
icon="doc-new"
icon-classes="mr-2"
@click="createNewItem('blob')"
/>
</li>
<li>
<upload
:path="path"
@create="createTempEntry"
/>
</li>
<li>
<item-button
:label="__('New directory')"
class="d-flex"
icon="folder-new"
icon-classes="mr-2"
@click="createNewItem($options.modalTypes.tree)"
/>
</li>
<li class="divider"></li>
</template>
<li>
<a
href="#"
role="button"
@click.stop.prevent="createNewItem('blob')"
>
{{ __('New file') }}
</a>
</li>
<li>
<upload
:branch-id="branch"
:path="path"
@create="createTempEntry"
<item-button
:label="__('Rename')"
class="d-flex"
icon="pencil"
icon-classes="mr-2"
@click="createNewItem($options.modalTypes.rename)"
/>
</li>
<li>
<a
href="#"
role="button"
@click.stop.prevent="createNewItem('tree')"
>
{{ __('New directory') }}
</a>
<item-button
:label="__('Delete')"
class="d-flex"
icon="remove"
icon-classes="mr-2"
@click="deleteEntry(path)"
/>
</li>
</ul>
</div>
<new-modal
v-if="openModal"
:type="modalType"
:branch-id="branch"
:path="path"
@hide="hideModal"
@create="createTempEntry"
/>
</div>
</template>

View file

@ -1,80 +1,90 @@
<script>
import { __ } from '~/locale';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { mapActions, mapState } from 'vuex';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { modalTypes } from '../../constants';
export default {
components: {
DeprecatedModal,
},
props: {
branchId: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
path: {
type: String,
required: true,
},
GlModal,
},
data() {
return {
entryName: this.path !== '' ? `${this.path}/` : '',
name: '',
};
},
computed: {
...mapState(['entryModal']),
entryName: {
get() {
if (this.entryModal.type === modalTypes.rename) {
return this.name || this.entryModal.entry.name;
}
return this.name || (this.entryModal.path !== '' ? `${this.entryModal.path}/` : '');
},
set(val) {
this.name = val;
},
},
modalTitle() {
if (this.type === 'tree') {
if (this.entryModal.type === modalTypes.tree) {
return __('Create new directory');
} else if (this.entryModal.type === modalTypes.rename) {
return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
}
return __('Create new file');
},
buttonLabel() {
if (this.type === 'tree') {
if (this.entryModal.type === modalTypes.tree) {
return __('Create directory');
} else if (this.entryModal.type === modalTypes.rename) {
return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
}
return __('Create file');
},
},
mounted() {
this.$refs.fieldName.focus();
},
methods: {
createEntryInStore() {
this.$emit('create', {
branchId: this.branchId,
name: this.entryName,
type: this.type,
});
this.hideModal();
...mapActions(['createTempEntry', 'renameEntry']),
submitForm() {
if (this.entryModal.type === modalTypes.rename) {
this.renameEntry({
path: this.entryModal.entry.path,
name: this.entryName,
});
} else {
this.createTempEntry({
name: this.name,
type: this.entryModal.type,
});
}
},
hideModal() {
this.$emit('hide');
focusInput() {
this.$refs.fieldName.focus();
},
closedModal() {
this.name = '';
},
},
};
</script>
<template>
<deprecated-modal
:title="modalTitle"
:primary-button-label="buttonLabel"
kind="success"
@cancel="hideModal"
@submit="createEntryInStore"
<gl-modal
id="ide-new-entry"
:header-title-text="modalTitle"
:footer-primary-button-text="buttonLabel"
footer-primary-button-variant="success"
@submit="submitForm"
@open="focusInput"
@closed="closedModal"
>
<form
slot="body"
<div
class="form-group row"
@submit.prevent="createEntryInStore"
>
<label class="label-light col-form-label col-sm-3">
<label class="label-bold col-form-label col-sm-3">
{{ __('Name') }}
</label>
<div class="col-sm-9">
@ -85,6 +95,6 @@ export default {
class="form-control"
/>
</div>
</form>
</deprecated-modal>
</div>
</gl-modal>
</template>

View file

@ -1,71 +1,85 @@
<script>
export default {
props: {
branchId: {
type: String,
required: true,
},
path: {
type: String,
required: false,
default: '',
},
},
mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile);
},
beforeDestroy() {
this.$refs.fileUpload.removeEventListener('change', this.openFile);
},
methods: {
createFile(target, file, isText) {
const { name } = file;
let { result } = target;
import Icon from '~/vue_shared/components/icon.vue';
import ItemButton from './button.vue';
if (!isText) {
// eslint-disable-next-line prefer-destructuring
result = result.split('base64,')[1];
}
this.$emit('create', {
name: `${(this.path ? `${this.path}/` : '')}${name}`,
branchId: this.branchId,
type: 'blob',
content: result,
base64: !isText,
});
},
readFile(file) {
const reader = new FileReader();
const isText = file.type.match(/text.*/) !== null;
reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true });
if (isText) {
reader.readAsText(file);
} else {
reader.readAsDataURL(file);
}
},
openFile() {
Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
},
startFileUpload() {
this.$refs.fileUpload.click();
},
export default {
components: {
Icon,
ItemButton,
},
props: {
path: {
type: String,
required: false,
default: '',
},
};
showLabel: {
type: Boolean,
required: false,
default: true,
},
buttonCssClasses: {
type: String,
required: false,
default: null,
},
},
mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile);
},
beforeDestroy() {
this.$refs.fileUpload.removeEventListener('change', this.openFile);
},
methods: {
createFile(target, file, isText) {
const { name } = file;
let { result } = target;
if (!isText) {
// eslint-disable-next-line prefer-destructuring
result = result.split('base64,')[1];
}
this.$emit('create', {
name: `${this.path ? `${this.path}/` : ''}${name}`,
type: 'blob',
content: result,
base64: !isText,
});
},
readFile(file) {
const reader = new FileReader();
const isText = file.type.match(/text.*/) !== null;
reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true });
if (isText) {
reader.readAsText(file);
} else {
reader.readAsDataURL(file);
}
},
openFile() {
Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
},
startFileUpload() {
this.$refs.fileUpload.click();
},
},
};
</script>
<template>
<div>
<a
href="#"
role="button"
@click.stop.prevent="startFileUpload"
>
{{ __('Upload file') }}
</a>
<item-button
:class="buttonCssClasses"
:show-label="showLabel"
:icon-classes="showLabel ? 'mr-2' : ''"
:label="__('Upload file')"
class="d-flex"
icon="upload"
@click="startFileUpload"
/>
<input
id="file-upload"
ref="fileUpload"

View file

@ -1,5 +1,5 @@
<script>
import { mapActions, mapState } from 'vuex';
import { mapActions, mapState, mapGetters } from 'vuex';
import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
import { rightSidebarViews } from '../../constants';
@ -7,6 +7,7 @@ import PipelinesList from '../pipelines/list.vue';
import JobsDetail from '../jobs/detail.vue';
import MergeRequestInfo from '../merge_requests/info.vue';
import ResizablePanel from '../resizable_panel.vue';
import Clientside from '../preview/clientside.vue';
export default {
directives: {
@ -18,15 +19,20 @@ export default {
JobsDetail,
ResizablePanel,
MergeRequestInfo,
Clientside,
},
computed: {
...mapState(['rightPane', 'currentMergeRequestId']),
...mapState(['rightPane', 'currentMergeRequestId', 'clientsidePreviewEnabled']),
...mapGetters(['packageJson']),
pipelinesActive() {
return (
this.rightPane === rightSidebarViews.pipelines ||
this.rightPane === rightSidebarViews.jobsDetail
);
},
showLivePreview() {
return this.packageJson && this.clientsidePreviewEnabled;
},
},
methods: {
...mapActions(['setRightPane']),
@ -49,8 +55,9 @@ export default {
:collapsible="false"
:initial-width="350"
:min-size="350"
class="multi-file-commit-panel-inner"
:class="`ide-right-sidebar-${rightPane}`"
side="right"
class="multi-file-commit-panel-inner"
>
<component :is="rightPane" />
</resizable-panel>
@ -98,6 +105,26 @@ export default {
/>
</button>
</li>
<li v-if="showLivePreview">
<button
v-tooltip
:title="__('Live preview')"
:aria-label="__('Live preview')"
:class="{
active: rightPane === $options.rightSidebarViews.clientSidePreview
}"
data-container="body"
data-placement="left"
class="ide-sidebar-link is-right"
type="button"
@click="clickTab($event, $options.rightSidebarViews.clientSidePreview)"
>
<icon
:size="16"
name="live-preview"
/>
</button>
</li>
</ul>
</nav>
</div>

View file

@ -0,0 +1,186 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { Manager } from 'smooshpack';
import { listen } from 'codesandbox-api';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import Navigator from './navigator.vue';
import { packageJsonPath } from '../../constants';
import { createPathWithExt } from '../../utils';
export default {
components: {
LoadingIcon,
Navigator,
},
data() {
return {
manager: {},
loading: false,
sandpackReady: false,
};
},
computed: {
...mapState(['entries', 'promotionSvgPath', 'links']),
...mapGetters(['packageJson', 'currentProject']),
normalizedEntries() {
return Object.keys(this.entries).reduce((acc, path) => {
const file = this.entries[path];
if (file.type === 'tree' || !(file.raw || file.content)) return acc;
return {
...acc,
[`/${path}`]: {
code: file.content || file.raw,
},
};
}, {});
},
mainEntry() {
if (!this.packageJson.raw) return false;
const parsedPackage = JSON.parse(this.packageJson.raw);
return parsedPackage.main;
},
showPreview() {
return this.mainEntry && !this.loading;
},
showEmptyState() {
return !this.mainEntry && !this.loading;
},
showOpenInCodeSandbox() {
return this.currentProject && this.currentProject.visibility === 'public';
},
sandboxOpts() {
return {
files: { ...this.normalizedEntries },
entry: `/${this.mainEntry}`,
showOpenInCodeSandbox: this.showOpenInCodeSandbox,
};
},
},
watch: {
entries: {
deep: true,
handler: 'update',
},
},
mounted() {
this.loading = true;
return this.loadFileContent(packageJsonPath)
.then(() => {
this.loading = false;
})
.then(() => this.$nextTick())
.then(() => this.initPreview());
},
beforeDestroy() {
if (!_.isEmpty(this.manager)) {
this.manager.listener();
}
this.manager = {};
if (this.listener) {
this.listener();
}
clearTimeout(this.timeout);
this.timeout = null;
},
methods: {
...mapActions(['getFileData', 'getRawFileData']),
loadFileContent(path) {
return this.getFileData({ path, makeFileActive: false }).then(() =>
this.getRawFileData({ path }),
);
},
initPreview() {
if (!this.mainEntry) return null;
return this.loadFileContent(this.mainEntry)
.then(() => this.$nextTick())
.then(() => {
this.initManager('#ide-preview', this.sandboxOpts, {
fileResolver: {
isFile: p => Promise.resolve(!!this.entries[createPathWithExt(p)]),
readFile: p => this.loadFileContent(createPathWithExt(p)).then(content => content),
},
});
this.listener = listen(e => {
switch (e.type) {
case 'done':
this.sandpackReady = true;
break;
default:
break;
}
});
});
},
update() {
if (!this.sandpackReady) return;
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
if (_.isEmpty(this.manager)) {
this.initPreview();
return;
}
this.manager.updatePreview(this.sandboxOpts);
}, 250);
},
initManager(el, opts, resolver) {
this.manager = new Manager(el, opts, resolver);
},
},
};
</script>
<template>
<div class="preview h-100 w-100 d-flex flex-column">
<template v-if="showPreview">
<navigator
:manager="manager"
/>
<div id="ide-preview"></div>
</template>
<div
v-else-if="showEmptyState"
v-once
class="d-flex h-100 flex-column align-items-center justify-content-center svg-content"
>
<img
:src="promotionSvgPath"
:alt="s__('IDE|Live Preview')"
width="130"
height="100"
/>
<h3>
{{ s__('IDE|Live Preview') }}
</h3>
<p class="text-center">
{{ s__('IDE|Preview your web application using Web IDE client-side evaluation.') }}
</p>
<a
:href="links.webIDEHelpPagePath"
class="btn btn-primary"
target="_blank"
rel="noopener noreferrer"
>
{{ s__('IDE|Get started with Live Preview') }}
</a>
</div>
<loading-icon
v-else
size="2"
class="align-self-center mt-auto mb-auto"
/>
</div>
</template>

View file

@ -0,0 +1,147 @@
<script>
import { listen } from 'codesandbox-api';
import Icon from '~/vue_shared/components/icon.vue';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
export default {
components: {
Icon,
LoadingIcon,
},
props: {
manager: {
type: Object,
required: true,
},
},
data() {
return {
currentBrowsingIndex: null,
navigationStack: [],
forwardNavigationStack: [],
path: '',
loading: true,
};
},
computed: {
backButtonDisabled() {
return this.navigationStack.length <= 1;
},
forwardButtonDisabled() {
return !this.forwardNavigationStack.length;
},
},
mounted() {
this.listener = listen(e => {
switch (e.type) {
case 'urlchange':
this.onUrlChange(e);
break;
case 'done':
this.loading = false;
break;
default:
break;
}
});
},
beforeDestroy() {
this.listener();
},
methods: {
onUrlChange(e) {
const lastPath = this.path;
this.path = e.url.replace(this.manager.bundlerURL, '') || '/';
if (lastPath !== this.path) {
this.currentBrowsingIndex =
this.currentBrowsingIndex === null ? 0 : this.currentBrowsingIndex + 1;
this.navigationStack.push(this.path);
}
},
back() {
const lastPath = this.path;
this.visitPath(this.navigationStack[this.currentBrowsingIndex - 1]);
this.forwardNavigationStack.push(lastPath);
if (this.currentBrowsingIndex === 1) {
this.currentBrowsingIndex = null;
this.navigationStack = [];
}
},
forward() {
this.visitPath(this.forwardNavigationStack.splice(0, 1)[0]);
},
refresh() {
this.visitPath(this.path);
},
visitPath(path) {
this.manager.iframe.src = `${this.manager.bundlerURL}${path}`;
},
},
};
</script>
<template>
<header class="ide-preview-header d-flex align-items-center">
<button
:aria-label="s__('IDE|Back')"
:disabled="backButtonDisabled"
:class="{
'disabled-content': backButtonDisabled
}"
type="button"
class="ide-navigator-btn d-flex align-items-center d-transparent border-0 bg-transparent"
@click="back"
>
<icon
:size="24"
name="chevron-left"
class="m-auto"
/>
</button>
<button
:aria-label="s__('IDE|Back')"
:disabled="forwardButtonDisabled"
:class="{
'disabled-content': forwardButtonDisabled
}"
type="button"
class="ide-navigator-btn d-flex align-items-center d-transparent border-0 bg-transparent"
@click="forward"
>
<icon
:size="24"
name="chevron-right"
class="m-auto"
/>
</button>
<button
:aria-label="s__('IDE|Refresh preview')"
type="button"
class="ide-navigator-btn d-flex align-items-center d-transparent border-0 bg-transparent"
@click="refresh"
>
<icon
:size="18"
name="retry"
class="m-auto"
/>
</button>
<div class="position-relative w-100 prepend-left-4">
<input
:value="path || '/'"
type="text"
class="ide-navigator-location form-control bg-white"
readonly
/>
<loading-icon
v-if="loading"
class="position-absolute ide-preview-loading-icon"
/>
</div>
</header>
</template>

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