Merge branch 'master' into 10244-add-project-ci-cd-settings

This commit is contained in:
Dylan Griffith 2018-04-24 15:07:34 +10:00
commit 1b9c1ac3ad
834 changed files with 12746 additions and 5722 deletions

View File

@ -1,4 +1,4 @@
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-65.0-node-8.x-yarn-1.2-postgresql-9.6"
.dedicated-runner: &dedicated-runner
retry: 1
@ -75,7 +75,7 @@ stages:
.use-mysql: &use-mysql
services:
- mysql:latest
- mysql:5.7
- redis:alpine
.rails5-variables: &rails5-variables

View File

@ -0,0 +1,70 @@
<!--
# Read me first!
Create this issue under https://dev.gitlab.org/gitlab/gitlabhq
Set the title to: `[Security] Description of the original issue`
-->
### Prior to the security release
- [ ] Read the [security process for developers] if you are not familiar with it.
- [ ] Link to the original issue adding it to the [links section](#links)
- [ ] Run `scripts/security-harness` in the CE, EE, and/or Omnibus to prevent pushing to any remote besides `dev.gitlab.org`
- [ ] Create an MR targetting `org` `master`, prefixing your branch with `security-`
- [ ] Label your MR with the ~security label, prefix the title with `WIP: [master]`
- [ ] Add a link to the MR to the [links section](#links)
- [ ] Add a link to an EE MR if required
- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**.
- [ ] Assign the MR to a RM once is reviewed and ready to be merged. Check the [RM list] to see who to ping.
#### Backports
- [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases
- [ ] At this point, it might be easy to squash the commits from the MR into one
- You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [seckpick documentation]
- [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable)
- [ ] Create each MR targetting the security branch `security-X-Y`
- [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager.
[seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/process.md#secpick-script
#### Documentation and final details
- [ ] Check the topic on #security to see when the next release is going ot happen and add a link to the [links section](#links)
- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
### Summary
#### Links
| Description | Link |
| -------- | -------- |
| Original issue | #TODO |
| Security release issue | #TODO |
| `master` MR | !TODO |
| `master` MR (EE) | !TODO |
| `Backport X.Y` MR | !TODO |
| `Backport X.Y` MR | !TODO |
| `Backport X.Y` MR | !TODO |
| `Backport X.Y` MR (EE) | !TODO |
| `Backport X.Y` MR (EE) | !TODO |
| `Backport X.Y` MR (EE) | !TODO |
#### Details
| Description | Details | Further details|
| -------- | -------- | -------- |
| Versions affected | X.Y | |
| Upgrade notes | | |
| GitLab Settings updated | Yes/No| |
| Migration required | Yes/No | |
| Thanks | | |
[security process for developers]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md
[RM list]: https://about.gitlab.com/release-managers/
/label ~security

View File

@ -45,4 +45,4 @@ When removing columns, tables, indexes or other structures:
- [ ] [Squashed related commits together](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-qa` manual pipeline job)
- [ ] End-to-end tests pass (`package-and-qa` manual pipeline job)

View File

@ -143,7 +143,7 @@ Lint/MissingCopEnableDirective:
Lint/NestedPercentLiteral:
Exclude:
- 'lib/gitlab/git/repository.rb'
- 'spec/support/email_format_shared_examples.rb'
- 'spec/support/shared_examples/email_format_shared_examples.rb'
# Offense count: 1
Lint/ReturnInVoidContext:
@ -195,8 +195,8 @@ Naming/HeredocDelimiterCase:
- 'spec/lib/gitlab/diff/parser_spec.rb'
- 'spec/lib/json_web_token/rsa_token_spec.rb'
- 'spec/models/commit_spec.rb'
- 'spec/support/repo_helpers.rb'
- 'spec/support/seed_repo.rb'
- 'spec/support/helpers/repo_helpers.rb'
- 'spec/support/helpers/seed_repo.rb'
# Offense count: 112
# Configuration parameters: Blacklist.
@ -496,7 +496,7 @@ Style/EmptyLiteral:
- 'spec/lib/gitlab/request_context_spec.rb'
- 'spec/lib/gitlab/workhorse_spec.rb'
- 'spec/requests/api/jobs_spec.rb'
- 'spec/support/chat_slash_commands_shared_examples.rb'
- 'spec/support/shared_examples/chat_slash_commands_shared_examples.rb'
# Offense count: 102
# Cop supports --auto-correct.

View File

@ -2,6 +2,213 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.7.0 (2018-04-22)
### Security (6 changes, 2 of them are from the community)
- Fixed some SSRF vulnerabilities in services, hooks and integrations. !2337
- Update ruby-saml to 1.7.2 and omniauth-saml to 1.10.0. !17734 (Takuya Noguchi)
- Update rack-protection to 2.0.1. !17835 (Takuya Noguchi)
- Adds confidential notes channel for Slack/Mattermost.
- Fix XSS on diff view stored on filenames.
- Fix GitLab Auth0 integration signing in the wrong user.
### Fixed (65 changes, 20 of them are from the community)
- File uploads in remote storage now support project renaming. !4597
- Fixed bug in dropdown selector when selecting the same selection again. !14631 (bitsapien)
- Fixed group deletion linked to Mattermost. !16209 (Julien Millau)
- Create commit API and Web IDE obey LFS filters. !16718
- Set breadcrumb for admin/runners/show. !17431 (Takuya Noguchi)
- Enable restore rake task to handle nested storage directories. !17516 (Balasankar C)
- Fix hover style of dropdown items in the right sidebar. !17519
- Improve empty state for canceled job. !17646
- Fix generated URL when listing repoitories for import. !17692
- Use singular in the diff stats if only one line has been changed. !17697 (Jan Beckmann)
- Long instance urls do not overflow anymore during project creation. !17717
- Fix importing multiple assignees from GitLab export. !17718
- Correct copy text for the promote milestone and label modals. !17726
- Fix search results stripping last endline when parsing the results. !17777 (Jasper Maes)
- Add read-only banner to all pages. !17798
- Fix viewing diffs on old merge requests. !17805
- Fix forking to subgroup via API when namespace is given by name. !17815 (Jan Beckmann)
- Fix UI breakdown for Create merge request button. !17821 (Takuya Noguchi)
- Unify format for nested non-task lists. !17823 (Takuya Noguchi)
- UX re-design branch items with flexbox. !17832 (Takuya Noguchi)
- Use porcelain commit lookup method on CI::CreatePipelineService. !17911
- Update dashboard milestones breadcrumb link. !17933 (George Tsiolis)
- Deleting a MR you are assigned to should decrements counter. !17951 (m b)
- Update no repository placeholder. !17964 (George Tsiolis)
- Drop JSON response in Project Milestone along with avoiding error. !17977 (Takuya Noguchi)
- Fix personal access token clipboard button style. !17978 (Fabian Schneider)
- Avoid validation errors when running the Pages domain verification service. !17992
- Project creation will now raise an error if a service template is invalid. !18013
- Add better LDAP connection handling. !18039
- Fix autolinking URLs containing ampersands. !18045
- Fix exceptions raised when migrating pipeline stages in the background. !18076
- Always display Labels section in issuable sidebar, even when the project has no labels. !18081 (Branka Martinovic)
- Fixed gitlab:uploads:migrate task ignoring some uploads. !18082
- Fixed gitlab:uploads:migrate task failing for Groups' avatar. !18088
- Increase dropdown width in pipeline graph & center action icon. !18089
- Fix `JobsController#raw` endpoint can not read traces in database. !18101
- Fix `gitlab-rake gitlab:two_factor:disable_for_all_users`. !18154
- Adjust 404's for LegacyDiffNote discussion rendering. !18201
- Work around Prometheus Helm chart name changes to fix integration. !18206 (joshlambert)
- Prioritize weight over title when sorting charts. !18233
- Verify that deploy token has valid access when pulling container registry image. !18260
- Stop redirecting the page in pipeline main actions.
- Fixed IDE button opening the wrong URL in tree list.
- Ensure hooks run when a deploy key without a user pushes.
- Fix 404 in group boards when moving issue between lists.
- Display state indicator for issuable references in non-project scope (e.g. when referencing issuables from group scope).
- Add missing port to artifact links.
- Fix data race between ObjectStorage background_upload and Pages publishing.
- Fixes unresolved discussions rendering the error state instead of the diff.
- Don't show Jump to Discussion button on Issues.
- Fix bug rendering group icons when forking.
- Automatically cleanup stale worktrees and lock files upon a push.
- Use the GitLab version as part of the appearances cache key.
- Fix Firefox stealing formatting characters on issue notes.
- Include matching branches and tags in protected branches / tags count. (Jan Beckmann)
- Fix 500 error when a merge request from a fork has conflicts and has not yet been updated.
- Test if remote repository exists when importing wikis.
- Hide emoji popup after multiple spaces. (Jan Beckmann)
- Fix relative uri when "#" is in branch name. (Jan)
- Escape Markdown characters properly when using autocomplete.
- Ignore project internal references in group context.
- Fix finding wiki file when Gitaly is enabled.
- Fix listing commit branch/tags that contain special characters.
- Ensure internal users (ghost, support bot) get assigned a namespace.
- Fix links to subdirectories of a directory with a plus character in its path.
### Deprecated (1 change)
- Remove support for legacy tar.gz pages artifacts. !18090
### Changed (22 changes, 2 of them are from the community)
- Add yellow favicon when `CANARY=true` to differientate canary environment. !12477
- Use human readable value build_timeout in Project. !17386
- Improved visual styles and consistency for commit hash and possible actions across commit lists. !17406
- Don't create permanent redirect routes. !17521
- Add empty repo check before running AutoDevOps pipeline. !17605
- Update wording to specify create/manage project vs group labels in labels dropdown. !17640
- Add tooltips to icons in lists of issues and merge requests. !17700
- Change avatar error message to include allowed file formats. !17747 (Fabian Schneider)
- Polish design for verifying domains. !17767
- Move email footer info to a single line. !17916
- Add average and maximum summary statistics to the prometheus dashboard. !17921
- Add additional cluster usage metrics to usage ping. !17922
- Move 'Registry' after 'CI/CD' in project navigation sidebar. !18018 (Elias Werberich)
- Redesign application settings to match project settings. !18019
- Allow HTTP(s) when git request is made by GitLab CI. !18021
- Added hover background color to IDE file list rows.
- Make project avatar in IDE consistent with the rest of GitLab.
- Show issues of subgroups in group-level issue board.
- Repository checksum calculation is handled by Gitaly when feature is enabled.
- Allow viewing timings for AJAX requests in the performance bar.
- Fixes remove source branch checkbox being visible when user cannot remove the branch.
- Make /-/ delimiter optional for search endpoints.
### Performance (24 changes, 11 of them are from the community)
- Move AssigneeTitle vue component. !17397 (George Tsiolis)
- Move TimeTrackingCollapsedState vue component. !17399 (George Tsiolis)
- Move MemoryGraph and MemoryUsage vue components. !17533 (George Tsiolis)
- Move UnresolvedDiscussions vue component. !17538 (George Tsiolis)
- Move NothingToMerge vue component. !17544 (George Tsiolis)
- Move ShaMismatch vue component. !17546 (George Tsiolis)
- Stop caching highlighted diffs in Redis unnecessarily. !17746
- Add i18n and update specs for ShaMismatch vue component. !17870 (George Tsiolis)
- Update spec import path for vue mount component helper. !17880 (George Tsiolis)
- Move TimeTrackingComparisonPane vue component. !17931 (George Tsiolis)
- Improves the performance of projects list page. !17934
- Remove N+1 query for Noteable association. !17956
- Improve performance of loading issues with lots of references to merge requests. !17986
- Reuse root_ref_hash for performance on Branches. !17998 (Takuya Noguchi)
- Update asciidoctor-plantuml to 0.0.8. !18022 (Takuya Noguchi)
- Cache personal projects count. !18197
- Reduce complexity of issuable finder query. !18219
- Reduce number of queries when viewing a merge request.
- Free open file descriptors and libgit2 buffers in UpdatePagesService.
- Memoize Git::Repository#has_visible_content?.
- Require at least one filter when listing issues or merge requests on dashboard page.
- lazy load diffs on merge request discussions.
- Bulk deleting refs is handled by Gitaly by default.
- ListCommitsByOid is executed by Gitaly by default.
### Added (38 changes, 7 of them are from the community)
- Add HTTPS-only pages. !16273 (rfwatson)
- adds closed by informations in issue api. !17042 (haseebeqx)
- Projects and groups badges settings UI. !17114
- Add per-runner configured job timeout. !17221
- Add alternate archive route for simplified packaging. !17225
- Add support for pipeline variables expressions in only/except. !17316
- Add object storage support for LFS objects, CI artifacts, and uploads. !17358
- Added confirmation modal for changing username. !17405
- Implement foreground verification of CI artifacts. !17578
- Extend API for exporting a project with direct upload URL. !17686
- Move ci/lint under project's namespace. !17729
- Add Total CPU/Memory consumption metrics for Kubernetes. !17731
- Adds the option to the project export API to override the project description and display GitLab export description once imported. !17744
- Port direct upload of LFS artifacts from EE. !17752
- Adds support for OmniAuth JWT provider. !17774
- Display error message on job's tooltip if this one fails. !17782
- Add 'Assigned Issues' and 'Assigned Merge Requests' as dashboard view choices for users. !17860 (Elias Werberich)
- Extend API for importing a project export with overwrite support. !17883
- Create Deploy Tokens to allow permanent access to repository and registry. !17894
- Detect commit message trailers and link users properly to their accounts on Gitlab. !17919 (cousine)
- Adds cancel btn to new pages domain page. !18026 (Jacopo Beschi @jacopo-beschi)
- API: Add parameter merge_method to projects. !18031 (Jan Beckmann)
- Introduce simpler env vars for auto devops REPLICAS and CANARY_REPLICAS #41436. !18036
- Allow overriding params on project import through API. !18086
- Support LFS objects when importing/exporting GitLab project archives. !18115
- Store sha256 checksum of artifact metadata. !18149
- Limit the number of failed logins when using LDAP for authentication. !43525
- Allow assigning and filtering issuables by ancestor group labels.
- Include subgroup issues when searching for group issues using the API.
- Allow to store uploads by default on Object Storage.
- Add slash command for moving issues. (Adam Pahlevi)
- Render MR commit SHA instead "diffs" when viable.
- Send @mention notifications even if a user has explicitly unsubscribed from item.
- Add support for Sidekiq JSON logging.
- Add Gitaly call details to performance bar.
- Add support for patch link extension for commit links on GitLab Flavored Markdown.
- Allow feature gates to be removed through the API.
- Allow merge requests related to a commit to be found via API.
### Other (27 changes, 11 of them are from the community)
- Send notification emails when push to a merge request. !7610 (YarNayar)
- Rename modal.vue to deprecated_modal.vue. !17438
- Atomic generation of internal ids for issues. !17580
- Use object ID to prevent duplicate keys Vue warning on Issue Boards page during development. !17682
- Update foreman from 0.78.0 to 0.84.0. !17690 (Takuya Noguchi)
- Add realtime pipeline status for adding/viewing files. !17705
- Update documentation to reflect current minimum required versions of node and yarn. !17706
- Update knapsack to 1.16.0. !17735 (Takuya Noguchi)
- Update CI services documnetation. !17749
- Added i18n support for the prometheus memory widget. !17753
- Use specific names for filtered CI variable controller parameters. !17796
- Apply NestingDepth (level 5) (framework/dropdowns.scss). !17820 (Takuya Noguchi)
- Clean up selectors in framework/header.scss. !17822 (Takuya Noguchi)
- Bump `state_machines-activerecord` to 0.5.1. !17924 (blackst0ne)
- Increase the memory limits used in the unicorn killer. !17948
- Replace the spinach test with an rspec analog. !17950 (blackst0ne)
- Remove unused index from events table. !18014
- Make all workhorse gitaly calls opt-out, take 2. !18043
- Update brakeman 3.6.1 to 4.2.1. !18122 (Takuya Noguchi)
- Replace the `project/issues/labels.feature` spinach test with an rspec analog. !18126 (blackst0ne)
- Bump html-pipeline to 2.7.1. !18132 (@blackst0ne)
- Remove test_ci rake task. !18139 (Takuya Noguchi)
- Add documentation for Pipelines failure reasons. !18352
- Improve JIRA event descriptions.
- Add query counts to profiler output.
- Move Sidekiq exporter logs to log/sidekiq_exporter.log.
- Upgrade Gitaly to upgrade its charlock_holmes.
## 10.6.4 (2018-04-09)
### Fixed (8 changes, 1 of them is from the community)

View File

@ -26,7 +26,9 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#priority-labels-deliverable-stretch-next-patch-release)
- [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release)
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-priority-labels-p1-p2-p3-etc)
- [Severity labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-severity-labels-s1-s2-s3-etc)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design-ui-elements)
- [Issue tracker](#issue-tracker)
@ -126,7 +128,9 @@ 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, ~Edge, ~Platform, etc.
- Priority: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Priority: ~P1, ~P2, ~P3, ~P4
- Severity: ~S1, ~S2, ~S3, ~S4
All labels, their meaning and priority are defined on the
[labels page][labels-page].
@ -185,10 +189,10 @@ indicate if an issue needs backend work, frontend work, or both.
Team labels are always capitalized so that they show up as the first label for
any issue.
### Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")
### Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")
Priority labels help us clearly communicate expectations of the work for the
release. There are two levels of priority labels:
Milestone labels help us clearly communicate expectations of the work for the
release. There are three levels of Milestone labels:
- ~Deliverable: Issues that are expected to be delivered in the current
milestone.
@ -203,16 +207,46 @@ 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.
### Severity labels (~S1, ~S2, etc.)
### Bug Priority labels (~P1, ~P2, ~P3 & etc.)
Severity labels help us clearly communicate the impact of a ~bug on users.
Bug Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be. If there are multiple defects, the priority decides which defect has to be fixed immediately versus later.
This label documents the planned timeline & urgency which is used to measure against our actual SLA on delivering ~bug fixes.
| Label | Meaning | Example |
|-------|------------------------------------------|---------|
| ~S1 | Feature broken, no workaround | Unable to create an issue |
| ~S2 | Feature broken, workaround unacceptable | Can push commits, but only via the command line |
| ~S3 | Feature broken, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue |
| ~S4 | Cosmetic issue | Label colors are incorrect / not being displayed |
| 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 |
#### Specific Priority guidance
| Label | Availability / Performance |
|-------|--------------------------------------------------------------|
| ~P1 | |
| ~P2 | The issue is (almost) guaranteed to occur in the near future |
| ~P3 | The issue is likely to occur in the near future |
| ~P4 | The issue _may_ occur but it's not likely |
### Bug Severity labels (~S1, ~S2, ~S3 & etc.)
Severity labels help us clearly communicate the impact of a ~bug on users.
| Label | Meaning | Impact of the defect | 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. |
| ~S3 | Major Severity | Broken Feature, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue. |
| ~S4 | Low Severity | Functionality inconvenience or cosmetic issue | Label colors are incorrect / not being displayed. |
#### Specific Severity guidance
| Label | Security Impact |
|-------|-------------------------------------------------------------------|
| ~S1 | >50% customers impacted (possible company extinction level event) |
| ~S2 | Multiple customers impacted (but not apocalyptic) |
| ~S3 | A single customer impacted |
| ~S4 | No customer impact, or expected impact within 30 days |
### Label for community contributors (~"Accepting Merge Requests")

View File

@ -1 +1 @@
0.95.0
0.96.1

10
Gemfile
View File

@ -62,7 +62,7 @@ gem 'akismet', '~> 2.0'
# Two-factor authentication
gem 'devise-two-factor', '~> 3.0.0'
gem 'rqrcode-rails3', '~> 0.1.7'
gem 'attr_encrypted', '~> 3.0.0'
gem 'attr_encrypted', '~> 3.1.0'
gem 'u2f', '~> 0.2.1'
# GitLab Pages
@ -82,7 +82,7 @@ gem 'net-ldap'
# Git Wiki
# Required manually in config/initializers/gollum.rb to control load order
gem 'gitlab-gollum-lib', '~> 4.2'
gem 'gitlab-gollum-lib', '~> 4.2', require: false
gem 'gitlab-gollum-rugged_adapter', '~> 0.4.4', require: false
@ -140,7 +140,7 @@ gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.6'
gem 'asciidoctor-plantuml', '0.0.8'
gem 'rouge', '~> 2.0'
gem 'rouge', '~> 3.1'
gem 'truncato', '~> 0.7.9'
gem 'bootstrap_form', '~> 2.7.0'
gem 'nokogiri', '~> 1.8.2'
@ -415,7 +415,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.94.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.97.0', require: 'gitaly'
gem 'grpc', '~> 1.10.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
@ -434,5 +434,3 @@ gem 'grape_logging', '~> 1.7'
# Asset synchronization
gem 'asset_sync', '~> 2.2.0'
gem 'goldiloader', '~> 2.0'

View File

@ -66,7 +66,7 @@ GEM
unf
ast (2.4.0)
atomic (1.1.99)
attr_encrypted (3.0.3)
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
attr_required (1.0.0)
autoprefixer-rails (6.2.3)
@ -206,7 +206,7 @@ GEM
railties (>= 3.0.0)
faraday (0.12.2)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.11.0.1)
faraday_middleware (0.12.2)
faraday (>= 0.7.4, < 1.0)
faraday_middleware-multi_json (0.0.6)
faraday_middleware
@ -290,9 +290,9 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.94.0)
gitaly-proto (0.97.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
grpc (~> 1.10)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
@ -303,12 +303,12 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
gitlab-gollum-lib (4.2.7.1)
gitlab-gollum-lib (4.2.7.2)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 2.1)
rouge (~> 3.1)
sanitize (~> 2.1)
stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4)
@ -331,9 +331,6 @@ GEM
rubyntlm (~> 0.5)
globalid (0.4.1)
activesupport (>= 4.2.0)
goldiloader (2.0.1)
activerecord (>= 4.2, < 5.2)
activesupport (>= 4.2, < 5.2)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
gon (6.1.0)
@ -590,7 +587,7 @@ GEM
orm_adapter (0.5.0)
os (0.9.6)
parallel (1.12.1)
parser (2.5.0.5)
parser (2.5.1.0)
ast (~> 2.4.0)
parslet (1.5.0)
blankslate (~> 2.0)
@ -750,7 +747,7 @@ GEM
retriable (3.1.1)
rinku (2.0.0)
rotp (2.1.2)
rouge (2.2.1)
rouge (3.1.1)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
@ -1004,7 +1001,7 @@ DEPENDENCIES
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.2.0)
attr_encrypted (~> 3.0.0)
attr_encrypted (~> 3.1.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
base32 (~> 0.3.0)
@ -1064,7 +1061,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.94.0)
gitaly-proto (~> 0.97.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
@ -1072,7 +1069,6 @@ DEPENDENCIES
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
goldiloader (~> 2.0)
gon (~> 6.1.0)
google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)
@ -1164,7 +1160,7 @@ DEPENDENCIES
redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
rouge (~> 2.0)
rouge (~> 3.1)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 3.6.0)

View File

@ -69,7 +69,7 @@ GEM
unf
ast (2.4.0)
atomic (1.1.100)
attr_encrypted (3.0.3)
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
attr_required (1.0.1)
autoprefixer-rails (8.1.0.1)
@ -291,9 +291,9 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.94.0)
gitaly-proto (0.97.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
grpc (~> 1.10)
github-linguist (5.3.3)
charlock_holmes (~> 0.7.5)
escape_utils (~> 1.1.0)
@ -304,6 +304,17 @@ GEM
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
multi_json
gitlab-gollum-lib (4.2.7.1)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 2.1)
sanitize (~> 2.1)
stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4)
mime-types (>= 1.15)
rugged (~> 0.25)
gitlab-grit (2.8.2)
charlock_holmes (~> 0.6)
diff-lcs (~> 1.1)
@ -321,22 +332,8 @@ GEM
rubyntlm (~> 0.5)
globalid (0.4.1)
activesupport (>= 4.2.0)
goldiloader (2.0.1)
activerecord (>= 4.2, < 5.2)
activesupport (>= 4.2, < 5.2)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
gollum-lib (4.2.7)
gemojione (~> 3.2)
github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0)
rouge (~> 2.1)
sanitize (~> 2.1)
stringex (~> 2.6)
gollum-rugged_adapter (0.4.4)
mime-types (>= 1.15)
rugged (~> 0.25)
gon (6.1.0)
actionpack (>= 3.0)
json
@ -1009,7 +1006,7 @@ DEPENDENCIES
asciidoctor (~> 1.5.6)
asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.2.0)
attr_encrypted (~> 3.0.0)
attr_encrypted (~> 3.1.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
base32 (~> 0.3.0)
@ -1069,15 +1066,14 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.94.0)
gitaly-proto (~> 0.97.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.2)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
goldiloader (~> 2.0)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)

26
LICENSE
View File

@ -1,25 +1,7 @@
Copyright (c) 2011-2017 GitLab B.V.
Copyright GitLab B.V.
With regard to the GitLab Software:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
For all third party components incorporated into the GitLab Software, those
components are licensed under the original license provided by the owner of the
applicable component.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -67,6 +67,12 @@ You can access a new installation with the login **`root`** and password **`5ive
GitLab is an open source project and we are very happy to accept community contributions. Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details.
## Licensing
GitLab Community Edition (CE) is available freely under the MIT Expat license.
All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component.
## Install a development environment
To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit).

View File

@ -113,6 +113,8 @@ class List {
issue.id = data.id;
issue.iid = data.iid;
issue.project = data.project;
issue.path = data.real_path;
issue.referencePath = data.reference_path;
if (this.issuesSize > 1) {
const moveBeforeId = this.issues[1].id;

View File

@ -16,6 +16,7 @@ class DeleteModal {
bindEvents() {
this.$toggleBtns.on('click', this.setModalData.bind(this));
this.$confirmInput.on('input', this.setDeleteDisabled.bind(this));
this.$deleteBtn.on('click', this.setDisableDeleteButton.bind(this));
}
setModalData(e) {
@ -30,6 +31,16 @@ class DeleteModal {
this.$deleteBtn.attr('disabled', e.currentTarget.value !== this.branchName);
}
setDisableDeleteButton(e) {
if (this.$deleteBtn.is('[disabled]')) {
e.preventDefault();
e.stopPropagation();
return false;
}
return true;
}
updateModal() {
this.$branchName.text(this.branchName);
this.$confirmInput.val('');

View File

@ -1,96 +1,102 @@
<script>
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 _ 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';
export default {
components: {
applicationRow,
clipboardButton,
export default {
components: {
applicationRow,
clipboardButton,
},
props: {
applications: {
type: Object,
required: false,
default: () => ({}),
},
props: {
applications: {
type: Object,
required: false,
default: () => ({}),
},
helpPath: {
type: String,
required: false,
default: '',
},
ingressHelpPath: {
type: String,
required: false,
default: '',
},
ingressDnsHelpPath: {
type: String,
required: false,
default: '',
},
managePrometheusPath: {
type: String,
required: false,
default: '',
},
helpPath: {
type: String,
required: false,
default: '',
},
computed: {
generalApplicationDescription() {
return sprintf(
_.escape(s__(
ingressHelpPath: {
type: String,
required: false,
default: '',
},
ingressDnsHelpPath: {
type: String,
required: false,
default: '',
},
managePrometheusPath: {
type: String,
required: false,
default: '',
},
},
computed: {
generalApplicationDescription() {
return sprintf(
_.escape(
s__(
`ClusterIntegration|Install applications on your Kubernetes cluster.
Read more about %{helpLink}`,
)), {
helpLink: `<a href="${this.helpPath}">
),
),
{
helpLink: `<a href="${this.helpPath}">
${_.escape(s__('ClusterIntegration|installing applications'))}
</a>`,
},
false,
);
},
ingressId() {
return INGRESS;
},
ingressInstalled() {
return this.applications.ingress.status === APPLICATION_INSTALLED;
},
ingressExternalIp() {
return this.applications.ingress.externalIp;
},
ingressDescription() {
const extraCostParagraph = sprintf(
_.escape(s__(
},
false,
);
},
ingressId() {
return INGRESS;
},
ingressInstalled() {
return this.applications.ingress.status === APPLICATION_INSTALLED;
},
ingressExternalIp() {
return this.applications.ingress.externalIp;
},
ingressDescription() {
const extraCostParagraph = sprintf(
_.escape(
s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources
like a load balancer, which may incur additional costs depending on
the hosting provider your Kubernetes cluster is installed on. If you are using GKE,
you can %{pricingLink}.`,
)), {
boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
the hosting provider your Kubernetes cluster is installed on. If you are using
Google Kubernetes Engine, you can %{pricingLink}.`,
),
),
{
boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|check the pricing here'))}</a>`,
},
false,
);
},
false,
);
const externalIpParagraph = sprintf(
_.escape(s__(
const externalIpParagraph = sprintf(
_.escape(
s__(
`ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS
at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}`,
)), {
ingressHelpLink: `<a href="${this.ingressHelpPath}">
),
),
{
ingressHelpLink: `<a href="${this.ingressHelpPath}">
${_.escape(s__('ClusterIntegration|More information'))}
</a>`,
},
false,
);
},
false,
);
return `
return `
<p>
${extraCostParagraph}
</p>
@ -98,22 +104,25 @@
${externalIpParagraph}
</p>
`;
},
prometheusDescription() {
return sprintf(
_.escape(s__(
},
prometheusDescription() {
return sprintf(
_.escape(
s__(
`ClusterIntegration|Prometheus is an open-source monitoring system
with %{gitlabIntegrationLink} to monitor deployed applications.`,
)), {
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
),
),
{
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|GitLab Integration'))}</a>`,
},
false,
);
},
},
false,
);
},
};
},
};
</script>
<template>
@ -205,7 +214,7 @@
>
{{ s__(`ClusterIntegration|The IP address is in
the process of being assigned. Please check your Kubernetes
cluster or Quotas on GKE if it takes a long time.`) }}
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }}
<a
:href="ingressHelpPath"

View File

@ -84,20 +84,21 @@ export default class CreateMergeRequestDropdown {
if (data.can_create_branch) {
this.available();
this.enable();
this.updateBranchName(data.suggested_branch_name);
if (!this.droplabInitialized) {
this.droplabInitialized = true;
this.initDroplab();
this.bindEvents();
}
} else if (data.has_related_branch) {
} else {
this.hide();
}
})
.catch(() => {
this.unavailable();
this.disable();
Flash('Failed to check if a new branch can be created.');
Flash(__('Failed to check related branches.'));
});
}
@ -409,13 +410,16 @@ export default class CreateMergeRequestDropdown {
this.unavailableButton.classList.remove('hide');
}
updateBranchName(suggestedBranchName) {
this.branchInput.value = suggestedBranchName;
this.updateCreatePaths('branch', suggestedBranchName);
}
updateInputState(target, ref, result) {
// target - 'branch' or 'ref' - which the input field we are searching a ref for.
// ref - string - what a user typed.
// result - string - what has been found on backend.
const pathReplacement = `$1${ref}`;
// If a found branch equals exact the same text a user typed,
// that means a new branch cannot be created as it already exists.
if (ref === result) {
@ -426,18 +430,12 @@ export default class CreateMergeRequestDropdown {
this.refIsValid = true;
this.refInput.dataset.value = ref;
this.showAvailableMessage('ref');
this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath,
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath,
pathReplacement);
this.updateCreatePaths(target, ref);
}
} else if (target === 'branch') {
this.branchIsValid = true;
this.showAvailableMessage('branch');
this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath,
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath,
pathReplacement);
this.updateCreatePaths(target, ref);
} else {
this.refIsValid = false;
this.refInput.dataset.value = ref;
@ -457,4 +455,15 @@ export default class CreateMergeRequestDropdown {
this.disableCreateAction();
}
}
// target - 'branch' or 'ref'
// ref - string - the new value to use as branch or ref
updateCreatePaths(target, ref) {
const pathReplacement = `$1${ref}`;
this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath,
pathReplacement);
this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath,
pathReplacement);
}
}

View File

@ -2,7 +2,9 @@
import $ from 'jquery';
import Pikaday from 'pikaday';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
class DueDateSelect {
@ -14,6 +16,7 @@ class DueDateSelect {
this.$dropdownParent = $dropdownParent;
this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
this.$block = $block;
this.$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
this.$selectbox = $dropdown.closest('.selectbox');
this.$value = $block.find('.value');
this.$valueContent = $block.find('.value-content');
@ -128,7 +131,8 @@ class DueDateSelect {
submitSelectedDate(isDropdown) {
const selectedDateValue = this.datePayload[this.abilityName].due_date;
const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
const hasDueDate = this.displayedDate !== 'No due date';
const displayedDateStyle = hasDueDate ? 'bold' : 'no-value';
this.$loading.removeClass('hidden').fadeIn();
@ -145,10 +149,13 @@ class DueDateSelect {
return axios.put(this.issueUpdateURL, this.datePayload)
.then(() => {
const tooltipText = hasDueDate ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})` : __('Due date');
if (isDropdown) {
this.$dropdown.trigger('loaded.gl.dropdown');
this.$dropdown.dropdown('toggle');
}
this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
return this.$loading.fadeOut();
});
}

View File

@ -1,31 +1,87 @@
<script>
import icon from '~/vue_shared/components/icon.vue';
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';
export default {
components: {
icon,
Icon,
},
directives: {
tooltip,
},
props: {
file: {
type: Object,
required: true,
},
showTooltip: {
type: Boolean,
required: false,
default: false,
},
showStagedIcon: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
changedIcon() {
return this.file.tempFile ? 'file-addition' : 'file-modified';
const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : '';
return this.file.tempFile ? `file-addition${suffix}` : `file-modified${suffix}`;
},
stagedIcon() {
return `${this.changedIcon}-solid`;
},
changedIconClass() {
return `multi-${this.changedIcon}`;
return `multi-${this.changedIcon} prepend-left-5 pull-left`;
},
tooltipTitle() {
if (!this.showTooltip) return undefined;
const type = this.file.tempFile ? 'addition' : 'modification';
if (this.file.changed && !this.file.staged) {
return sprintf(__('Unstaged %{type}'), {
type,
});
} else if (!this.file.changed && this.file.staged) {
return sprintf(__('Staged %{type}'), {
type,
});
} else if (this.file.changed && this.file.staged) {
return sprintf(__('Unstaged and staged %{type}'), {
type: pluralize(type),
});
}
return undefined;
},
},
};
</script>
<template>
<icon
:name="changedIcon"
:size="12"
:css-classes="`ide-file-changed-icon ${changedIconClass}`"
/>
<span
v-tooltip
:title="tooltipTitle"
data-container="body"
data-placement="right"
class="ide-file-changed-icon"
>
<icon
v-if="file.staged && showStagedIcon"
:name="stagedIcon"
:size="12"
:css-classes="changedIconClass"
/>
<icon
v-if="file.changed || file.tempFile || (file.staged && !showStagedIcon)"
:name="changedIcon"
:size="12"
:css-classes="changedIconClass"
/>
</span>
</template>

View File

@ -1,41 +1,27 @@
<script>
import { mapState } from 'vuex';
import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
import { mapState } from 'vuex';
import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
export default {
components: {
RadioGroup,
export default {
components: {
RadioGroup,
},
computed: {
...mapState(['currentBranchId']),
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
{ branchName: `<strong class="monospace">${this.currentBranchId}</strong>` },
false,
);
},
computed: {
...mapState([
'currentBranchId',
]),
newMergeRequestHelpText() {
return sprintf(
__('Creates a new branch from %{branchName} and re-directs to create a new merge request'),
{ branchName: this.currentBranchId },
);
},
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
{ branchName: `<strong>${this.currentBranchId}</strong>` },
false,
);
},
commitToNewBranchText() {
return sprintf(
__('Creates a new branch from %{branchName}'),
{ branchName: this.currentBranchId },
);
},
},
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
};
},
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
};
</script>
<template>
@ -53,13 +39,11 @@
:value="$options.commitToNewBranch"
:label="__('Create a new branch')"
:show-input="true"
:help-text="commitToNewBranchText"
/>
<radio-group
:value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')"
:show-input="true"
:help-text="newMergeRequestHelpText"
/>
</div>
</template>

View File

@ -0,0 +1,93 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState(['lastCommitMsg', 'rightPanelCollapsed']),
...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
statusSvg() {
return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
},
},
methods: {
...mapActions(['toggleRightPanelCollapsed']),
},
};
</script>
<template>
<div
class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state"
>
<header
class="multi-file-commit-panel-header"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<button
v-tooltip
:title="collapseButtonTooltip"
data-container="body"
data-placement="left"
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
:aria-label="__('Toggle sidebar')"
@click.stop="toggleRightPanelCollapsed"
>
<icon
:name="collapseButtonIcon"
:size="18"
/>
</button>
</header>
<div
class="ide-commit-empty-state-container"
v-if="!rightPanelCollapsed"
>
<div class="svg-content svg-80">
<img :src="statusSvg" />
</div>
<div class="append-right-default prepend-left-default">
<div
class="text-content text-center"
v-if="!lastCommitMsg"
>
<h4>
{{ __('No changes') }}
</h4>
<p>
{{ __('Edit files in the editor and commit changes here') }}
</p>
</div>
<div
class="text-content text-center"
v-else
>
<h4>
{{ __('All changes are committed') }}
</h4>
<p v-html="lastCommitMsg"></p>
</div>
</div>
</div>
</div>
</template>

View File

@ -1,56 +1,132 @@
<script>
import { mapState } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import listItem from './list_item.vue';
import listCollapsed from './list_collapsed.vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import ListItem from './list_item.vue';
import ListCollapsed from './list_collapsed.vue';
export default {
components: {
icon,
listItem,
listCollapsed,
export default {
components: {
Icon,
ListItem,
ListCollapsed,
},
directives: {
tooltip,
},
props: {
title: {
type: String,
required: true,
},
props: {
title: {
type: String,
required: true,
},
fileList: {
type: Array,
required: true,
},
fileList: {
type: Array,
required: true,
},
computed: {
...mapState([
'currentProjectId',
'currentBranchId',
'rightPanelCollapsed',
]),
isCommitInfoShown() {
return this.rightPanelCollapsed || this.fileList.length;
},
showToggle: {
type: Boolean,
required: false,
default: true,
},
methods: {
toggleCollapsed() {
this.$emit('toggleCollapsed');
},
iconName: {
type: String,
required: true,
},
};
action: {
type: String,
required: true,
},
actionBtnText: {
type: String,
required: true,
},
itemActionComponent: {
type: String,
required: true,
},
stagedList: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState(['rightPanelCollapsed']),
...mapGetters(['collapseButtonIcon', 'collapseButtonTooltip']),
titleText() {
return sprintf(__('%{title} changes'), {
title: this.title,
});
},
},
methods: {
...mapActions(['toggleRightPanelCollapsed', 'stageAllChanges', 'unstageAllChanges']),
actionBtnClicked() {
this[this.action]();
},
},
};
</script>
<template>
<div
class="ide-commit-list-container"
:class="{
'multi-file-commit-list': isCommitInfoShown
'is-collapsed': rightPanelCollapsed,
}"
>
<header
class="multi-file-commit-panel-header"
>
<div
v-if="!rightPanelCollapsed"
class="multi-file-commit-panel-header-title"
:class="{
'append-right-10': showToggle,
}"
>
<icon
v-once
:name="iconName"
:size="18"
/>
{{ titleText }}
<button
type="button"
class="btn btn-blank btn-link ide-staged-action-btn"
@click="actionBtnClicked"
>
{{ actionBtnText }}
</button>
</div>
<button
v-if="showToggle"
v-tooltip
:title="collapseButtonTooltip"
data-container="body"
data-placement="left"
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
:aria-label="__('Toggle sidebar')"
@click.stop="toggleRightPanelCollapsed"
>
<icon
:name="collapseButtonIcon"
:size="18"
/>
</button>
</header>
<list-collapsed
v-if="rightPanelCollapsed"
:files="fileList"
:icon-name="iconName"
:title="title"
/>
<template v-else>
<ul
v-if="fileList.length"
class="list-unstyled append-bottom-0"
class="multi-file-commit-list list-unstyled append-bottom-0"
>
<li
v-for="file in fileList"
@ -58,9 +134,18 @@
>
<list-item
:file="file"
:action-component="itemActionComponent"
:key-prefix="title"
:staged-list="stagedList"
/>
</li>
</ul>
<p
v-else
class="multi-file-commit-list help-block"
>
{{ __('No changes') }}
</p>
</template>
</div>
</template>

View File

@ -1,35 +1,110 @@
<script>
import { mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { sprintf, n__, __ } from '~/locale';
export default {
components: {
icon,
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
files: {
type: Array,
required: true,
},
computed: {
...mapGetters([
'addedFiles',
'modifiedFiles',
]),
iconName: {
type: String,
required: true,
},
};
title: {
type: String,
required: true,
},
},
computed: {
addedFilesLength() {
return this.files.filter(f => f.tempFile).length;
},
modifiedFilesLength() {
return this.files.filter(f => !f.tempFile).length;
},
addedFilesIconClass() {
return this.addedFilesLength ? 'multi-file-addition' : '';
},
modifiedFilesClass() {
return this.modifiedFilesLength ? 'multi-file-modified' : '';
},
additionsTooltip() {
return sprintf(n__('1 %{type} addition', '%d %{type} additions', this.addedFilesLength), {
type: this.title.toLowerCase(),
});
},
modifiedTooltip() {
return sprintf(
n__('1 %{type} modification', '%d %{type} modifications', this.modifiedFilesLength),
{ type: this.title.toLowerCase() },
);
},
titleTooltip() {
return sprintf(__('%{title} changes'), { title: this.title });
},
additionIconName() {
return this.title.toLowerCase() === 'staged' ? 'file-addition-solid' : 'file-addition';
},
modifiedIconName() {
return this.title.toLowerCase() === 'staged' ? 'file-modified-solid' : 'file-modified';
},
},
};
</script>
<template>
<div
class="multi-file-commit-list-collapsed text-center"
>
<icon
name="file-addition"
:size="18"
css-classes="multi-file-addition append-bottom-10"
/>
{{ addedFiles.length }}
<icon
name="file-modified"
:size="18"
css-classes="multi-file-modified prepend-top-10 append-bottom-10"
/>
{{ modifiedFiles.length }}
<div
v-tooltip
:title="titleTooltip"
data-container="body"
data-placement="left"
class="append-bottom-15"
>
<icon
v-once
:name="iconName"
:size="18"
/>
</div>
<div
v-tooltip
:title="additionsTooltip"
data-container="body"
data-placement="left"
class="append-bottom-10"
>
<icon
:name="additionIconName"
:size="18"
:css-classes="addedFilesIconClass"
/>
</div>
{{ addedFilesLength }}
<div
v-tooltip
:title="modifiedTooltip"
data-container="body"
data-placement="left"
class="prepend-top-10 append-bottom-10"
>
<icon
:name="modifiedIconName"
:size="18"
:css-classes="modifiedFilesClass"
/>
</div>
{{ modifiedFilesLength }}
</div>
</template>

View File

@ -1,34 +1,69 @@
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue';
export default {
components: {
Icon,
StageButton,
UnstageButton,
},
props: {
file: {
type: Object,
required: true,
},
actionComponent: {
type: String,
required: true,
},
keyPrefix: {
type: String,
required: false,
default: '',
},
stagedList: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
iconName() {
return this.file.tempFile ? 'file-addition' : 'file-modified';
const prefix = this.stagedList ? '-solid' : '';
return this.file.tempFile ? `file-addition${prefix}` : `file-modified${prefix}`;
},
iconClass() {
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
return `multi-file-${this.file.tempFile ? 'additions' : 'modified'} append-right-8`;
},
},
methods: {
...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']),
openFileInEditor(file) {
return this.openPendingTab(file).then(changeViewer => {
...mapActions([
'discardFileChanges',
'updateViewer',
'openPendingTab',
'unstageChange',
'stageChange',
]),
openFileInEditor() {
return this.openPendingTab({
file: this.file,
keyPrefix: this.keyPrefix.toLowerCase(),
}).then(changeViewer => {
if (changeViewer) {
this.updateViewer('diff');
}
});
},
fileAction() {
if (this.file.staged) {
this.unstageChange(this.file.path);
} else {
this.stageChange(this.file.path);
}
},
},
};
</script>
@ -38,7 +73,9 @@ export default {
<button
type="button"
class="multi-file-commit-list-path"
@click="openFileInEditor(file)">
@dblclick="fileAction"
@click="openFileInEditor"
>
<span class="multi-file-commit-list-file-path">
<icon
:name="iconName"
@ -47,12 +84,9 @@ export default {
/>{{ file.path }}
</span>
</button>
<button
type="button"
class="btn btn-blank multi-file-discard-btn"
@click="discardFileChanges(file.path)"
>
Discard
</button>
<component
:is="actionComponent"
:path="file.path"
/>
</div>
</template>

View File

@ -0,0 +1,130 @@
<script>
import { __, sprintf } from '../../../locale';
import Icon from '../../../vue_shared/components/icon.vue';
import popover from '../../../vue_shared/directives/popover';
import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants';
export default {
directives: {
popover,
},
components: {
Icon,
},
props: {
text: {
type: String,
required: true,
},
},
data() {
return {
scrollTop: 0,
isFocused: false,
};
},
computed: {
allLines() {
return this.text.split('\n').map((line, i) => ({
text: line.substr(0, this.getLineLength(i)) || ' ',
highlightedText: line.substr(this.getLineLength(i)),
}));
},
},
methods: {
handleScroll() {
if (this.$refs.textarea) {
this.$nextTick(() => {
this.scrollTop = this.$refs.textarea.scrollTop;
});
}
},
getLineLength(i) {
return i === 0 ? MAX_TITLE_LENGTH : MAX_BODY_LENGTH;
},
onInput(e) {
this.$emit('input', e.target.value);
},
updateIsFocused(isFocused) {
this.isFocused = isFocused;
},
},
popoverOptions: {
trigger: 'hover',
placement: 'top',
content: sprintf(
__(`
The character highligher helps you keep the subject line to %{titleLength} characters
and wrap the body at %{bodyLength} so they are readable in git.
`),
{ titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH },
),
},
};
</script>
<template>
<fieldset class="common-note-form ide-commit-message-field">
<div
class="md-area"
:class="{
'is-focused': isFocused
}"
>
<div
v-once
class="md-header"
>
<ul class="nav-links">
<li>
{{ __('Commit Message') }}
<span
v-popover="$options.popoverOptions"
class="help-block prepend-left-10"
>
<icon
name="question"
/>
</span>
</li>
</ul>
</div>
<div class="ide-commit-message-textarea-container">
<div class="ide-commit-message-highlights-container">
<div
class="note-textarea highlights monospace"
:style="{
transform: `translate3d(0, ${-scrollTop}px, 0)`
}"
>
<div
v-for="(line, index) in allLines"
:key="index"
>
<span
v-text="line.text"
>
</span><mark
v-show="line.highlightedText"
v-text="line.highlightedText"
>
</mark>
</div>
</div>
</div>
<textarea
class="note-textarea ide-commit-message-textarea"
name="commit-message"
:placeholder="__('Write a commit message...')"
:value="text"
@scroll="handleScroll"
@input="onInput"
@focus="updateIsFocused(true)"
@blur="updateIsFocused(false)"
ref="textarea"
>
</textarea>
</div>
</div>
</fieldset>
</template>

View File

@ -1,52 +1,40 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import { mapActions, mapState, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
export default {
directives: {
tooltip,
},
props: {
value: {
type: String,
required: true,
},
props: {
value: {
type: String,
required: true,
},
label: {
type: String,
required: false,
default: null,
},
checked: {
type: Boolean,
required: false,
default: false,
},
showInput: {
type: Boolean,
required: false,
default: false,
},
helpText: {
type: String,
required: false,
default: null,
},
label: {
type: String,
required: false,
default: null,
},
computed: {
...mapState('commit', [
'commitAction',
]),
...mapGetters('commit', [
'newBranchName',
]),
checked: {
type: Boolean,
required: false,
default: false,
},
methods: {
...mapActions('commit', [
'updateCommitAction',
'updateBranchName',
]),
showInput: {
type: Boolean,
required: false,
default: false,
},
};
},
computed: {
...mapState('commit', ['commitAction']),
...mapGetters('commit', ['newBranchName']),
},
methods: {
...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
},
};
</script>
<template>
@ -65,18 +53,6 @@
{{ label }}
</template>
<slot v-else></slot>
<span
v-if="helpText"
v-tooltip
class="help-block inline"
:title="helpText"
>
<i
class="fa fa-question-circle"
aria-hidden="true"
>
</i>
</span>
</span>
</label>
<div
@ -85,7 +61,7 @@
>
<input
type="text"
class="form-control"
class="form-control monospace"
:placeholder="newBranchName"
@input="updateBranchName($event.target.value)"
/>

View File

@ -0,0 +1,59 @@
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
path: {
type: String,
required: true,
},
},
methods: {
...mapActions(['stageChange', 'discardFileChanges']),
},
};
</script>
<template>
<div
v-once
class="multi-file-discard-btn"
>
<button
v-tooltip
type="button"
class="btn btn-blank append-right-5"
:aria-label="__('Stage changes')"
:title="__('Stage changes')"
data-container="body"
@click.stop="stageChange(path)"
>
<icon
name="mobile-issue-close"
:size="12"
/>
</button>
<button
v-tooltip
type="button"
class="btn btn-blank"
:aria-label="__('Discard changes')"
:title="__('Discard changes')"
data-container="body"
@click.stop="discardFileChanges(path)"
>
<icon
name="remove"
:size="12"
/>
</button>
</div>
</template>

View File

@ -0,0 +1,45 @@
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
path: {
type: String,
required: true,
},
},
methods: {
...mapActions(['unstageChange']),
},
};
</script>
<template>
<div
v-once
class="multi-file-discard-btn"
>
<button
v-tooltip
type="button"
class="btn btn-blank"
:aria-label="__('Unstage changes')"
:title="__('Unstage changes')"
data-container="body"
@click="unstageChange(path)"
>
<icon
name="history"
:size="12"
/>
</button>
</div>
</template>

View File

@ -1,5 +1,4 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import icon from '~/vue_shared/components/icon.vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
import repoCommitSection from './repo_commit_section.vue';
@ -22,13 +21,6 @@ export default {
required: true,
},
},
computed: {
...mapState(['changedFiles', 'rightPanelCollapsed']),
...mapGetters(['currentIcon']),
},
methods: {
...mapActions(['setPanelCollapsedStatus']),
},
};
</script>
@ -41,40 +33,6 @@ export default {
<div
class="multi-file-commit-panel-section"
>
<header
class="multi-file-commit-panel-header"
:class="{
'is-collapsed': rightPanelCollapsed,
}"
>
<div
class="multi-file-commit-panel-header-title"
v-if="!rightPanelCollapsed"
>
<div
v-if="changedFiles.length"
>
<icon
name="list-bulleted"
:size="18"
/>
Staged
</div>
</div>
<button
type="button"
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
@click.stop="setPanelCollapsedStatus({
side: 'right',
collapsed: !rightPanelCollapsed,
})"
>
<icon
:name="currentIcon"
:size="18"
/>
</button>
</header>
<repo-commit-section
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"

View File

@ -22,13 +22,6 @@ export default {
<template>
<div class="ide-status-bar">
<div class="ref-name">
<icon
name="branch"
:size="12"
/>
{{ file.branchId }}
</div>
<div>
<div v-if="file.lastCommit && file.lastCommit.id">
Last commit:

View File

@ -1,20 +1,24 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import commitFilesList from './commit_sidebar/list.vue';
import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue';
import CommitMessageField from './commit_sidebar/message_field.vue';
import * as consts from '../stores/modules/commit/constants';
import Actions from './commit_sidebar/actions.vue';
export default {
components: {
DeprecatedModal,
icon,
commitFilesList,
Icon,
CommitFilesList,
EmptyState,
Actions,
LoadingButton,
CommitMessageField,
},
directives: {
tooltip,
@ -30,43 +34,19 @@ export default {
},
},
computed: {
...mapState([
'currentProjectId',
'currentBranchId',
'rightPanelCollapsed',
'lastCommitMsg',
'changedFiles',
]),
...mapState(['changedFiles', 'stagedFiles', 'rightPanelCollapsed']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters('commit', [
'commitButtonDisabled',
'discardDraftButtonDisabled',
'branchName',
]),
statusSvg() {
return this.lastCommitMsg
? this.committedStateSvgPath
: this.noChangesStateSvgPath;
},
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled', 'branchName']),
},
methods: {
...mapActions(['setPanelCollapsedStatus']),
...mapActions('commit', [
'updateCommitMessage',
'discardDraft',
'commitChanges',
'updateCommitAction',
]),
toggleCollapsed() {
this.setPanelCollapsedStatus({
side: 'right',
collapsed: !this.rightPanelCollapsed,
});
},
forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() =>
this.commitChanges(),
);
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
},
},
};
@ -75,9 +55,6 @@ export default {
<template>
<div
class="multi-file-commit-panel-section"
:class="{
'multi-file-commit-empty-state-container': !changedFiles.length
}"
>
<deprecated-modal
id="ide-create-branch-modal"
@ -91,30 +68,36 @@ export default {
Would you like to create a new branch?`) }}
</template>
</deprecated-modal>
<commit-files-list
title="Staged"
:file-list="changedFiles"
:collapsed="rightPanelCollapsed"
@toggleCollapsed="toggleCollapsed"
/>
<template
v-if="changedFiles.length"
v-if="changedFiles.length || stagedFiles.length"
>
<commit-files-list
icon-name="unstaged"
:title="__('Unstaged')"
:file-list="changedFiles"
action="stageAllChanges"
:action-btn-text="__('Stage all')"
item-action-component="stage-button"
/>
<commit-files-list
icon-name="staged"
:title="__('Staged')"
:file-list="stagedFiles"
action="unstageAllChanges"
:action-btn-text="__('Unstage all')"
item-action-component="unstage-button"
:show-toggle="false"
:staged-list="true"
/>
<form
class="form-horizontal multi-file-commit-form"
@submit.prevent.stop="commitChanges"
v-if="!rightPanelCollapsed"
>
<div class="multi-file-commit-fieldset">
<textarea
class="form-control multi-file-commit-message"
name="commit-message"
:value="commitMessage"
:placeholder="__('Write a commit message...')"
@input="updateCommitMessage($event.target.value)"
>
</textarea>
</div>
<commit-message-field
:text="commitMessage"
@input="updateCommitMessage"
/>
<div class="clearfix prepend-top-15">
<actions />
<loading-button
@ -135,38 +118,10 @@ export default {
</div>
</form>
</template>
<div
v-else-if="!rightPanelCollapsed"
class="row js-empty-state"
>
<div class="col-xs-10 col-xs-offset-1">
<div class="svg-content svg-80">
<img :src="statusSvg" />
</div>
</div>
<div class="col-xs-10 col-xs-offset-1">
<div
class="text-content text-center"
v-if="!lastCommitMsg"
>
<h4>
{{ __('No changes') }}
</h4>
<p>
{{ __('Edit files in the editor and commit changes here') }}
</p>
</div>
<div
class="text-content text-center"
v-else
>
<h4>
{{ __('All changes are committed') }}
</h4>
<p v-html="lastCommitMsg">
</p>
</div>
</div>
</div>
<empty-state
v-else
:no-changes-state-svg-path="noChangesStateSvgPath"
:committed-state-svg-path="committedStateSvgPath"
/>
</div>
</template>

View File

@ -20,7 +20,7 @@ export default {
},
computed: {
...mapState(['rightPanelCollapsed', 'viewer', 'delayViewerUpdated', 'panelResizing']),
...mapGetters(['currentMergeRequest']),
...mapGetters(['currentMergeRequest', 'getStagedFile']),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.content;
},
@ -120,7 +120,12 @@ export default {
setupEditor() {
if (!this.file || !this.editor.instance) return;
this.model = this.editor.createModel(this.file);
const head = this.getStagedFile(this.file.path);
this.model = this.editor.createModel(
this.file,
this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null,
);
if (this.viewer === 'mrdiff') {
this.editor.attachMergeRequestModel(this.model);

View File

@ -102,8 +102,11 @@ export default {
v-if="file.mrChange"
/>
<changed-file-icon
v-if="file.changed || file.tempFile || file.staged"
:file="file"
v-if="file.changed || file.tempFile"
:show-tooltip="true"
:show-staged-icon="true"
class="prepend-top-5 pull-right"
/>
</span>
<new-dropdown

View File

@ -26,13 +26,16 @@ export default {
},
computed: {
closeLabel() {
if (this.tab.changed || this.tab.tempFile) {
if (this.fileHasChanged) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
showChangedIcon() {
return this.tab.changed ? !this.tabMouseOver : false;
return this.fileHasChanged ? !this.tabMouseOver : false;
},
fileHasChanged() {
return this.tab.changed || this.tab.tempFile || this.tab.staged;
},
},
@ -42,18 +45,18 @@ export default {
this.updateDelayViewerUpdated(true);
if (tab.pending) {
this.openPendingTab(tab);
this.openPendingTab({ file: tab, keyPrefix: tab.staged ? 'staged' : 'unstaged' });
} else {
this.$router.push(`/project${tab.url}`);
}
},
mouseOverTab() {
if (this.tab.changed) {
if (this.fileHasChanged) {
this.tabMouseOver = true;
}
},
mouseOutTab() {
if (this.tab.changed) {
if (this.fileHasChanged) {
this.tabMouseOver = false;
}
},

View File

@ -0,0 +1,3 @@
// Fuzzy file finder
export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72;

View File

@ -36,11 +36,11 @@ const router = new VueRouter({
base: `${gon.relative_url_root}/-/ide/`,
routes: [
{
path: '/project/:namespace/:project',
path: '/project/:namespace/:project+',
component: EmptyRouterComponent,
children: [
{
path: ':targetmode/:branch/*',
path: ':targetmode(edit|tree|blob)/:branch/*',
component: EmptyRouterComponent,
},
{

View File

@ -3,15 +3,16 @@ import Disposable from './disposable';
import eventHub from '../../eventhub';
export default class Model {
constructor(monaco, file) {
constructor(monaco, file, head = null) {
this.monaco = monaco;
this.disposable = new Disposable();
this.file = file;
this.head = head;
this.content = file.content !== '' ? file.content : file.raw;
this.disposable.add(
(this.originalModel = this.monaco.editor.createModel(
this.file.raw,
head ? head.content : this.file.raw,
undefined,
new this.monaco.Uri(null, null, `original/${this.file.key}`),
)),
@ -31,13 +32,15 @@ export default class Model {
);
}
this.events = new Map();
this.events = new Set();
this.updateContent = this.updateContent.bind(this);
this.updateNewContent = this.updateNewContent.bind(this);
this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.key}`, this.dispose);
eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
eventHub.$on(`editor.update.model.content.${this.file.key}`, this.updateContent);
eventHub.$on(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent);
}
get url() {
@ -73,22 +76,36 @@ export default class Model {
}
onChange(cb) {
this.events.set(
this.path,
this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))),
);
this.events.add(this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))));
}
updateContent(content) {
onDispose(cb) {
this.events.add(cb);
}
updateContent({ content, changed }) {
this.getOriginalModel().setValue(content);
if (!changed) {
this.getModel().setValue(content);
}
}
updateNewContent(content) {
this.getModel().setValue(content);
}
dispose() {
this.disposable.dispose();
this.events.forEach(cb => {
if (typeof cb === 'function') cb();
});
this.events.clear();
eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose);
eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
eventHub.$off(`editor.update.model.content.${this.file.key}`, this.updateContent);
eventHub.$off(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent);
}
}

View File

@ -17,12 +17,12 @@ export default class ModelManager {
return this.models.get(key);
}
addModel(file) {
addModel(file, head = null) {
if (this.hasCachedModel(file.key)) {
return this.getModel(file.key);
}
const model = new Model(this.monaco, file);
const model = new Model(this.monaco, file, head);
this.models.set(model.path, model);
this.disposable.add(model);

View File

@ -38,6 +38,15 @@ export default class DecorationsController {
);
}
hasDecorations(model) {
return this.decorations.has(model.url);
}
removeDecorations(model) {
this.decorations.delete(model.url);
this.editorDecorations.delete(model.url);
}
dispose() {
this.decorations.clear();
this.editorDecorations.clear();

View File

@ -3,7 +3,7 @@ import { throttle } from 'underscore';
import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable';
export const getDiffChangeType = (change) => {
export const getDiffChangeType = change => {
if (change.modified) {
return 'modified';
} else if (change.added) {
@ -16,12 +16,7 @@ export const getDiffChangeType = (change) => {
};
export const getDecorator = change => ({
range: new monaco.Range(
change.lineNumber,
1,
change.endLineNumber,
1,
),
range: new monaco.Range(change.lineNumber, 1, change.endLineNumber, 1),
options: {
isWholeLine: true,
linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
@ -31,6 +26,7 @@ export const getDecorator = change => ({
export default class DirtyDiffController {
constructor(modelManager, decorationsController) {
this.disposable = new Disposable();
this.models = new Map();
this.editorSimpleWorker = null;
this.modelManager = modelManager;
this.decorationsController = decorationsController;
@ -42,7 +38,15 @@ export default class DirtyDiffController {
}
attachModel(model) {
if (this.models.has(model.url)) return;
model.onChange(() => this.throttledComputeDiff(model));
model.onDispose(() => {
this.decorationsController.removeDecorations(model);
this.models.delete(model.url);
});
this.models.set(model.url, model);
}
computeDiff(model) {
@ -54,7 +58,11 @@ export default class DirtyDiffController {
}
reDecorate(model) {
this.decorationsController.decorate(model);
if (this.decorationsController.hasDecorations(model)) {
this.decorationsController.decorate(model);
} else {
this.computeDiff(model);
}
}
decorate({ data }) {
@ -65,6 +73,7 @@ export default class DirtyDiffController {
dispose() {
this.disposable.dispose();
this.models.clear();
this.dirtyDiffWorker.removeEventListener('message', this.decorate);
this.dirtyDiffWorker.terminate();

View File

@ -77,8 +77,8 @@ export default class Editor {
}
}
createModel(file) {
return this.modelManager.addModel(file);
createModel(file, head = null) {
return this.modelManager.addModel(file, head);
}
attachModel(model) {

View File

@ -1,3 +1,4 @@
import $ from 'jquery';
import Vue from 'vue';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
@ -32,6 +33,22 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
}
};
export const toggleRightPanelCollapsed = (
{ dispatch, state },
e = undefined,
) => {
if (e) {
$(e.currentTarget)
.tooltip('hide')
.blur();
}
dispatch('setPanelCollapsedStatus', {
side: 'right',
collapsed: !state.rightPanelCollapsed,
});
};
export const setResizingStatus = ({ commit }, resizing) => {
commit(types.SET_RESIZING_STATUS, resizing);
};
@ -104,6 +121,14 @@ export const scrollToTab = () => {
});
};
export const stageAllChanges = ({ state, commit }) => {
state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
};
export const unstageAllChanges = ({ state, commit }) => {
state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path));
};
export const updateViewer = ({ commit }, viewer) => {
commit(types.UPDATE_VIEWER, viewer);
};

View File

@ -24,7 +24,10 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
if (nextFileToOpen.pending) {
dispatch('updateViewer', 'diff');
dispatch('openPendingTab', nextFileToOpen);
dispatch('openPendingTab', {
file: nextFileToOpen,
keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged',
});
} else {
dispatch('updateDelayViewerUpdated', true);
router.push(`/project${nextFileToOpen.url}`);
@ -153,7 +156,7 @@ export const setFileViewMode = ({ state, commit }, { file, viewMode }) => {
commit(types.SET_FILE_VIEWMODE, { file, viewMode });
};
export const discardFileChanges = ({ state, commit }, path) => {
export const discardFileChanges = ({ dispatch, state, commit, getters }, path) => {
const file = state.entries[path];
commit(types.DISCARD_FILE_CHANGES, path);
@ -161,17 +164,40 @@ export const discardFileChanges = ({ state, commit }, path) => {
if (file.tempFile && file.opened) {
commit(types.TOGGLE_FILE_OPEN, path);
} else if (getters.activeFile && file.path === getters.activeFile.path) {
dispatch('updateDelayViewerUpdated', true)
.then(() => {
router.push(`/project${file.url}`);
})
.catch(e => {
throw e;
});
}
eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
eventHub.$emit(`editor.update.model.new.content.${file.key}`, file.content);
eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content);
};
export const openPendingTab = ({ commit, getters, dispatch, state }, file) => {
if (getters.activeFile && getters.activeFile.path === file.path && state.viewer === 'diff') {
export const stageChange = ({ commit, state }, path) => {
const stagedFile = state.stagedFiles.find(f => f.path === path);
commit(types.STAGE_CHANGE, path);
if (stagedFile) {
eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
}
};
export const unstageChange = ({ commit }, path) => {
commit(types.UNSTAGE_CHANGE, path);
};
export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => {
if (getters.activeFile && getters.activeFile === file && state.viewer === 'diff') {
return false;
}
commit(types.ADD_PENDING_TAB, { file });
commit(types.ADD_PENDING_TAB, { file, keyPrefix });
dispatch('scrollToTab');

View File

@ -5,45 +5,71 @@ import * as types from '../mutation_types';
export const getProjectData = (
{ commit, state, dispatch },
{ namespace, projectId, force = false } = {},
) => new Promise((resolve, reject) => {
if (!state.projects[`${namespace}/${projectId}`] || force) {
commit(types.TOGGLE_LOADING, { entry: state });
service.getProjectData(namespace, projectId)
.then(res => res.data)
.then((data) => {
) =>
new Promise((resolve, reject) => {
if (!state.projects[`${namespace}/${projectId}`] || force) {
commit(types.TOGGLE_LOADING, { entry: state });
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
resolve(data);
})
.catch(() => {
flash('Error loading project data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Project not loaded ${namespace}/${projectId}`));
});
} else {
resolve(state.projects[`${namespace}/${projectId}`]);
}
});
service
.getProjectData(namespace, projectId)
.then(res => res.data)
.then(data => {
commit(types.TOGGLE_LOADING, { entry: state });
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
if (!state.currentProjectId)
commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
resolve(data);
})
.catch(() => {
flash(
'Error loading project data. Please try again.',
'alert',
document,
null,
false,
true,
);
reject(new Error(`Project not loaded ${namespace}/${projectId}`));
});
} else {
resolve(state.projects[`${namespace}/${projectId}`]);
}
});
export const getBranchData = (
{ commit, state, dispatch },
{ projectId, branchId, force = false } = {},
) => new Promise((resolve, reject) => {
if ((typeof state.projects[`${projectId}`] === 'undefined' ||
!state.projects[`${projectId}`].branches[branchId])
|| force) {
service.getBranchData(`${projectId}`, branchId)
.then(({ data }) => {
const { id } = data.commit;
commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
resolve(data);
})
.catch(() => {
flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
});
} else {
resolve(state.projects[`${projectId}`].branches[branchId]);
}
});
) =>
new Promise((resolve, reject) => {
if (
typeof state.projects[`${projectId}`] === 'undefined' ||
!state.projects[`${projectId}`].branches[branchId] ||
force
) {
service
.getBranchData(`${projectId}`, branchId)
.then(({ data }) => {
const { id } = data.commit;
commit(types.SET_BRANCH, {
projectPath: `${projectId}`,
branchName: branchId,
branch: data,
});
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
commit(types.SET_CURRENT_BRANCH, branchId);
resolve(data);
})
.catch(() => {
flash(
'Error loading branch data. Please try again.',
'alert',
document,
null,
false,
true,
);
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
});
} else {
resolve(state.projects[`${projectId}`].branches[branchId]);
}
});

View File

@ -1,3 +1,5 @@
import { __ } from '~/locale';
export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
@ -29,9 +31,15 @@ export const currentMergeRequest = state => {
};
// eslint-disable-next-line no-confusing-arrow
export const currentIcon = state =>
export const collapseButtonIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state => !!state.changedFiles.length;
export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
// eslint-disable-next-line no-confusing-arrow
export const collapseButtonTooltip = state =>
state.rightPanelCollapsed ? __('Expand sidebar') : __('Collapse sidebar');
export const hasMergeRequest = state => !!state.currentMergeRequestId;
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);

View File

@ -98,40 +98,25 @@ export const updateFilesAfterCommit = (
{ root: true },
);
rootState.changedFiles.forEach(entry => {
rootState.stagedFiles.forEach(file => {
const changedFile = rootState.changedFiles.find(f => f.path === file.path);
commit(
rootTypes.SET_LAST_COMMIT_DATA,
rootTypes.UPDATE_FILE_AFTER_COMMIT,
{
entry,
file,
lastCommit,
},
{ root: true },
);
eventHub.$emit(`editor.update.model.content.${entry.path}`, entry.content);
commit(
rootTypes.SET_FILE_RAW_DATA,
{
file: entry,
raw: entry.content,
},
{ root: true },
);
commit(
rootTypes.TOGGLE_FILE_CHANGED,
{
file: entry,
changed: false,
},
{ root: true },
);
eventHub.$emit(`editor.update.model.content.${file.key}`, {
content: file.content,
changed: !!changedFile,
});
});
commit(rootTypes.REMOVE_ALL_CHANGES_FILES, null, { root: true });
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) {
if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH && rootGetters.activeFile) {
router.push(
`/project/${rootState.currentProjectId}/blob/${branch}/${rootGetters.activeFile.path}`,
);
@ -184,6 +169,8 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
{ root: true },
);
}
commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
})
.then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH));
})

View File

@ -1,12 +1,17 @@
import * as consts from './constants';
export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading;
const BRANCH_SUFFIX_COUNT = 5;
export const discardDraftButtonDisabled = state =>
state.commitMessage === '' || state.submitCommitLoading;
export const commitButtonDisabled = (state, getters, rootState) =>
getters.discardDraftButtonDisabled || !rootState.changedFiles.length;
getters.discardDraftButtonDisabled || !rootState.stagedFiles.length;
export const newBranchName = (state, _, rootState) =>
`${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(-5)}`;
`${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(
-BRANCH_SUFFIX_COUNT,
)}`;
export const branchName = (state, getters, rootState) => {
if (

View File

@ -51,5 +51,10 @@ export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
export const CLEAR_STAGED_CHANGES = 'CLEAR_STAGED_CHANGES';
export const STAGE_CHANGE = 'STAGE_CHANGE';
export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE';
export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';

View File

@ -49,6 +49,11 @@ export default {
lastCommitMsg,
});
},
[types.CLEAR_STAGED_CHANGES](state) {
Object.assign(state, {
stagedFiles: [],
});
},
[types.SET_ENTRIES](state, entries) {
Object.assign(state, {
entries,
@ -95,6 +100,22 @@ export default {
delayViewerUpdated,
});
},
[types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) {
const changedFile = state.changedFiles.find(f => f.path === file.path);
Object.assign(state.entries[file.path], {
raw: file.content,
changed: !!changedFile,
staged: false,
lastCommit: Object.assign(state.entries[file.path].lastCommit, {
id: lastCommit.commit.id,
url: lastCommit.commit_path,
message: lastCommit.commit.message,
author: lastCommit.commit.author_name,
updatedAt: lastCommit.commit.authored_date,
}),
});
},
...projectMutations,
...mergeRequestMutation,
...fileMutations,

View File

@ -57,7 +57,9 @@ export default {
});
},
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
const changed = content !== state.entries[path].raw;
const stagedFile = state.stagedFiles.find(f => f.path === path);
const rawContent = stagedFile ? stagedFile.content : state.entries[path].raw;
const changed = content !== rawContent;
Object.assign(state.entries[path], {
content,
@ -91,8 +93,10 @@ export default {
});
},
[types.DISCARD_FILE_CHANGES](state, path) {
const stagedFile = state.stagedFiles.find(f => f.path === path);
Object.assign(state.entries[path], {
content: state.entries[path].raw,
content: stagedFile ? stagedFile.content : state.entries[path].raw,
changed: false,
});
},
@ -106,16 +110,67 @@ export default {
changedFiles: state.changedFiles.filter(f => f.path !== path),
});
},
[types.STAGE_CHANGE](state, path) {
const stagedFile = state.stagedFiles.find(f => f.path === path);
Object.assign(state, {
changedFiles: state.changedFiles.filter(f => f.path !== path),
entries: Object.assign(state.entries, {
[path]: Object.assign(state.entries[path], {
staged: true,
changed: false,
}),
}),
});
if (stagedFile) {
Object.assign(stagedFile, {
...state.entries[path],
});
} else {
Object.assign(state, {
stagedFiles: state.stagedFiles.concat({
...state.entries[path],
}),
});
}
},
[types.UNSTAGE_CHANGE](state, path) {
const changedFile = state.changedFiles.find(f => f.path === path);
const stagedFile = state.stagedFiles.find(f => f.path === path);
if (!changedFile && stagedFile) {
Object.assign(state.entries[path], {
...stagedFile,
key: state.entries[path].key,
active: state.entries[path].active,
opened: state.entries[path].opened,
changed: true,
});
Object.assign(state, {
changedFiles: state.changedFiles.concat(state.entries[path]),
});
}
Object.assign(state, {
stagedFiles: state.stagedFiles.filter(f => f.path !== path),
entries: Object.assign(state.entries, {
[path]: Object.assign(state.entries[path], {
staged: false,
}),
}),
});
},
[types.TOGGLE_FILE_CHANGED](state, { file, changed }) {
Object.assign(state.entries[file.path], {
changed,
});
},
[types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
const pendingTab = state.openFiles.find(f => f.path === file.path && f.pending);
let openFiles = state.openFiles.map(f =>
Object.assign(f, { active: f.path === file.path, opened: false }),
);
const key = `${keyPrefix}-${file.key}`;
const pendingTab = state.openFiles.find(f => f.key === key && f.pending);
let openFiles = state.openFiles.map(f => Object.assign(f, { active: false, opened: false }));
if (!pendingTab) {
const openFile = openFiles.find(f => f.path === file.path);
@ -126,10 +181,11 @@ export default {
if (f.path === file.path) {
return acc.concat({
...f,
content: file.content,
active: true,
pending: true,
opened: true,
key: `${keyPrefix}-${f.key}`,
key,
});
}

View File

@ -17,12 +17,8 @@ export default {
});
},
[types.SET_DIRECTORY_DATA](state, { data, treePath }) {
Object.assign(state, {
trees: Object.assign(state.trees, {
[treePath]: {
tree: data,
},
}),
Object.assign(state.trees[treePath], {
tree: data,
});
},
[types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {

View File

@ -3,6 +3,7 @@ export default () => ({
currentBranchId: '',
currentMergeRequestId: '',
changedFiles: [],
stagedFiles: [],
endpoints: {},
lastCommitMsg: '',
lastCommitPath: '',

View File

@ -15,6 +15,7 @@ export const dataStructure = () => ({
opened: false,
active: false,
changed: false,
staged: false,
lastCommitPath: '',
lastCommit: {
id: '',
@ -101,7 +102,7 @@ export const setPageTitle = title => {
export const createCommitPayload = (branch, newBranch, state, rootState) => ({
branch,
commit_message: state.commitMessage,
actions: rootState.changedFiles.map(f => ({
actions: rootState.stagedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.content,

View File

@ -30,10 +30,10 @@ export default class IssuableContext {
const $selectbox = $block.find('.selectbox');
if ($selectbox.is(':visible')) {
$selectbox.hide();
$block.find('.value').show();
$block.find('.value:not(.dont-hide)').show();
} else {
$selectbox.show();
$block.find('.value').hide();
$block.find('.value:not(.dont-hide)').hide();
}
if ($selectbox.is(':visible')) {

View File

@ -1,82 +1,94 @@
<script>
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import callout from '../../vue_shared/components/callout.vue';
export default {
name: 'JobHeaderSection',
components: {
ciHeader,
loadingIcon,
export default {
name: 'JobHeaderSection',
components: {
ciHeader,
loadingIcon,
callout,
},
props: {
job: {
type: Object,
required: true,
},
props: {
job: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
data() {
return {
actions: this.getActions(),
};
},
data() {
return {
actions: this.getActions(),
};
},
computed: {
status() {
return this.job && this.job.status;
},
computed: {
status() {
return this.job && this.job.status;
},
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length;
},
/**
* When job has not started the key will be `false`
* When job started the key will be a string with a date.
*/
jobStarted() {
return !this.job.started === false;
},
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length;
},
watch: {
job() {
this.actions = this.getActions();
},
shouldRenderReason() {
return !!(this.job.status && this.job.callout_message);
},
methods: {
getActions() {
const actions = [];
/**
* When job has not started the key will be `false`
* When job started the key will be a string with a date.
*/
jobStarted() {
return !this.job.started === false;
},
},
watch: {
job() {
this.actions = this.getActions();
},
},
methods: {
getActions() {
const actions = [];
if (this.job.new_issue_path) {
actions.push({
label: 'New issue',
path: this.job.new_issue_path,
cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
type: 'link',
});
}
return actions;
},
if (this.job.new_issue_path) {
actions.push({
label: 'New issue',
path: this.job.new_issue_path,
cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
type: 'link',
});
}
return actions;
},
};
},
};
</script>
<template>
<div class="js-build-header build-header top-area">
<ci-header
v-if="shouldRenderContent"
:status="status"
item-name="Job"
:item-id="job.id"
:time="job.created_at"
:user="job.user"
:actions="actions"
:has-sidebar-button="true"
:should-render-triggered-label="jobStarted"
<header>
<div class="js-build-header build-header top-area">
<ci-header
v-if="shouldRenderContent"
:status="status"
item-name="Job"
:item-id="job.id"
:time="job.created_at"
:user="job.user"
:actions="actions"
:has-sidebar-button="true"
:should-render-triggered-label="jobStarted"
/>
<loading-icon
v-if="isLoading"
size="2"
class="prepend-top-default append-bottom-default"
/>
</div>
<callout
v-if="shouldRenderReason"
:message="job.callout_message"
/>
<loading-icon
v-if="isLoading"
size="2"
class="prepend-top-default append-bottom-default"
/>
</div>
</header>
</template>

View File

@ -1,80 +1,119 @@
<script>
import detailRow from './sidebar_detail_row.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
import detailRow from './sidebar_detail_row.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { timeIntervalInWords } from '../../lib/utils/datetime_utility';
export default {
name: 'SidebarDetailsBlock',
components: {
detailRow,
loadingIcon,
export default {
name: 'SidebarDetailsBlock',
components: {
detailRow,
loadingIcon,
},
mixins: [timeagoMixin],
props: {
job: {
type: Object,
required: true,
},
mixins: [
timeagoMixin,
],
props: {
job: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
runnerHelpUrl: {
type: String,
required: false,
default: '',
},
isLoading: {
type: Boolean,
required: true,
},
computed: {
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length > 0;
},
coverage() {
return `${this.job.coverage}%`;
},
duration() {
return timeIntervalInWords(this.job.duration);
},
queued() {
return timeIntervalInWords(this.job.queued);
},
runnerId() {
return `#${this.job.runner.id}`;
},
hasTimeout() {
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
},
timeout() {
if (this.job.metadata == null) {
return '';
}
canUserRetry: {
type: Boolean,
required: false,
default: false,
},
runnerHelpUrl: {
type: String,
required: false,
default: '',
},
},
computed: {
shouldRenderContent() {
return !this.isLoading && Object.keys(this.job).length > 0;
},
coverage() {
return `${this.job.coverage}%`;
},
duration() {
return timeIntervalInWords(this.job.duration);
},
queued() {
return timeIntervalInWords(this.job.queued);
},
runnerId() {
return `#${this.job.runner.id}`;
},
retryButtonClass() {
let className = 'js-retry-button pull-right btn btn-retry visible-md-block visible-lg-block';
className +=
this.job.status && this.job.recoverable
? ' btn-primary'
: ' btn-inverted-secondary';
return className;
},
hasTimeout() {
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
},
timeout() {
if (this.job.metadata == null) {
return '';
}
let t = this.job.metadata.timeout_human_readable;
if (this.job.metadata.timeout_source !== '') {
t += ` (from ${this.job.metadata.timeout_source})`;
}
let t = this.job.metadata.timeout_human_readable;
if (this.job.metadata.timeout_source !== '') {
t += ` (from ${this.job.metadata.timeout_source})`;
}
return t;
},
renderBlock() {
return this.job.merge_request ||
this.job.duration ||
this.job.finished_data ||
this.job.erased_at ||
this.job.queued ||
this.job.runner ||
this.job.coverage ||
this.job.tags.length ||
this.job.cancel_path;
},
return t;
},
};
renderBlock() {
return (
this.job.merge_request ||
this.job.duration ||
this.job.finished_data ||
this.job.erased_at ||
this.job.queued ||
this.job.runner ||
this.job.coverage ||
this.job.tags.length ||
this.job.cancel_path
);
},
},
};
</script>
<template>
<div>
<div class="block">
<strong class="inline prepend-top-8">
{{ job.name }}
</strong>
<a
v-if="canUserRetry"
:class="retryButtonClass"
:href="job.retry_path"
data-method="post"
rel="nofollow"
>
{{ __('Retry') }}
</a>
<button
type="button"
:aria-label="__('Toggle Sidebar')"
class="btn btn-blank gutter-toggle pull-right
visible-xs-block visible-sm-block js-sidebar-build-toggle"
>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-angle-double-right"
></i>
</button>
</div>
<template v-if="shouldRenderContent">
<div
class="block retry-link"
@ -85,16 +124,16 @@
class="js-new-issue btn btn-new btn-inverted"
:href="job.new_issue_path"
>
New issue
{{ __('New issue') }}
</a>
<a
v-if="job.retry_path"
v-if="canUserRetry"
class="js-retry-job btn btn-inverted-secondary"
:href="job.retry_path"
data-method="post"
rel="nofollow"
>
Retry
{{ __('Retry') }}
</a>
</div>
<div :class="{block : renderBlock }">
@ -103,7 +142,7 @@
v-if="job.merge_request"
>
<span class="build-light-text">
Merge Request:
{{ __('Merge Request:') }}
</span>
<a :href="job.merge_request.path">
!{{ job.merge_request.iid }}
@ -158,7 +197,7 @@
v-if="job.tags.length"
>
<span class="build-light-text">
Tags:
{{ __('Tags:') }}
</span>
<span
v-for="(tag, i) in job.tags"
@ -178,7 +217,7 @@
data-method="post"
rel="nofollow"
>
Cancel
{{ __('Cancel') }}
</a>
</div>
</div>

View File

@ -35,9 +35,11 @@ export default () => {
});
// Sidebar information block
const detailsBlockElement = document.getElementById('js-details-block-vue');
const detailsBlockDataset = detailsBlockElement.dataset;
// eslint-disable-next-line
new Vue({
el: '#js-details-block-vue',
el: detailsBlockElement,
components: {
detailsBlock,
},
@ -50,6 +52,7 @@ export default () => {
return createElement('details-block', {
props: {
isLoading: this.mediator.state.isLoading,
canUserRetry: !!('canUserRetry' in detailsBlockDataset),
job: this.mediator.store.state.job,
runnerHelpUrl: dataset.runnerHelpUrl,
},

View File

@ -83,7 +83,7 @@ export default class LabelsSelect {
$dropdown.trigger('loading.gl.dropdown');
axios.put(issueUpdateURL, data)
.then(({ data }) => {
var labelCount, template, labelTooltipTitle, labelTitles;
var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels;
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
@ -115,8 +115,7 @@ export default class LabelsSelect {
labelTooltipTitle = labelTitles.join(', ');
}
else {
labelTooltipTitle = '';
$sidebarLabelTooltip.tooltip('destroy');
labelTooltipTitle = __('Labels');
}
$sidebarLabelTooltip

View File

@ -4,6 +4,7 @@
import $ from 'jquery';
import _ from 'underscore';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store';
@ -25,7 +26,7 @@ export default class MilestoneSelect {
}
$els.each((i, dropdown) => {
let collapsedSidebarLabelTemplate, milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
let milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
const $dropdown = $(dropdown);
const projectId = $dropdown.data('projectId');
const milestonesUrl = $dropdown.data('milestones');
@ -52,7 +53,6 @@ export default class MilestoneSelect {
if (issueUpdateURL) {
milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>');
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
@ -214,10 +214,16 @@ export default class MilestoneSelect {
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
return $sidebarCollapsedValue
.attr('data-original-title', `${data.milestone.name}<br />${data.milestone.remaining}`)
.find('span')
.text(data.milestone.title);
} else {
$value.html(milestoneLinkNoneTemplate);
return $sidebarCollapsedValue.find('span').text('No');
return $sidebarCollapsedValue
.attr('data-original-title', __('Milestone'))
.find('span')
.text(__('None'));
}
})
.catch(() => {

View File

@ -19,7 +19,6 @@ import AjaxCache from '~/lib/utils/ajax_cache';
import Vue from 'vue';
import syntaxHighlight from '~/syntax_highlight';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
@ -198,6 +197,8 @@ export default class Notes {
);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff);
this.$wrapperEl.on('click', '.js-toggle-lazy-diff-retry-button', this.onClickRetryLazyLoad.bind(this));
// fetch notes when tab becomes visible
this.$wrapperEl.on('visibilitychange', this.visibilityChange);
// when issue status changes, we need to refresh data
@ -244,6 +245,7 @@ export default class Notes {
this.$wrapperEl.off('click', '.js-comment-resolve-button');
this.$wrapperEl.off('click', '.system-note-commit-list-toggler');
this.$wrapperEl.off('click', '.js-toggle-lazy-diff');
this.$wrapperEl.off('click', '.js-toggle-lazy-diff-retry-button');
this.$wrapperEl.off('ajax:success', '.js-main-target-form');
this.$wrapperEl.off('ajax:success', '.js-discussion-note-form');
this.$wrapperEl.off('ajax:complete', '.js-main-target-form');
@ -1425,22 +1427,21 @@ export default class Notes {
const { discussion_html } = data;
const lines = $(discussion_html).find('.line_holder');
lines.addClass('fade-in');
$container.find('tbody').prepend(lines);
$container.find('.diff-content > table > tbody').prepend(lines);
const fileHolder = $container.find('.file-holder');
$container.find('.line-holder-placeholder').remove();
syntaxHighlight(fileHolder);
}
static renderDiffError($container) {
$container.find('.line_content').html(
$(`
<div class="nothing-here-block">
${__(
'Unable to load the diff.',
)} <a class="js-toggle-lazy-diff" href="javascript:void(0)">Try again</a>?
</div>
`),
);
onClickRetryLazyLoad(e) {
const $retryButton = $(e.currentTarget);
$retryButton.prop('disabled', true);
return this.loadLazyDiff(e)
.then(() => {
$retryButton.prop('disabled', false);
});
}
loadLazyDiff(e) {
@ -1449,20 +1450,35 @@ export default class Notes {
$container.find('.js-toggle-lazy-diff').removeClass('js-toggle-lazy-diff');
const tableEl = $container.find('tbody');
if (tableEl.length === 0) return;
const $tableEl = $container.find('tbody');
if ($tableEl.length === 0) return;
const fileHolder = $container.find('.file-holder');
const url = fileHolder.data('linesPath');
axios
const $errorContainer = $container.find('.js-error-lazy-load-diff');
const $successContainer = $container.find('.js-success-lazy-load');
/**
* We only fetch resolved discussions.
* Unresolved discussions don't have an endpoint being provided.
*/
if (url) {
return axios
.get(url)
.then(({ data }) => {
// Reset state in case last request returned error
$successContainer.removeClass('hidden');
$errorContainer.addClass('hidden');
Notes.renderDiffContent($container, data);
})
.catch(() => {
Notes.renderDiffError($container);
$successContainer.addClass('hidden');
$errorContainer.removeClass('hidden');
});
}
return Promise.resolve();
}
toggleCommitList(e) {

View File

@ -19,7 +19,7 @@ function getSystemDate(systemUtcOffsetSeconds) {
const date = new Date();
const localUtcOffsetMinutes = 0 - date.getTimezoneOffset();
const systemUtcOffsetMinutes = systemUtcOffsetSeconds / 60;
date.setMinutes((date.getMinutes() - localUtcOffsetMinutes) + systemUtcOffsetMinutes);
date.setMinutes(date.getMinutes() - localUtcOffsetMinutes + systemUtcOffsetMinutes);
return date;
}
@ -35,18 +35,36 @@ function formatTooltipText({ date, count }) {
return `${contribText}<br />${dateDayName} ${dateText}`;
}
const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]);
const initColorKey = () =>
d3
.scaleLinear()
.range(['#acd5f2', '#254e77'])
.domain([0, 3]);
export default class ActivityCalendar {
constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0) {
constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0, firstDayOfWeek = 0) {
this.calendarActivitiesPath = calendarActivitiesPath;
this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = '';
this.daySpace = 1;
this.daySize = 15;
this.daySizeWithSpace = this.daySize + (this.daySpace * 2);
this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
this.daySizeWithSpace = this.daySize + this.daySpace * 2;
this.monthNames = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
this.months = [];
this.firstDayOfWeek = firstDayOfWeek;
// Loop through the timestamps to create a group of objects
// The group of objects will be grouped based on the day of the week they are
@ -70,7 +88,7 @@ export default class ActivityCalendar {
// Create a new group array if this is the first day of the week
// or if is first object
if ((day === 0 && i !== 0) || i === 0) {
if ((day === this.firstDayOfWeek && i !== 0) || i === 0) {
this.timestampsTmp.push([]);
group += 1;
}
@ -109,21 +127,30 @@ export default class ActivityCalendar {
}
renderSvg(container, group) {
const width = ((group + 1) * this.daySizeWithSpace) + this.getExtraWidthPadding(group);
return d3.select(container)
const width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
return d3
.select(container)
.append('svg')
.attr('width', width)
.attr('height', 167)
.attr('class', 'contrib-calendar');
.attr('width', width)
.attr('height', 167)
.attr('class', 'contrib-calendar');
}
dayYPos(day) {
return this.daySizeWithSpace * ((day + 7 - this.firstDayOfWeek) % 7);
}
renderDays() {
this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g')
this.svg
.selectAll('g')
.data(this.timestampsTmp)
.enter()
.append('g')
.attr('transform', (group, i) => {
_.each(group, (stamp, a) => {
if (a === 0 && stamp.day === 0) {
const month = stamp.date.getMonth();
const x = (this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace;
const x = this.daySizeWithSpace * i + 1 + this.daySizeWithSpace;
const lastMonth = _.last(this.months);
if (
lastMonth == null ||
@ -133,86 +160,113 @@ export default class ActivityCalendar {
}
}
});
return `translate(${(this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace}, 18)`;
return `translate(${this.daySizeWithSpace * i + 1 + this.daySizeWithSpace}, 18)`;
})
.selectAll('rect')
.data(stamp => stamp)
.enter()
.append('rect')
.attr('x', '0')
.attr('y', stamp => this.daySizeWithSpace * stamp.day)
.attr('width', this.daySize)
.attr('height', this.daySize)
.attr('fill', stamp => (
stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'
))
.attr('title', stamp => formatTooltipText(stamp))
.attr('class', 'user-contrib-cell js-tooltip')
.attr('data-container', 'body')
.on('click', this.clickDay);
.data(stamp => stamp)
.enter()
.append('rect')
.attr('x', '0')
.attr('y', stamp => this.dayYPos(stamp.day))
.attr('width', this.daySize)
.attr('height', this.daySize)
.attr(
'fill',
stamp => (stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed'),
)
.attr('title', stamp => formatTooltipText(stamp))
.attr('class', 'user-contrib-cell js-tooltip')
.attr('data-container', 'body')
.on('click', this.clickDay);
}
renderDayTitles() {
const days = [
{
text: 'M',
y: 29 + (this.daySizeWithSpace * 1),
}, {
y: 29 + this.dayYPos(1),
},
{
text: 'W',
y: 29 + (this.daySizeWithSpace * 3),
}, {
y: 29 + this.dayYPos(2),
},
{
text: 'F',
y: 29 + (this.daySizeWithSpace * 5),
y: 29 + this.dayYPos(3),
},
];
this.svg.append('g')
this.svg
.append('g')
.selectAll('text')
.data(days)
.enter()
.append('text')
.attr('text-anchor', 'middle')
.attr('x', 8)
.attr('y', day => day.y)
.text(day => day.text)
.attr('class', 'user-contrib-text');
.data(days)
.enter()
.append('text')
.attr('text-anchor', 'middle')
.attr('x', 8)
.attr('y', day => day.y)
.text(day => day.text)
.attr('class', 'user-contrib-text');
}
renderMonths() {
this.svg.append('g')
this.svg
.append('g')
.attr('direction', 'ltr')
.selectAll('text')
.data(this.months)
.enter()
.append('text')
.attr('x', date => date.x)
.attr('y', 10)
.attr('class', 'user-contrib-text')
.text(date => this.monthNames[date.month]);
.data(this.months)
.enter()
.append('text')
.attr('x', date => date.x)
.attr('y', 10)
.attr('class', 'user-contrib-text')
.text(date => this.monthNames[date.month]);
}
renderKey() {
const keyValues = ['no contributions', '1-9 contributions', '10-19 contributions', '20-29 contributions', '30+ contributions'];
const keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
const keyValues = [
'no contributions',
'1-9 contributions',
'10-19 contributions',
'20-29 contributions',
'30+ contributions',
];
const keyColors = [
'#ededed',
this.colorKey(0),
this.colorKey(1),
this.colorKey(2),
this.colorKey(3),
];
this.svg.append('g')
.attr('transform', `translate(18, ${(this.daySizeWithSpace * 8) + 16})`)
this.svg
.append('g')
.attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
.selectAll('rect')
.data(keyColors)
.enter()
.append('rect')
.attr('width', this.daySize)
.attr('height', this.daySize)
.attr('x', (color, i) => this.daySizeWithSpace * i)
.attr('y', 0)
.attr('fill', color => color)
.attr('class', 'js-tooltip')
.attr('title', (color, i) => keyValues[i])
.attr('data-container', 'body');
.data(keyColors)
.enter()
.append('rect')
.attr('width', this.daySize)
.attr('height', this.daySize)
.attr('x', (color, i) => this.daySizeWithSpace * i)
.attr('y', 0)
.attr('fill', color => color)
.attr('class', 'js-tooltip')
.attr('title', (color, i) => keyValues[i])
.attr('data-container', 'body');
}
initColor() {
const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
return d3.scaleThreshold().domain([0, 10, 20, 30]).range(colorRange);
const colorRange = [
'#ededed',
this.colorKey(0),
this.colorKey(1),
this.colorKey(2),
this.colorKey(3),
];
return d3
.scaleThreshold()
.domain([0, 10, 20, 30])
.range(colorRange);
}
clickDay(stamp) {
@ -227,14 +281,15 @@ export default class ActivityCalendar {
$('.user-calendar-activities').html(LOADING_HTML);
axios.get(this.calendarActivitiesPath, {
params: {
date,
},
responseType: 'text',
})
.then(({ data }) => $('.user-calendar-activities').html(data))
.catch(() => flash(__('An error occurred while retrieving calendar activity')));
axios
.get(this.calendarActivitiesPath, {
params: {
date,
},
responseType: 'text',
})
.then(({ data }) => $('.user-calendar-activities').html(data))
.catch(() => flash(__('An error occurred while retrieving calendar activity')));
} else {
this.currentSelectedDate = '';
$('.user-calendar-activities').html('');

View File

@ -10,29 +10,25 @@ export default class PerformanceBarService {
}
static registerInterceptor(peekUrl, callback) {
vueResourceInterceptor = (request, next) => {
next(response => {
const requestId = response.headers['x-request-id'];
const requestUrl = response.url;
if (requestUrl !== peekUrl && requestId) {
callback(requestId, requestUrl);
}
});
};
Vue.http.interceptors.push(vueResourceInterceptor);
return axios.interceptors.response.use(response => {
const interceptor = response => {
const requestId = response.headers['x-request-id'];
const requestUrl = response.config.url;
// Get the request URL from response.config for Axios, and response for
// Vue Resource.
const requestUrl = (response.config || response).url;
const cachedResponse = response.headers['x-gitlab-from-cache'] === 'true';
if (requestUrl !== peekUrl && requestId) {
if (requestUrl !== peekUrl && requestId && !cachedResponse) {
callback(requestId, requestUrl);
}
return response;
});
};
vueResourceInterceptor = (request, next) => next(interceptor);
Vue.http.interceptors.push(vueResourceInterceptor);
return axios.interceptors.response.use(interceptor);
}
static removeInterceptor(interceptor) {

View File

@ -32,26 +32,38 @@ export default {
required: true,
},
buttonDisabled: {
requestFinishedFor: {
type: String,
required: false,
default: null,
default: '',
},
},
data() {
return {
isDisabled: false,
linkRequested: '',
};
},
computed: {
cssClass() {
const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`;
},
isDisabled() {
return this.buttonDisabled === this.link;
},
watch: {
requestFinishedFor() {
if (this.requestFinishedFor === this.linkRequested) {
this.isDisabled = false;
}
},
},
methods: {
onClickAction() {
$(this.$el).tooltip('hide');
eventHub.$emit('graphAction', this.link);
this.linkRequested = this.link;
this.isDisabled = true;
},
},
};
@ -62,7 +74,8 @@ export default {
@click="onClickAction"
v-tooltip
:title="tooltipText"
class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper"
class="js-ci-action btn btn-blank
btn-transparent ci-action-icon-container ci-action-icon-wrapper"
:class="cssClass"
data-container="body"
:disabled="isDisabled"

View File

@ -1,53 +0,0 @@
<script>
import icon from '../../../vue_shared/components/icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
* TODO: Remove UJS from here and use an async request instead.
*/
export default {
components: {
icon,
},
directives: {
tooltip,
},
props: {
tooltipText: {
type: String,
required: true,
},
link: {
type: String,
required: true,
},
actionMethod: {
type: String,
required: true,
},
actionIcon: {
type: String,
required: true,
},
},
};
</script>
<template>
<a
v-tooltip
:data-method="actionMethod"
:title="tooltipText"
:href="link"
rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon"
data-container="body"
aria-label="Job's action"
>
<icon :name="actionIcon" />
</a>
</template>

View File

@ -1,77 +1,83 @@
<script>
import $ from 'jquery';
import jobNameComponent from './job_name_component.vue';
import jobComponent from './job_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
import $ from 'jquery';
import JobNameComponent from './job_name_component.vue';
import JobComponent from './job_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders the dropdown for the pipeline graph.
*
* The following object should be provided as `job`:
*
* {
* "id": 4256,
* "name": "test",
* "status": {
* "icon": "icon_status_success",
* "text": "passed",
* "label": "passed",
* "group": "success",
* "details_path": "/root/ci-mock/builds/4256",
* "action": {
* "icon": "retry",
* "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry",
* "method": "post"
* }
* }
* }
*/
export default {
directives: {
tooltip,
/**
* Renders the dropdown for the pipeline graph.
*
* The following object should be provided as `job`:
*
* {
* "id": 4256,
* "name": "test",
* "status": {
* "icon": "icon_status_success",
* "text": "passed",
* "label": "passed",
* "group": "success",
* "details_path": "/root/ci-mock/builds/4256",
* "action": {
* "icon": "retry",
* "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry",
* "method": "post"
* }
* }
* }
*/
export default {
directives: {
tooltip,
},
components: {
JobComponent,
JobNameComponent,
},
props: {
job: {
type: Object,
required: true,
},
components: {
jobComponent,
jobNameComponent,
requestFinishedFor: {
type: String,
required: false,
default: '',
},
},
props: {
job: {
type: Object,
required: true,
},
computed: {
tooltipText() {
return `${this.job.name} - ${this.job.status.label}`;
},
},
computed: {
tooltipText() {
return `${this.job.name} - ${this.job.status.label}`;
},
},
mounted() {
this.stopDropdownClickPropagation();
},
mounted() {
this.stopDropdownClickPropagation();
},
methods: {
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
methods: {
/**
* When the user right clicks or cmd/ctrl + click in the job name or the action icon
* the dropdown should not be closed so we stop propagation
* of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el
.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
.on('click', (e) => {
e.stopPropagation();
});
},
stopDropdownClickPropagation() {
$(
'.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item',
this.$el,
).on('click', e => {
e.stopPropagation();
});
},
};
},
};
</script>
<template>
<div class="ci-job-dropdown-container">
@ -101,8 +107,8 @@
:key="i">
<job-component
:job="item"
:is-dropdown="true"
css-class-job-name="mini-pipeline-graph-dropdown-item"
:request-finished-for="requestFinishedFor"
/>
</li>
</ul>

View File

@ -7,7 +7,6 @@ export default {
StageColumnComponent,
LoadingIcon,
},
props: {
isLoading: {
type: Boolean,
@ -17,10 +16,10 @@ export default {
type: Object,
required: true,
},
actionDisabled: {
requestFinishedFor: {
type: String,
required: false,
default: null,
default: '',
},
},
@ -75,7 +74,7 @@ export default {
:key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
:action-disabled="actionDisabled"
:request-finished-for="requestFinishedFor"
/>
</ul>
</div>

View File

@ -1,6 +1,5 @@
<script>
import ActionComponent from './action_component.vue';
import DropdownActionComponent from './dropdown_action_component.vue';
import JobNameComponent from './job_name_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip';
export default {
components: {
ActionComponent,
DropdownActionComponent,
JobNameComponent,
},
directives: {
tooltip,
},
@ -44,26 +41,17 @@ export default {
type: Object,
required: true,
},
cssClassJobName: {
type: String,
required: false,
default: '',
},
isDropdown: {
type: Boolean,
required: false,
default: false,
},
actionDisabled: {
requestFinishedFor: {
type: String,
required: false,
default: null,
default: '',
},
},
computed: {
status() {
return this.job && this.job.status ? this.job.status : {};
@ -134,19 +122,11 @@ export default {
</div>
<action-component
v-if="hasAction && !isDropdown"
v-if="hasAction"
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
:button-disabled="actionDisabled"
/>
<dropdown-action-component
v-if="hasAction && isDropdown"
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
:action-method="status.action.method"
:request-finished-for="requestFinishedFor"
/>
</div>
</template>

View File

@ -29,10 +29,11 @@ export default {
required: false,
default: '',
},
actionDisabled: {
requestFinishedFor: {
type: String,
required: false,
default: null,
default: '',
},
},
@ -74,12 +75,12 @@ export default {
v-if="job.size === 1"
:job="job"
css-class-job-name="build-content"
:action-disabled="actionDisabled"
/>
<dropdown-job-component
v-if="job.size > 1"
:job="job"
:request-finished-for="requestFinishedFor"
/>
</li>

View File

@ -25,7 +25,7 @@ export default () => {
data() {
return {
mediator,
actionDisabled: null,
requestFinishedFor: null,
};
},
created() {
@ -36,15 +36,17 @@ export default () => {
},
methods: {
postAction(action) {
this.actionDisabled = action;
// Click was made, reset this variable
this.requestFinishedFor = null;
this.mediator.service.postAction(action)
this.mediator.service
.postAction(action)
.then(() => {
this.mediator.refreshPipeline();
this.actionDisabled = null;
this.requestFinishedFor = action;
})
.catch(() => {
this.actionDisabled = null;
this.requestFinishedFor = action;
Flash(__('An error occurred while making the request.'));
});
},
@ -54,7 +56,7 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
actionDisabled: this.actionDisabled,
requestFinishedFor: this.requestFinishedFor,
},
});
},
@ -79,7 +81,8 @@ export default () => {
},
methods: {
postAction(action) {
this.mediator.service.postAction(action.path)
this.mediator.service
.postAction(action.path)
.then(() => this.mediator.refreshPipeline())
.catch(() => Flash(__('An error occurred while making the request.')));
},

View File

@ -5,6 +5,7 @@ import _ from 'underscore';
import Cookies from 'js-cookie';
import flash from './flash';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
function Sidebar(currentUser) {
this.toggleTodo = this.toggleTodo.bind(this);
@ -41,12 +42,14 @@ Sidebar.prototype.addEventListeners = function() {
};
Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon;
var $allGutterToggleIcons, $this, isExpanded, tooltipLabel;
e.preventDefault();
$this = $(this);
$thisIcon = $this.find('i');
isExpanded = $this.find('i').hasClass('fa-angle-double-right');
tooltipLabel = isExpanded ? __('Expand sidebar') : __('Collapse sidebar');
$allGutterToggleIcons = $('.js-sidebar-toggle i');
if ($thisIcon.hasClass('fa-angle-double-right')) {
if (isExpanded) {
$allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
$('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
$('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
@ -57,6 +60,9 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
}
$this.attr('data-original-title', tooltipLabel);
if (!triggered) {
Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
}

View File

@ -1,6 +1,12 @@
<script>
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'Assignees',
directives: {
tooltip,
},
props: {
rootPath: {
type: String,
@ -14,6 +20,11 @@ export default {
type: Boolean,
required: true,
},
issuableType: {
type: String,
require: true,
default: 'issue',
},
},
data() {
return {
@ -62,6 +73,12 @@ export default {
names.push(`+ ${this.users.length - maxRender} more`);
}
if (!this.users.length) {
const emptyTooltipLabel = this.issuableType === 'issue' ?
__('Assignee(s)') : __('Assignee');
names.push(emptyTooltipLabel);
}
return names.join(', ');
},
sidebarAvatarCounter() {
@ -109,7 +126,8 @@ export default {
<div>
<div
class="sidebar-collapsed-icon sidebar-collapsed-user"
:class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }"
:class="{ 'multiple-users': hasMoreThanOneAssignee }"
v-tooltip
data-container="body"
data-placement="left"
:title="collapsedTooltipTitle"

View File

@ -1,9 +1,9 @@
<script>
import Flash from '../../../flash';
import Flash from '~/flash';
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
import AssigneeTitle from './assignee_title.vue';
import Assignees from './assignees.vue';
import Store from '../../stores/sidebar_store';
import eventHub from '../../event_hub';
export default {
name: 'SidebarAssignees',
@ -25,6 +25,11 @@ export default {
required: false,
default: false,
},
issuableType: {
type: String,
require: true,
default: 'issue',
},
},
data() {
return {
@ -90,6 +95,7 @@ export default {
:users="store.assignees"
:editable="store.editable"
@assign-self="assignSelf"
:issuable-type="issuableType"
/>
</div>
</template>

View File

@ -1,15 +1,19 @@
<script>
import Flash from '../../../flash';
import { __ } from '~/locale';
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
import Icon from '../../../vue_shared/components/icon.vue';
import { __ } from '../../../locale';
import eventHub from '../../event_hub';
export default {
components: {
editForm,
Icon,
},
directives: {
tooltip,
},
props: {
isConfidential: {
required: true,
@ -33,6 +37,9 @@ export default {
confidentialityIcon() {
return this.isConfidential ? 'eye-slash' : 'eye';
},
tooltipLabel() {
return this.isConfidential ? __('Confidential') : __('Not confidential');
},
},
created() {
eventHub.$on('closeConfidentialityForm', this.toggleForm);
@ -65,6 +72,10 @@ export default {
<div
class="sidebar-collapsed-icon"
@click="toggleForm"
v-tooltip
data-container="body"
data-placement="left"
:title="tooltipLabel"
>
<icon
:name="confidentialityIcon"

View File

@ -1,15 +1,22 @@
<script>
import { __ } from '~/locale';
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import issuableMixin from '~/vue_shared/mixins/issuable';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
import Icon from '../../../vue_shared/components/icon.vue';
import eventHub from '../../event_hub';
export default {
components: {
editForm,
Icon,
},
directives: {
tooltip,
},
mixins: [issuableMixin],
props: {
@ -44,6 +51,10 @@ export default {
isLockDialogOpen() {
return this.mediator.store.isLockDialogOpen;
},
tooltipLabel() {
return this.isLocked ? __('Locked') : __('Unlocked');
},
},
created() {
@ -85,6 +96,10 @@ export default {
<div
class="sidebar-collapsed-icon"
@click="toggleForm"
v-tooltip
data-container="body"
data-placement="left"
:title="tooltipLabel"
>
<icon
:name="lockIcon"

View File

@ -1,9 +1,13 @@
<script>
import { __, n__, sprintf } from '../../../locale';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue';
import { __, n__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
export default {
directives: {
tooltip,
},
components: {
loadingIcon,
userAvatarImage,
@ -72,7 +76,13 @@
<template>
<div>
<div class="sidebar-collapsed-icon">
<div
class="sidebar-collapsed-icon"
v-tooltip
data-container="body"
data-placement="left"
:title="participantLabel"
>
<i
class="fa fa-users"
aria-hidden="true"

View File

@ -1,12 +1,17 @@
<script>
import icon from '../../../vue_shared/components/icon.vue';
import { abbreviateTime } from '../../../lib/utils/pretty_time';
import { __, sprintf } from '~/locale';
import { abbreviateTime } from '~/lib/utils/pretty_time';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'TimeTrackingCollapsedState',
components: {
icon,
},
directives: {
tooltip,
},
props: {
showComparisonState: {
type: Boolean,
@ -79,6 +84,21 @@
return '';
},
timeTrackedTooltipText() {
let title;
if (this.showComparisonState) {
title = __('Time remaining');
} else if (this.showEstimateOnlyState) {
title = __('Estimated');
} else if (this.showSpentOnlyState) {
title = __('Time spent');
}
return sprintf('%{title}: %{text}', ({ title, text: this.text }));
},
tooltipText() {
return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText;
},
},
methods: {
abbreviateTime(timeStr) {
@ -89,7 +109,13 @@
</script>
<template>
<div class="sidebar-collapsed-icon">
<div
class="sidebar-collapsed-icon"
v-tooltip
data-container="body"
data-placement="left"
:title="tooltipText"
>
<icon name="timer" />
<div class="time-tracking-collapsed-summary">
<div :class="divClass">

View File

@ -27,6 +27,7 @@ function mountAssigneesComponent(mediator) {
mediator,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
},
}),
});

View File

@ -5,6 +5,7 @@
import $ from 'jquery';
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
import ModalStore from './boards/stores/modal_store';
// TODO: remove eventHub hack after code splitting refactor
@ -182,7 +183,7 @@ function UsersSelect(currentUser, els, options = {}) {
return axios.put(issueURL, data)
.then(({ data }) => {
var user;
var user, tooltipTitle;
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
if (data.assignee) {
@ -191,15 +192,17 @@ function UsersSelect(currentUser, els, options = {}) {
username: data.assignee.username,
avatar: data.assignee.avatar_url
};
tooltipTitle = _.escape(user.name);
} else {
user = {
name: 'Unassigned',
username: '',
avatar: ''
};
tooltipTitle = __('Assignee');
}
$value.html(assigneeTemplate(user));
$collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle');
$collapsedSidebar.attr('title', tooltipTitle).tooltip('fixTitle');
return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
});
};

View File

@ -1,18 +0,0 @@
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetPipelineBlocked',
components: {
statusIcon,
},
template: `
<div class="mr-widget-body media">
<status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
</span>
</div>
</div>
`,
};

View File

@ -0,0 +1,25 @@
<script>
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'PipelineFailed',
components: {
statusIcon,
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon
status="warning"
:show-disabled-button="true"
/>
<div class="media-body space-children">
<span class="bold">
{{ s__(`mrWidget|The pipeline for this merge request failed.
Please retry the job or push a new commit to fix the failure`) }}
</span>
</div>
</div>
</template>

View File

@ -1,3 +1,4 @@
<script>
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll';
@ -7,7 +8,10 @@ import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetReadyToMerge',
name: 'ReadyToMerge',
components: {
statusIcon,
},
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
@ -26,9 +30,6 @@ export default {
warningSvg,
};
},
components: {
statusIcon,
},
computed: {
shouldShowMergeWhenPipelineSucceedsText() {
return this.mr.isPipelineActive;
@ -217,136 +218,146 @@ export default {
});
},
},
template: `
<div class="mr-widget-body media">
<status-icon :status="iconClass" />
<div class="media-body">
<div class="mr-widget-body-controls media space-children">
<span class="btn-group append-bottom-5">
<button
@click="handleMergeButtonClick()"
:disabled="isMergeButtonDisabled"
:class="mergeButtonClass"
type="button"
class="qa-merge-button">
<i
v-if="isMakingRequest"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
{{mergeButtonText}}
</button>
<button
v-if="shouldShowMergeOptionsDropdown"
:disabled="isMergeButtonDisabled"
type="button"
class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
data-toggle="dropdown"
aria-label="Select merge moment">
<i
class="fa fa-chevron-down"
aria-hidden="true" />
</button>
<ul
v-if="shouldShowMergeOptionsDropdown"
class="dropdown-menu dropdown-menu-right"
role="menu">
<li>
<a
@click.prevent="handleMergeButtonClick(true)"
class="merge_when_pipeline_succeeds"
href="#">
<span class="media">
<span
v-html="successSvg"
class="merge-opt-icon"
aria-hidden="true"></span>
<span class="media-body merge-opt-title">Merge when pipeline succeeds</span>
</span>
</a>
</li>
<li>
<a
@click.prevent="handleMergeButtonClick(false, true)"
class="accept-merge-request"
href="#">
<span class="media">
<span
v-html="warningSvg"
class="merge-opt-icon"
aria-hidden="true"></span>
<span class="media-body merge-opt-title">Merge immediately</span>
</span>
</a>
</li>
</ul>
</span>
<div class="media-body-wrap space-children">
<template v-if="shouldShowMergeControls()">
<label v-if="mr.canRemoveSourceBranch">
<input
id="remove-source-branch-input"
v-model="removeSourceBranch"
class="js-remove-source-branch-checkbox"
:disabled="isRemoveSourceBranchButtonDisabled"
type="checkbox"/> Remove source branch
</label>
};
</script>
<!-- Placeholder for EE extension of this component -->
<squash-before-merge
v-if="shouldShowSquashBeforeMerge"
:mr="mr"
:is-merge-button-disabled="isMergeButtonDisabled" />
<span
v-if="mr.ffOnlyEnabled"
class="js-fast-forward-message">
Fast-forward merge without a merge commit
</span>
<button
v-else
@click="toggleCommitMessageEditor"
:disabled="isMergeButtonDisabled"
class="js-modify-commit-message-button btn btn-default btn-xs"
type="button">
Modify commit message
</button>
</template>
<template v-else>
<span class="bold js-resolve-mr-widget-items-message">
You can only merge once the items above are resolved
</span>
</template>
</div>
</div>
<div
v-if="showCommitMessageEditor"
class="prepend-top-default commit-message-editor">
<div class="form-group clearfix">
<label
class="control-label"
for="commit-message">
Commit message
<template>
<div class="mr-widget-body media">
<status-icon :status="iconClass" />
<div class="media-body">
<div class="mr-widget-body-controls media space-children">
<span class="btn-group append-bottom-5">
<button
@click="handleMergeButtonClick()"
:disabled="isMergeButtonDisabled"
:class="mergeButtonClass"
type="button"
class="qa-merge-button">
<i
v-if="isMakingRequest"
class="fa fa-spinner fa-spin"
aria-hidden="true"
></i>
{{ mergeButtonText }}
</button>
<button
v-if="shouldShowMergeOptionsDropdown"
:disabled="isMergeButtonDisabled"
type="button"
class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
data-toggle="dropdown"
aria-label="Select merge moment">
<i
class="fa fa-chevron-down"
aria-hidden="true"
></i>
</button>
<ul
v-if="shouldShowMergeOptionsDropdown"
class="dropdown-menu dropdown-menu-right"
role="menu">
<li>
<a
@click.prevent="handleMergeButtonClick(true)"
class="merge_when_pipeline_succeeds"
href="#">
<span class="media">
<span
v-html="successSvg"
class="merge-opt-icon"
aria-hidden="true"></span>
<span class="media-body merge-opt-title">Merge when pipeline succeeds</span>
</span>
</a>
</li>
<li>
<a
@click.prevent="handleMergeButtonClick(false, true)"
class="accept-merge-request"
href="#">
<span class="media">
<span
v-html="warningSvg"
class="merge-opt-icon"
aria-hidden="true"></span>
<span class="media-body merge-opt-title">Merge immediately</span>
</span>
</a>
</li>
</ul>
</span>
<div class="media-body-wrap space-children">
<template v-if="shouldShowMergeControls()">
<label v-if="mr.canRemoveSourceBranch">
<input
id="remove-source-branch-input"
v-model="removeSourceBranch"
class="js-remove-source-branch-checkbox"
:disabled="isRemoveSourceBranchButtonDisabled"
type="checkbox"/> Remove source branch
</label>
<div class="col-sm-10">
<div class="commit-message-container">
<div class="max-width-marker"></div>
<textarea
v-model="commitMessage"
class="form-control js-commit-message"
required="required"
rows="14"
name="Commit message"></textarea>
</div>
<p class="hint">Try to keep the first line under 52 characters and the others under 72</p>
<div class="hint">
<a
@click.prevent="updateCommitMessage"
href="#">{{commitMessageLinkTitle}}</a>
</div>
<!-- Placeholder for EE extension of this component -->
<squash-before-merge
v-if="shouldShowSquashBeforeMerge"
:mr="mr"
:is-merge-button-disabled="isMergeButtonDisabled" />
<span
v-if="mr.ffOnlyEnabled"
class="js-fast-forward-message">
Fast-forward merge without a merge commit
</span>
<button
v-else
@click="toggleCommitMessageEditor"
:disabled="isMergeButtonDisabled"
class="js-modify-commit-message-button btn btn-default btn-xs"
type="button">
Modify commit message
</button>
</template>
<template v-else>
<span class="bold js-resolve-mr-widget-items-message">
You can only merge once the items above are resolved
</span>
</template>
</div>
</div>
<div
v-if="showCommitMessageEditor"
class="prepend-top-default commit-message-editor">
<div class="form-group clearfix">
<label
class="control-label"
for="commit-message">
Commit message
</label>
<div class="col-sm-10">
<div class="commit-message-container">
<div class="max-width-marker"></div>
<textarea
id="commit-message"
v-model="commitMessage"
class="form-control js-commit-message"
required="required"
rows="14"
name="Commit message"></textarea>
</div>
<p class="hint">
Try to keep the first line under 52 characters and the others under 72
</p>
<div class="hint">
<a
@click.prevent="updateCommitMessage"
href="#"
>
{{ commitMessageLinkTitle }}
</a>
</div>
</div>
</div>
</div>
</div>
`,
};
</div>
</template>

View File

@ -27,11 +27,11 @@ export { default as ConflictsState } from './components/states/mr_widget_conflic
export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue';
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue';
export { default as ReadyToMergeState } from './components/states/mr_widget_ready_to_merge';
export { default as ReadyToMergeState } from './components/states/ready_to_merge.vue';
export { default as ShaMismatchState } from './components/states/sha_mismatch.vue';
export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue';
export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
export { default as PipelineFailedState } from './components/states/pipeline_failed.vue';
export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
export { default as RebaseState } from './components/states/mr_widget_rebase.vue';
export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed.vue';

View File

@ -0,0 +1,27 @@
<script>
const calloutVariants = ['danger', 'success', 'info', 'warning'];
export default {
props: {
category: {
type: String,
required: false,
default: calloutVariants[0],
validator: value => calloutVariants.includes(value),
},
message: {
type: String,
required: true,
},
},
};
</script>
<template>
<div
:class="`bs-callout bs-callout-${category}`"
role="alert"
aria-live="assertive"
>
{{ message }}
</div>
</template>

View File

@ -1,52 +1,52 @@
<script>
import ciIcon from './ci_icon.vue';
import tooltip from '../directives/tooltip';
/**
* Renders CI Badge link with CI icon and status text based on
* API response shared between all places where it is used.
*
* Receives status object containing:
* status: {
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
* group:"running" // used for CSS class
* icon: "icon_status_running" // used to render the icon
* label:"running" // used for potential tooltip
* text:"running" // text rendered
* }
*
* Used in:
* - Pipelines table - first column
* - Jobs table - first column
* - Pipeline show view - header
* - Job show view - header
* - MR widget
*/
import CiIcon from './ci_icon.vue';
import tooltip from '../directives/tooltip';
/**
* Renders CI Badge link with CI icon and status text based on
* API response shared between all places where it is used.
*
* Receives status object containing:
* status: {
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
* group:"running" // used for CSS class
* icon: "icon_status_running" // used to render the icon
* label:"running" // used for potential tooltip
* text:"running" // text rendered
* }
*
* Used in:
* - Pipelines table - first column
* - Jobs table - first column
* - Pipeline show view - header
* - Job show view - header
* - MR widget
*/
export default {
components: {
ciIcon,
export default {
components: {
CiIcon,
},
directives: {
tooltip,
},
props: {
status: {
type: Object,
required: true,
},
directives: {
tooltip,
showText: {
type: Boolean,
required: false,
default: true,
},
props: {
status: {
type: Object,
required: true,
},
showText: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
cssClass() {
const className = this.status.group;
return className ? `ci-status ci-${className}` : 'ci-status';
},
computed: {
cssClass() {
const className = this.status.group;
return className ? `ci-status ci-${className}` : 'ci-status';
},
},
};
},
};
</script>
<template>
<a

View File

@ -1,45 +1,44 @@
<script>
import icon from '../../vue_shared/components/icon.vue';
import Icon from '../../vue_shared/components/icon.vue';
/**
* Renders CI icon based on API response shared between all places where it is used.
*
* Receives status object containing:
* status: {
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
* group:"running" // used for CSS class
* icon: "icon_status_running" // used to render the icon
* label:"running" // used for potential tooltip
* text:"running" // text rendered
* }
*
* Used in:
* - Pipelines table Badge
* - Pipelines table mini graph
* - Pipeline graph
* - Pipeline show view badge
* - Jobs table
* - Jobs show view header
* - Jobs show view sidebar
*/
export default {
components: {
icon,
/**
* Renders CI icon based on API response shared between all places where it is used.
*
* Receives status object containing:
* status: {
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
* group:"running" // used for CSS class
* icon: "icon_status_running" // used to render the icon
* label:"running" // used for potential tooltip
* text:"running" // text rendered
* }
*
* Used in:
* - Pipelines table Badge
* - Pipelines table mini graph
* - Pipeline graph
* - Pipeline show view badge
* - Jobs table
* - Jobs show view header
* - Jobs show view sidebar
*/
export default {
components: {
Icon,
},
props: {
status: {
type: Object,
required: true,
},
props: {
status: {
type: Object,
required: true,
},
},
computed: {
cssClass() {
const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
},
computed: {
cssClass() {
const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
},
},
};
},
};
</script>
<template>
<span :class="cssClass">

View File

@ -1,40 +1,50 @@
<script>
/**
* Falls back to the code used in `copy_to_clipboard.js`
*/
import tooltip from '../directives/tooltip';
/**
* Falls back to the code used in `copy_to_clipboard.js`
*
* Renders a button with a clipboard icon that copies the content of `data-clipboard-text`
* when clicked.
*
* @example
* <clipboard-button
* title="Copy to clipbard"
* text="Content to be copied"
* css-class="btn-transparent"
* />
*/
import tooltip from '../directives/tooltip';
export default {
name: 'ClipboardButton',
directives: {
tooltip,
export default {
name: 'ClipboardButton',
directives: {
tooltip,
},
props: {
text: {
type: String,
required: true,
},
props: {
text: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
tooltipPlacement: {
type: String,
required: false,
default: 'top',
},
tooltipContainer: {
type: [String, Boolean],
required: false,
default: false,
},
cssClass: {
type: String,
required: false,
default: 'btn-default',
},
title: {
type: String,
required: true,
},
};
tooltipPlacement: {
type: String,
required: false,
default: 'top',
},
tooltipContainer: {
type: [String, Boolean],
required: false,
default: false,
},
cssClass: {
type: String,
required: false,
default: 'btn-default',
},
},
};
</script>
<template>

View File

@ -1,119 +1,111 @@
<script>
import commitIconSvg from 'icons/_icon_commit.svg';
import userAvatarLink from './user_avatar/user_avatar_link.vue';
import tooltip from '../directives/tooltip';
import icon from '../../vue_shared/components/icon.vue';
import UserAvatarLink from './user_avatar/user_avatar_link.vue';
import tooltip from '../directives/tooltip';
import Icon from '../../vue_shared/components/icon.vue';
export default {
directives: {
tooltip,
export default {
directives: {
tooltip,
},
components: {
UserAvatarLink,
Icon,
},
props: {
/**
* Indicates the existance of a tag.
* Used to render the correct icon, if true will render `fa-tag` icon,
* if false will render a svg sprite fork icon
*/
tag: {
type: Boolean,
required: false,
default: false,
},
components: {
userAvatarLink,
icon,
/**
* If provided is used to render the branch name and url.
* Should contain the following properties:
* name
* ref_url
*/
commitRef: {
type: Object,
required: false,
default: () => ({}),
},
/**
* Used to link to the commit sha.
*/
commitUrl: {
type: String,
required: false,
default: '',
},
props: {
/**
* Indicates the existance of a tag.
* Used to render the correct icon, if true will render `fa-tag` icon,
* if false will render a svg sprite fork icon
*/
tag: {
type: Boolean,
required: false,
default: false,
},
/**
* If provided is used to render the branch name and url.
* Should contain the following properties:
* name
* ref_url
*/
commitRef: {
type: Object,
required: false,
default: () => ({}),
},
/**
* Used to link to the commit sha.
*/
commitUrl: {
type: String,
required: false,
default: '',
},
/**
* Used to show the commit short sha that links to the commit url.
*/
shortSha: {
type: String,
required: false,
default: '',
},
/**
* If provided shows the commit tile.
*/
title: {
type: String,
required: false,
default: '',
},
/**
* If provided renders information about the author of the commit.
* When provided should include:
* `avatar_url` to render the avatar icon
* `web_url` to link to user profile
* `username` to render alt and title tags
*/
author: {
type: Object,
required: false,
default: () => ({}),
},
showBranch: {
type: Boolean,
required: false,
default: true,
},
/**
* Used to show the commit short sha that links to the commit url.
*/
shortSha: {
type: String,
required: false,
default: '',
},
computed: {
/**
* Used to verify if all the properties needed to render the commit
* ref section were provided.
*
* @returns {Boolean}
*/
hasCommitRef() {
return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
},
/**
* Used to verify if all the properties needed to render the commit
* author section were provided.
*
* @returns {Boolean}
*/
hasAuthor() {
return this.author &&
this.author.avatar_url &&
this.author.path &&
this.author.username;
},
/**
* If information about the author is provided will return a string
* to be rendered as the alt attribute of the img tag.
*
* @returns {String}
*/
userImageAltDescription() {
return this.author &&
this.author.username ? `${this.author.username}'s avatar` : null;
},
/**
* If provided shows the commit tile.
*/
title: {
type: String,
required: false,
default: '',
},
created() {
this.commitIconSvg = commitIconSvg;
/**
* If provided renders information about the author of the commit.
* When provided should include:
* `avatar_url` to render the avatar icon
* `web_url` to link to user profile
* `username` to render alt and title tags
*/
author: {
type: Object,
required: false,
default: () => ({}),
},
};
showBranch: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
/**
* Used to verify if all the properties needed to render the commit
* ref section were provided.
*
* @returns {Boolean}
*/
hasCommitRef() {
return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
},
/**
* Used to verify if all the properties needed to render the commit
* author section were provided.
*
* @returns {Boolean}
*/
hasAuthor() {
return this.author && this.author.avatar_url && this.author.path && this.author.username;
},
/**
* If information about the author is provided will return a string
* to be rendered as the alt attribute of the img tag.
*
* @returns {String}
*/
userImageAltDescription() {
return this.author && this.author.username ? `${this.author.username}'s avatar` : null;
},
},
};
</script>
<template>
<div class="branch-commit">
@ -141,11 +133,10 @@
{{ commitRef.name }}
</a>
</template>
<div
v-html="commitIconSvg"
<icon
name="commit"
class="commit-icon js-commit-icon"
>
</div>
/>
<a
class="commit-sha"
@ -175,7 +166,7 @@
</a>
</span>
<span v-else>
Cant find HEAD commit for this branch
Can't find HEAD commit for this branch
</span>
</div>
</div>

View File

@ -1,33 +1,33 @@
<script>
import { __ } from '~/locale';
/**
* Port of detail_behavior expand button.
*
* @example
* <expand-button>
* <template slot="expanded">
* Text goes here.
* </template>
* </expand-button>
*/
export default {
name: 'ExpandButton',
data() {
return {
isCollapsed: true,
};
import { __ } from '~/locale';
/**
* Port of detail_behavior expand button.
*
* @example
* <expand-button>
* <template slot="expanded">
* Text goes here.
* </template>
* </expand-button>
*/
export default {
name: 'ExpandButton',
data() {
return {
isCollapsed: true,
};
},
computed: {
ariaLabel() {
return __('Click to expand text');
},
computed: {
ariaLabel() {
return __('Click to expand text');
},
},
methods: {
onClick() {
this.isCollapsed = !this.isCollapsed;
},
methods: {
onClick() {
this.isCollapsed = !this.isCollapsed;
},
},
};
},
};
</script>
<template>
<span>

View File

@ -1,78 +1,78 @@
<script>
import ciIconBadge from './ci_badge_link.vue';
import loadingIcon from './loading_icon.vue';
import timeagoTooltip from './time_ago_tooltip.vue';
import tooltip from '../directives/tooltip';
import userAvatarImage from './user_avatar/user_avatar_image.vue';
import CiIconBadge from './ci_badge_link.vue';
import LoadingIcon from './loading_icon.vue';
import TimeagoTooltip from './time_ago_tooltip.vue';
import tooltip from '../directives/tooltip';
import UserAvatarImage from './user_avatar/user_avatar_image.vue';
/**
* Renders header component for job and pipeline page based on UI mockups
*
* Used in:
* - job show page
* - pipeline show page
*/
export default {
components: {
ciIconBadge,
loadingIcon,
timeagoTooltip,
userAvatarImage,
/**
* Renders header component for job and pipeline page based on UI mockups
*
* Used in:
* - job show page
* - pipeline show page
*/
export default {
components: {
CiIconBadge,
LoadingIcon,
TimeagoTooltip,
UserAvatarImage,
},
directives: {
tooltip,
},
props: {
status: {
type: Object,
required: true,
},
directives: {
tooltip,
itemName: {
type: String,
required: true,
},
props: {
status: {
type: Object,
required: true,
},
itemName: {
type: String,
required: true,
},
itemId: {
type: Number,
required: true,
},
time: {
type: String,
required: true,
},
user: {
type: Object,
required: false,
default: () => ({}),
},
actions: {
type: Array,
required: false,
default: () => [],
},
hasSidebarButton: {
type: Boolean,
required: false,
default: false,
},
shouldRenderTriggeredLabel: {
type: Boolean,
required: false,
default: true,
},
itemId: {
type: Number,
required: true,
},
time: {
type: String,
required: true,
},
user: {
type: Object,
required: false,
default: () => ({}),
},
actions: {
type: Array,
required: false,
default: () => [],
},
hasSidebarButton: {
type: Boolean,
required: false,
default: false,
},
shouldRenderTriggeredLabel: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
userAvatarAltText() {
return `${this.user.name}'s avatar`;
},
computed: {
userAvatarAltText() {
return `${this.user.name}'s avatar`;
},
},
methods: {
onClickAction(action) {
this.$emit('actionClicked', action);
},
methods: {
onClickAction(action) {
this.$emit('actionClicked', action);
},
};
},
};
</script>
<template>

View File

@ -1,76 +1,75 @@
<script>
/* This is a re-usable vue component for rendering a svg sprite
icon
/* This is a re-usable vue component for rendering a svg sprite
icon
Sample configuration:
Sample configuration:
<icon
name="retry"
:size="32"
css-classes="top"
/>
<icon
name="retry"
:size="32"
css-classes="top"
/>
*/
// only allow classes in images.scss e.g. s12
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
*/
// only allow classes in images.scss e.g. s12
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
export default {
props: {
name: {
type: String,
required: true,
},
export default {
props: {
name: {
type: String,
required: true,
},
size: {
type: Number,
required: false,
default: 16,
validator(value) {
return validSizes.includes(value);
},
},
cssClasses: {
type: String,
required: false,
default: '',
},
width: {
type: Number,
required: false,
default: null,
},
height: {
type: Number,
required: false,
default: null,
},
y: {
type: Number,
required: false,
default: null,
},
x: {
type: Number,
required: false,
default: null,
size: {
type: Number,
required: false,
default: 16,
validator(value) {
return validSizes.includes(value);
},
},
computed: {
spriteHref() {
return `${gon.sprite_icons}#${this.name}`;
},
iconSizeClass() {
return this.size ? `s${this.size}` : '';
},
cssClasses: {
type: String,
required: false,
default: '',
},
};
width: {
type: Number,
required: false,
default: null,
},
height: {
type: Number,
required: false,
default: null,
},
y: {
type: Number,
required: false,
default: null,
},
x: {
type: Number,
required: false,
default: null,
},
},
computed: {
spriteHref() {
return `${gon.sprite_icons}#${this.name}`;
},
iconSizeClass() {
return this.size ? `s${this.size}` : '';
},
},
};
</script>
<template>
@ -79,7 +78,8 @@
:width="width"
:height="height"
:x="x"
:y="y">
:y="y"
>
<use v-bind="{ 'xlink:href':spriteHref }" />
</svg>
</template>

View File

@ -1,4 +1,5 @@
<script>
import $ from 'jquery';
import { __ } from '~/locale';
import LabelsSelect from '~/labels_select';
import LoadingIcon from '../../loading_icon.vue';
@ -98,11 +99,18 @@ export default {
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
handleClick: this.handleClick,
});
$(this.$refs.dropdown).on('hidden.gl.dropdown', this.handleDropdownHidden);
},
methods: {
handleClick(label) {
this.$emit('onLabelClick', label);
},
handleCollapsedValueClick() {
this.$emit('toggleCollapse');
},
handleDropdownHidden() {
this.$emit('onDropdownClose');
},
},
};
</script>
@ -112,6 +120,7 @@ export default {
<dropdown-value-collapsed
v-if="showCreate"
:labels="context.labels"
@onValueClick="handleCollapsedValueClick"
/>
<dropdown-title
:can-edit="canEdit"
@ -133,7 +142,10 @@ export default {
:name="hiddenInputName"
:label="label"
/>
<div class="dropdown">
<div
class="dropdown"
ref="dropdown"
>
<dropdown-button
:ability-name="abilityName"
:field-name="hiddenInputName"

View File

@ -26,6 +26,11 @@ export default {
return labelsString;
},
},
methods: {
handleClick() {
this.$emit('onValueClick');
},
},
};
</script>
@ -36,6 +41,7 @@ export default {
data-placement="left"
data-container="body"
:title="labelsList"
@click="handleClick"
>
<i
aria-hidden="true"

View File

@ -1,18 +1,29 @@
<script>
export default {
name: 'ToggleSidebar',
props: {
collapsed: {
type: Boolean,
required: true,
},
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'ToggleSidebar',
directives: {
tooltip,
},
props: {
collapsed: {
type: Boolean,
required: true,
},
methods: {
toggle() {
this.$emit('toggle');
},
},
computed: {
tooltipLabel() {
return this.collapsed ? __('Expand sidebar') : __('Collapse sidebar');
},
};
},
methods: {
toggle() {
this.$emit('toggle');
},
},
};
</script>
<template>
@ -20,6 +31,10 @@
type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action"
@click="toggle"
v-tooltip
data-container="body"
data-placement="left"
:title="tooltipLabel"
>
<i
aria-label="toggle collapse"

View File

@ -503,3 +503,7 @@ fieldset[disabled] .btn,
@extend %disabled;
}
}
.btn-no-padding {
padding: 0;
}

View File

@ -16,7 +16,7 @@
.nav-header-btn {
padding: 10px $gl-sidebar-padding;
color: inherit;
transition-duration: .3s;
transition-duration: 0.3s;
position: absolute;
top: 0;
cursor: pointer;
@ -137,6 +137,12 @@
}
}
.issuable-sidebar .labels {
.value.dont-hide ~ .selectbox {
padding-top: $gl-padding-8;
}
}
.pikaday-container {
.pika-single {
margin-top: 2px;
@ -151,4 +157,3 @@
.sidebar-collapsed-icon .sidebar-collapsed-value {
font-size: 12px;
}

View File

@ -247,6 +247,7 @@ $btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
$issue-status-expired: $orange-500;
$issuable-sidebar-color: $gl-text-color-secondary;
$sidebar-block-hover-color: #ebebeb;
$group-path-color: #999;
$namespace-kind-color: #aaa;
$panel-heading-link-color: #777;
@ -373,6 +374,8 @@ $dropdown-hover-color: $blue-400;
$link-active-background: rgba(0, 0, 0, 0.04);
$link-hover-background: rgba(0, 0, 0, 0.06);
$inactive-badge-background: rgba(0, 0, 0, 0.08);
$sidebar-toggle-height: 60px;
$sidebar-milestone-toggle-bottom-margin: 10px;
/*
* Buttons

View File

@ -1,39 +1,56 @@
@keyframes fade-out-status {
0%, 50% { opacity: 1; }
100% { opacity: 0; }
0%,
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes blinking-dots {
0% {
background-color: rgba($white-light, 1);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
24px 0 0 0 rgba($white-light, 0.2);
24px 0 0 0 rgba($white-light, 0.2);
}
25% {
background-color: rgba($white-light, 0.4);
box-shadow: 12px 0 0 0 rgba($white-light, 2),
24px 0 0 0 rgba($white-light, 0.2);
24px 0 0 0 rgba($white-light, 0.2);
}
75% {
background-color: rgba($white-light, 0.4);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
24px 0 0 0 rgba($white-light, 1);
24px 0 0 0 rgba($white-light, 1);
}
100% {
background-color: rgba($white-light, 1);
box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
24px 0 0 0 rgba($white-light, 0.2);
24px 0 0 0 rgba($white-light, 0.2);
}
}
@keyframes blinking-scroll-button {
0% { opacity: 0.2; }
25% { opacity: 0.5; }
50% { opacity: 0.7; }
100% { opacity: 1; }
0% {
opacity: 0.2;
}
25% {
opacity: 0.5;
}
50% {
opacity: 0.7;
}
100% {
opacity: 1;
}
}
.build-page {
@ -125,12 +142,12 @@
.btn-scroll.animate {
.first-triangle {
animation: blinking-scroll-button 1s ease infinite;
animation-delay: .3s;
animation-delay: 0.3s;
}
.second-triangle {
animation: blinking-scroll-button 1s ease infinite;
animation-delay: .2s;
animation-delay: 0.2s;
}
.third-triangle {

View File

@ -180,10 +180,6 @@
justify-content: space-between;
align-items: center;
flex-grow: 1;
.merge-request-branches & {
flex-direction: column;
}
}
.commit-content {

View File

@ -160,6 +160,11 @@
}
}
}
.diff-loading-error-block {
padding: $gl-padding * 2 $gl-padding;
text-align: center;
}
}
.image {

View File

@ -187,7 +187,12 @@
padding-left: 10px;
&:hover {
color: $gray-darkest;
color: $gl-text-color;
}
&:hover,
&:focus {
text-decoration: none;
}
}
@ -368,6 +373,14 @@
padding: 15px 0 0;
border-bottom: 0;
overflow: hidden;
&:hover {
background-color: $sidebar-block-hover-color;
}
&.issuable-sidebar-header {
padding-top: 0;
}
}
.participants {
@ -380,8 +393,17 @@
.gutter-toggle {
width: 100%;
height: $sidebar-toggle-height;
margin-left: 0;
padding-left: 25px;
padding-left: 0;
border-bottom: 1px solid $border-gray-dark;
}
a.gutter-toggle {
display: flex;
justify-content: center;
flex-direction: column;
text-align: center;
}
.sidebar-collapsed-icon {
@ -428,10 +450,10 @@
.btn-clipboard {
border: 0;
background: transparent;
color: $issuable-sidebar-color;
&:hover {
background: transparent;
color: $gl-text-color;
}
}

View File

@ -762,3 +762,20 @@
max-width: 100%;
}
}
// Hack alert: we've rewritten `btn` class in a way that
// we've broken it and it is not possible to use with `btn-link`
// which causes a blank button when it's disabled and hovering
// The css in here is the boostrap one
.btn-link-retry {
&[disabled] {
cursor: not-allowed;
box-shadow: none;
opacity: .65;
&:hover {
color: $file-mode-changed;
text-decoration: none;
}
}
}

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