Merge branch 'master' into 'es-module-autosave'

# Conflicts:
#   app/assets/javascripts/issuable_form.js
#   app/assets/javascripts/notes.js
This commit is contained in:
Mike Greiling 2017-10-24 07:52:21 +00:00
commit f7bdfe5098
925 changed files with 22704 additions and 9523 deletions

View File

@ -49,11 +49,11 @@ stages:
- gitlab-org
.tests-metadata-state: &tests-metadata-state
services: []
<<: *dedicated-runner
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
TESTS_METADATA_S3_BUCKET: "gitlab-ce-cache"
before_script:
- source scripts/utils.sh
artifacts:
expire_in: 31d
paths:
@ -80,6 +80,7 @@ stages:
.rspec-metadata: &rspec-metadata
<<: *dedicated-runner
<<: *pull-cache
<<: *except-docs
stage: test
script:
- JOB_NAME=( $CI_JOB_NAME )
@ -109,16 +110,15 @@ stages:
.rspec-metadata-pg: &rspec-metadata-pg
<<: *rspec-metadata
<<: *use-pg
<<: *except-docs
.rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-metadata
<<: *use-mysql
<<: *except-docs
.spinach-metadata: &spinach-metadata
<<: *dedicated-runner
<<: *pull-cache
<<: *except-docs
stage: test
script:
- JOB_NAME=( $CI_JOB_NAME )
@ -141,12 +141,10 @@ stages:
.spinach-metadata-pg: &spinach-metadata-pg
<<: *spinach-metadata
<<: *use-pg
<<: *except-docs
.spinach-metadata-mysql: &spinach-metadata-mysql
<<: *spinach-metadata
<<: *use-mysql
<<: *except-docs
.only-canonical-masters: &only-canonical-masters
only:
@ -157,12 +155,8 @@ stages:
# Trigger a package build in omnibus-gitlab repository
build-package:
image: ruby:2.3-alpine
image: ruby:2.4-alpine
before_script: []
services: []
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
stage: build
cache: {}
when: manual
@ -183,13 +177,9 @@ build-package:
- apk add --update openssl
- wget https://gitlab.com/gitlab-org/gitlab-ce/raw/master/scripts/trigger-build-docs
- chmod 755 trigger-build-docs
services: []
cache: {}
dependencies: []
artifacts: {}
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
GIT_STRATEGY: none
when: manual
only:
@ -222,7 +212,6 @@ review-docs-cleanup:
# Retrieve knapsack and rspec_flaky reports
retrieve-tests-metadata:
<<: *tests-metadata-state
<<: *dedicated-runner
<<: *except-docs
stage: prepare
cache:
@ -240,7 +229,6 @@ retrieve-tests-metadata:
update-tests-metadata:
<<: *tests-metadata-state
<<: *dedicated-runner
<<: *only-canonical-masters
stage: post-test
cache:
@ -305,69 +293,69 @@ setup-test-env:
- public/assets
- tmp/tests
rspec-pg 0 25: *rspec-metadata-pg
rspec-pg 1 25: *rspec-metadata-pg
rspec-pg 2 25: *rspec-metadata-pg
rspec-pg 3 25: *rspec-metadata-pg
rspec-pg 4 25: *rspec-metadata-pg
rspec-pg 5 25: *rspec-metadata-pg
rspec-pg 6 25: *rspec-metadata-pg
rspec-pg 7 25: *rspec-metadata-pg
rspec-pg 8 25: *rspec-metadata-pg
rspec-pg 9 25: *rspec-metadata-pg
rspec-pg 10 25: *rspec-metadata-pg
rspec-pg 11 25: *rspec-metadata-pg
rspec-pg 12 25: *rspec-metadata-pg
rspec-pg 13 25: *rspec-metadata-pg
rspec-pg 14 25: *rspec-metadata-pg
rspec-pg 15 25: *rspec-metadata-pg
rspec-pg 16 25: *rspec-metadata-pg
rspec-pg 17 25: *rspec-metadata-pg
rspec-pg 18 25: *rspec-metadata-pg
rspec-pg 19 25: *rspec-metadata-pg
rspec-pg 20 25: *rspec-metadata-pg
rspec-pg 21 25: *rspec-metadata-pg
rspec-pg 22 25: *rspec-metadata-pg
rspec-pg 23 25: *rspec-metadata-pg
rspec-pg 24 25: *rspec-metadata-pg
rspec-pg 0 26: *rspec-metadata-pg
rspec-pg 1 26: *rspec-metadata-pg
rspec-pg 2 26: *rspec-metadata-pg
rspec-pg 3 26: *rspec-metadata-pg
rspec-pg 4 26: *rspec-metadata-pg
rspec-pg 5 26: *rspec-metadata-pg
rspec-pg 6 26: *rspec-metadata-pg
rspec-pg 7 26: *rspec-metadata-pg
rspec-pg 8 26: *rspec-metadata-pg
rspec-pg 9 26: *rspec-metadata-pg
rspec-pg 10 26: *rspec-metadata-pg
rspec-pg 11 26: *rspec-metadata-pg
rspec-pg 12 26: *rspec-metadata-pg
rspec-pg 13 26: *rspec-metadata-pg
rspec-pg 14 26: *rspec-metadata-pg
rspec-pg 15 26: *rspec-metadata-pg
rspec-pg 16 26: *rspec-metadata-pg
rspec-pg 17 26: *rspec-metadata-pg
rspec-pg 18 26: *rspec-metadata-pg
rspec-pg 19 26: *rspec-metadata-pg
rspec-pg 20 26: *rspec-metadata-pg
rspec-pg 21 26: *rspec-metadata-pg
rspec-pg 22 26: *rspec-metadata-pg
rspec-pg 23 26: *rspec-metadata-pg
rspec-pg 24 26: *rspec-metadata-pg
rspec-pg 25 26: *rspec-metadata-pg
rspec-mysql 0 25: *rspec-metadata-mysql
rspec-mysql 1 25: *rspec-metadata-mysql
rspec-mysql 2 25: *rspec-metadata-mysql
rspec-mysql 3 25: *rspec-metadata-mysql
rspec-mysql 4 25: *rspec-metadata-mysql
rspec-mysql 5 25: *rspec-metadata-mysql
rspec-mysql 6 25: *rspec-metadata-mysql
rspec-mysql 7 25: *rspec-metadata-mysql
rspec-mysql 8 25: *rspec-metadata-mysql
rspec-mysql 9 25: *rspec-metadata-mysql
rspec-mysql 10 25: *rspec-metadata-mysql
rspec-mysql 11 25: *rspec-metadata-mysql
rspec-mysql 12 25: *rspec-metadata-mysql
rspec-mysql 13 25: *rspec-metadata-mysql
rspec-mysql 14 25: *rspec-metadata-mysql
rspec-mysql 15 25: *rspec-metadata-mysql
rspec-mysql 16 25: *rspec-metadata-mysql
rspec-mysql 17 25: *rspec-metadata-mysql
rspec-mysql 18 25: *rspec-metadata-mysql
rspec-mysql 19 25: *rspec-metadata-mysql
rspec-mysql 20 25: *rspec-metadata-mysql
rspec-mysql 21 25: *rspec-metadata-mysql
rspec-mysql 22 25: *rspec-metadata-mysql
rspec-mysql 23 25: *rspec-metadata-mysql
rspec-mysql 24 25: *rspec-metadata-mysql
rspec-mysql 0 26: *rspec-metadata-mysql
rspec-mysql 1 26: *rspec-metadata-mysql
rspec-mysql 2 26: *rspec-metadata-mysql
rspec-mysql 3 26: *rspec-metadata-mysql
rspec-mysql 4 26: *rspec-metadata-mysql
rspec-mysql 5 26: *rspec-metadata-mysql
rspec-mysql 6 26: *rspec-metadata-mysql
rspec-mysql 7 26: *rspec-metadata-mysql
rspec-mysql 8 26: *rspec-metadata-mysql
rspec-mysql 9 26: *rspec-metadata-mysql
rspec-mysql 10 26: *rspec-metadata-mysql
rspec-mysql 11 26: *rspec-metadata-mysql
rspec-mysql 12 26: *rspec-metadata-mysql
rspec-mysql 13 26: *rspec-metadata-mysql
rspec-mysql 14 26: *rspec-metadata-mysql
rspec-mysql 15 26: *rspec-metadata-mysql
rspec-mysql 16 26: *rspec-metadata-mysql
rspec-mysql 17 26: *rspec-metadata-mysql
rspec-mysql 18 26: *rspec-metadata-mysql
rspec-mysql 19 26: *rspec-metadata-mysql
rspec-mysql 20 26: *rspec-metadata-mysql
rspec-mysql 21 26: *rspec-metadata-mysql
rspec-mysql 22 26: *rspec-metadata-mysql
rspec-mysql 23 26: *rspec-metadata-mysql
rspec-mysql 24 26: *rspec-metadata-mysql
rspec-mysql 25 26: *rspec-metadata-mysql
spinach-pg 0 5: *spinach-metadata-pg
spinach-pg 1 5: *spinach-metadata-pg
spinach-pg 2 5: *spinach-metadata-pg
spinach-pg 3 5: *spinach-metadata-pg
spinach-pg 4 5: *spinach-metadata-pg
spinach-pg 0 4: *spinach-metadata-pg
spinach-pg 1 4: *spinach-metadata-pg
spinach-pg 2 4: *spinach-metadata-pg
spinach-pg 3 4: *spinach-metadata-pg
spinach-mysql 0 5: *spinach-metadata-mysql
spinach-mysql 1 5: *spinach-metadata-mysql
spinach-mysql 2 5: *spinach-metadata-mysql
spinach-mysql 3 5: *spinach-metadata-mysql
spinach-mysql 4 5: *spinach-metadata-mysql
spinach-mysql 0 4: *spinach-metadata-mysql
spinach-mysql 1 4: *spinach-metadata-mysql
spinach-mysql 2 4: *spinach-metadata-mysql
spinach-mysql 3 4: *spinach-metadata-mysql
# Static analysis jobs
.ruby-static-analysis: &ruby-static-analysis

View File

@ -624,7 +624,7 @@ Style/PredicateName:
# branches, and conditions.
Metrics/AbcSize:
Enabled: true
Max: 55.25
Max: 54.28
# This cop checks if the length of a block exceeds some maximum value.
Metrics/BlockLength:
@ -665,7 +665,7 @@ Metrics/ParameterLists:
# A complexity metric geared towards measuring complexity for a human reader.
Metrics/PerceivedComplexity:
Enabled: true
Max: 15
Max: 14
# Lint ########################################################################

View File

@ -2,6 +2,204 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.1.0 (2017-10-22)
- [SECURITY] Use a timeout on certain git operations. !14872
- [SECURITY] Move project repositories between namespaces when renaming users.
- [SECURITY] Prevent an open redirect on project pages.
- [SECURITY] Prevent a persistent XSS in user-provided markup.
- [REMOVED] Remove the ability to visit the issue edit form directly. !14523
- [REMOVED] Remove animate.js and label animation.
- [FIXED] Perform prometheus data endpoint requests in parallel. !14003
- [FIXED] Escape quotes in git username. !14020 (Brandon Everett)
- [FIXED] Fixed non-UTF-8 valid branch names from causing an error. !14090
- [FIXED] Read import sources from setting at first initialization. !14141 (Visay Keo)
- [FIXED] Display full pre-receive and post-receive hook output in GitLab UI. !14222 (Robin Bobbitt)
- [FIXED] Fix incorrect X-axis labels in Prometheus graphs. !14258
- [FIXED] Fix the default branches sorting to actually be 'Last updated'. !14295
- [FIXED] Fixes project denial of service via gitmodules using Extended ASCII. !14301
- [FIXED] Fix the filesystem shard health check to check all configured shards. !14341
- [FIXED] Compare email addresses case insensitively when verifying GPG signatures. !14376 (Tim Bishop)
- [FIXED] Allow the git circuit breaker to correctly handle missing repository storages. !14417
- [FIXED] Fix `rake gitlab:incoming_email:check` and make it report the actual error. !14423
- [FIXED] Does not check if an invariant hashed storage path exists on disk when renaming projects. !14428
- [FIXED] Also reserve refs/replace after importing a project. !14436
- [FIXED] Fix profile image orientation based on EXIF data gvieira37. !14461 (gvieira37)
- [FIXED] Move the deployment flag content to the left when deployment marker is near the end. !14514
- [FIXED] Fix notes type created from import. This should fix some missing notes issues from imported projects. !14524
- [FIXED] Fix bottom spacing for dropdowns that open upwards. !14535
- [FIXED] Adjusts tag link to avoid underlining spaces. !14544 (Guilherme Vieira)
- [FIXED] Add missing space in Sidekiq memory killer log message. !14553 (Benjamin Drung)
- [FIXED] Ensure no exception is raised when Raven tries to get the current user in API context. !14580
- [FIXED] Fix edit project service cancel button position. !14596 (Matt Coleman)
- [FIXED] Fix case sensitive email confirmation on signup. !14606 (robdel12)
- [FIXED] Whitelist authorized_keys.lock in the gitlab:check rake task. !14624
- [FIXED] Allow merge in MR widget with no pipeline but using "Only allow merge requests to be merged if the pipeline succeeds". !14633
- [FIXED] Fix navigation dropdown close animation on mobile screens. !14649
- [FIXED] Fix the project import with issues and milestones. !14657
- [FIXED] Use explicit boolean true attribute for show-disabled-button in Vue files. !14672
- [FIXED] Make tabs on top scrollable on admin dashboard. !14685 (Takuya Noguchi)
- [FIXED] Fix broken Y-axis scaling in some Prometheus graphs. !14693
- [FIXED] Search or compare LDAP DNs case-insensitively and ignore excess whitespace. !14697
- [FIXED] Allow prometheus graphs to correctly handle NaN values. !14741
- [FIXED] Don't show an "Unsubscribe" link in snippet comment notifications. !14764
- [FIXED] Fixed duplicate notifications when added multiple labels on an issue. !14798
- [FIXED] Fix alignment for indeterminate marker in dropdowns. !14809
- [FIXED] Fix error when updating a forked project with deleted `ForkedProjectLink`. !14916
- [FIXED] Correctly render asset path for locales with a region. !14924
- [FIXED] Fix the external URLs generated for online view of HTML artifacts. !14977
- [FIXED] Reschedule merge request diff background migrations to catch failures from 9.5 run.
- [FIXED] fix merge request widget status icon for failed CI.
- [FIXED] Fix the number representing the amount of commits related to a push event.
- [FIXED] Sync up hover and legend data across all graphs for the prometheus dashboard.
- [FIXED] Fixes mini pipeline graph in commit view.
- [FIXED] Fix comment deletion confirmation dialog typo.
- [FIXED] Fix project snippets breadcrumb link.
- [FIXED] Make usage ping scheduling more robust.
- [FIXED] Make "merge ongoing" check more consistent.
- [FIXED] Add 1000+ counters to job page.
- [FIXED] Fixed issue/merge request breadcrumb titles not having links.
- [FIXED] Fixed commit avatars being centered vertically.
- [FIXED] Tooltips in the commit info box now all face the same direction. (Jedidiah Broadbent)
- [FIXED] Fixed navbar title colors leaking out of the navbar.
- [FIXED] Fix bug that caused merge requests with diff notes imported from Bitbucket to raise errors.
- [FIXED] Correctly detect multiple issue URLs after 'Closes...' in MR descriptions.
- [FIXED] Set default scope on PATs that don't have one set to allow them to be revoked.
- [FIXED] Fix application setting to cache nil object.
- [FIXED] Fix image diff swipe handle offset to correctly align with the frame.
- [FIXED] Force non diff resolved discussion to display when collapse toggled.
- [FIXED] Fix resolved discussions not expanding on side by side view.
- [FIXED] Fixed the sidebar scrollbar overlapping links.
- [FIXED] Issue board tooltips are now the correct width when the column is collapsed. (Jedidiah Broadbent)
- [FIXED] Improve autodevops banner UX and render it only in project page.
- [FIXED] Fix typo in cycle analytics breaking time component.
- [FIXED] Force two up view to load by default for image diffs.
- [FIXED] Fixed milestone breadcrumb links.
- [FIXED] Fixed group sort dropdown defaulting to empty.
- [FIXED] Fixed notes not being scrolled to in merge requests.
- [FIXED] Adds Event polyfill for IE11.
- [FIXED] Update native unicode emojis to always render as normal text (previously could render italicized). (Branka Martinovic)
- [FIXED] Sort JobsController by id, not created_at.
- [FIXED] Fix revision and total size missing for Container Registry.
- [FIXED] Fixed milestone issuable assignee link URL.
- [FIXED] Fixed breadcrumbs container expanding in side-by-side diff view.
- [FIXED] Fixed merge request widget merged & closed date tooltip text.
- [FIXED] Prevent creating multiple ApplicationSetting instances.
- [FIXED] Fix username and ID not logging in production_json.log for Git activity.
- [FIXED] Make Redcarpet Markdown renderer thread-safe.
- [FIXED] Two factor auth messages in settings no longer overlap the button. (Jedidiah Broadbent)
- [FIXED] Made the "remember me" check boxes have consistent styles and alignment. (Jedidiah Broadbent)
- [FIXED] Prevent branches or tags from starting with invalid characters (e.g. -, .).
- [DEPRECATED] Removed two legacy config options. (Daniel Voogsgerd)
- [CHANGED] Show notes number more user-friendly in the graph. !13949 (Vladislav Kaverin)
- [CHANGED] Link SAML users to LDAP by email. !14216
- [CHANGED] Display whether branch has been merged when deleting protected branch. !14220
- [CHANGED] Make the labels in the Compare form less confusing. !14225
- [CHANGED] Confirmation email shows link as text instead of human readable text. !14243 (bitsapien)
- [CHANGED] Return only group's members in user dropdowns on issuables list pages. !14249
- [CHANGED] Added defaults for protected branches dropdowns on the repository settings. !14278
- [CHANGED] Show confirmation modal before deleting account. !14360
- [CHANGED] Allow creating merge requests across a fork network. !14422
- [CHANGED] Re-arrange script HTML tags before template HTML tags in .vue files. !14671
- [CHANGED] Create idea of read-only database. !14688
- [CHANGED] Add active states to nav bar counters.
- [CHANGED] Add view replaced file link for image diffs.
- [CHANGED] Adjust tooltips to adhere to 8px grid and make them more readable.
- [CHANGED] breadcrumbs receives padding when double lined.
- [CHANGED] Allow developer role to admin milestones.
- [CHANGED] Stop using Sidekiq for updating Key#last_used_at.
- [CHANGED] Include GitLab full name in Slack messages.
- [ADDED] Expose last pipeline details in API response when getting a single commit. !13521 (Mehdi Lahmam (@mehlah))
- [ADDED] Allow to use same periods for different housekeeping tasks (effectively skipping the lesser task). !13711 (cernvcs)
- [ADDED] Add GitLab-Pages version to Admin Dashboard. !14040 (travismiller)
- [ADDED] Commenting on image diffs. !14061
- [ADDED] Script to migrate project's repositories to new Hashed Storage. !14067
- [ADDED] Hide close MR button after merge without reloading page. !14122 (Jacopo Beschi @jacopo-beschi)
- [ADDED] Add Gitaly version to Admin Dashboard. !14313 (Jacopo Beschi @jacopo-beschi)
- [ADDED] Add 'closed_at' attribute to Issues API. !14316 (Vitaliy @blackst0ne Klachkov)
- [ADDED] Add tooltip for milestone due date to issue and merge request lists. !14318 (Vitaliy @blackst0ne Klachkov)
- [ADDED] Improve list of sorting options. !14320 (Vitaliy @blackst0ne Klachkov)
- [ADDED] Add client and call site metadata to Gitaly calls for better traceability. !14332
- [ADDED] Strip gitlab-runner section markers in build trace HTML view. !14393
- [ADDED] Add online view of HTML artifacts for public projects. !14399
- [ADDED] Create Kubernetes cluster on GKE from k8s service. !14470
- [ADDED] Add support for GPG subkeys in signature verification. !14517
- [ADDED] Parse and store gitlab-runner timestamped section markers. !14551
- [ADDED] Add "implements" to the default issue closing message regex. !14612 (Guilherme Vieira)
- [ADDED] Replace `tag: true` into `:tag` in the specs. !14653 (Jacopo Beschi @jacopo-beschi)
- [ADDED] Discussion lock for issues and merge requests.
- [ADDED] Add an API endpoint to determine the forks of a project.
- [ADDED] Add help text to runner edit: tags should be separated by commas. (Brendan O'Leary)
- [ADDED] Only copy old/new code when selecting left/right side of parallel diff.
- [ADDED] Expose avatar_url when requesting list of projects from API with simple=true.
- [ADDED] A confirmation email is now sent when adding a secondary email address. (digitalmoksha)
- [ADDED] Move Custom merge methods from EE.
- [ADDED] Makes @mentions links have a different styling for better separation.
- [ADDED] Added tabs to dashboard/projects to easily switch to personal projects.
- [OTHER] Extract AutocompleteController#users into finder. !13778 (Maxim Rydkin, Mayra Cabrera)
- [OTHER] Replace 'project/wiki.feature' spinach test with an rspec analog. !13856 (Vitaliy @blackst0ne Klachkov)
- [OTHER] Expand docs for changing username or group path. !13914
- [OTHER] Move `lib/ci` to `lib/gitlab/ci`. !14078 (Maxim Rydkin)
- [OTHER] Decrease Cyclomatic Complexity threshold to 13. !14152 (Maxim Rydkin)
- [OTHER] Decrease Perceived Complexity threshold to 15. !14160 (Maxim Rydkin)
- [OTHER] Replace project/group_links.feature spinach test with an rspec analog. !14169 (Vitaliy @blackst0ne Klachkov)
- [OTHER] Replace the project/milestone.feature spinach test with an rspec analog. !14171 (Vitaliy @blackst0ne Klachkov)
- [OTHER] Replace the profile/emails.feature spinach test with an rspec analog. !14172 (Vitaliy @blackst0ne Klachkov)
- [OTHER] Replace the project/team_management.feature spinach test with an rspec analog. !14173 (Vitaliy @blackst0ne Klachkov)
- [OTHER] Replace the 'project/merge_requests/accept.feature' spinach test with an rspec analog. !14176 (Vitaliy @blackst0ne Klachkov)
- [OTHER] Replace the 'project/builds/summary.feature' spinach test with an rspec analog. !14177 (Vitaliy @blackst0ne Klachkov)
- [OTHER] Optimize the boards' issues fetching. !14198
- [OTHER] Replace the 'project/merge_requests/revert.feature' spinach test with an rspec analog. !14201 (Vitaliy @blackst0ne Klachkov)
- [OTHER] Replace the 'project/issues/award_emoji.feature' spinach test with an rspec analog. !14202 (Vitaliy @blackst0ne Klachkov)
- [OTHER] Replace the 'profile/active_tab.feature' spinach test with an rspec analog. !14239 (Vitaliy @blackst0ne Klachkov)
- [OTHER] Replace the 'search.feature' spinach test with an rspec analog. !14248 (Vitaliy @blackst0ne Klachkov)
- [OTHER] Load sidebar participants avatars only when visible. !14270
- [OTHER] Adds gitlab features and components to usage ping data. !14305
- [OTHER] Replace the 'project/archived.feature' spinach test with an rspec analog. !14322 (Vitaliy @blackst0ne Klachkov)
- [OTHER] Replace the 'project/commits/revert.feature' spinach test with an rspec analog. !14325 (Vitaliy @blackst0ne Klachkov)
- [OTHER] Replace the 'project/snippets.feature' spinach test with an rspec analog. !14326 (Vitaliy @blackst0ne Klachkov)
- [OTHER] Add link to OpenID Connect documentation. !14368 (Markus Koller)
- [OTHER] Upgrade doorkeeper-openid_connect. !14372 (Markus Koller)
- [OTHER] Upgrade gitlab-markup gem. !14395 (Markus Koller)
- [OTHER] Index projects on repository storage. !14414
- [OTHER] Replace the 'project/shortcuts.feature' spinach test with an rspec analog. !14431 (Vitaliy @blackst0ne Klachkov)
- [OTHER] Replace the 'project/service.feature' spinach test with an rspec analog. !14432 (Vitaliy @blackst0ne Klachkov)
- [OTHER] Improve GitHub import performance. !14445
- [OTHER] Add basic sprintf implementation to JavaScript. !14506
- [OTHER] Replace the 'project/merge_requests.feature' spinach test with an rspec analog. !14621 (Vitaliy @blackst0ne Klachkov)
- [OTHER] Update GitLab Pages to v0.6.0. !14630
- [OTHER] Add documentation to summarise project archiving. !14650
- [OTHER] Remove 'Repo' prefix from API entites. !14694 (Vitaliy @blackst0ne Klachkov)
- [OTHER] Removes cycle analytics service and store from global namespace.
- [OTHER] Improves i18n for Auto Devops callout.
- [OTHER] Exports common_utils utility functions as modules.
- [OTHER] Use `simple=true` for projects API in Projects dropdown for better search performance.
- [OTHER] Change index on ci_builds to optimize Jobs Controller.
- [OTHER] Add index for merge_requests.merge_commit_sha.
- [OTHER] Add (partial) index on Labels.template.
- [OTHER] Cache issue and MR template names in Redis.
- [OTHER] changed dashed border button color to be darker.
- [OTHER] Speed up permission checks.
- [OTHER] Fix docs for lightweight tag creation via API.
- [OTHER] Clarify artifact download via the API only accepts branch or tag name for ref.
- [OTHER] Change recommended MySQL version to 5.6.
- [OTHER] Bump google-api-client Gem from 0.8.6 to 0.13.6.
- [OTHER] Detect when changelog entries are invalid.
- [OTHER] Use a UNION ALL for getting merge request notes.
- [OTHER] Remove an index on ci_builds meant to be only temporary.
- [OTHER] Remove a SQL query from the todos index page.
- Support custom attributes on users. !13038 (Markus Koller)
- made read-only APIs for public merge requests available without authentication. !13291 (haseebeqx)
- Hide read_registry scope when registry is disabled on instance. !13314 (Robin Bobbitt)
- creation of keys moved to services. !13331 (haseebeqx)
- Add username as GL_USERNAME in hooks.
## 10.0.4 (2017-10-16)
- [SECURITY] Move project repositories between namespaces when renaming users.
- [SECURITY] Prevent an open redirect on project pages.
- [SECURITY] Prevent a persistent XSS in user-provided markup.
## 10.0.3 (2017-10-05)
- [FIXED] find_user Users helper method no longer overrides find_user API helper method. !14418
@ -212,6 +410,14 @@ entry.
- Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi)
- [BUGIFX] Improves subgroup creation permissions. !13418
## 9.5.9 (2017-10-16)
- [SECURITY] Move project repositories between namespaces when renaming users.
- [SECURITY] Prevent an open redirect on project pages.
- [SECURITY] Prevent a persistent XSS in user-provided markup.
- [FIXED] Allow using newlines in pipeline email service recipients. !14250
- Escape user name in filtered search bar.
## 9.5.8 (2017-10-04)
- [FIXED] Fixed fork button being disabled for users who can fork to a group.
@ -457,6 +663,15 @@ entry.
- Use a specialized class for querying events to improve performance.
- Update build badges to be pipeline badges and display passing instead of success.
## 9.4.7 (2017-10-16)
- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller)
- [SECURITY] Move project repositories between namespaces when renaming users.
- [SECURITY] Prevent an open redirect on project pages.
- [SECURITY] Prevent a persistent XSS in user-provided markup.
- [FIXED] Allow using newlines in pipeline email service recipients. !14250
- Escape user name in filtered search bar.
## 9.4.6 (2017-09-06)
- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller)

View File

@ -1 +1 @@
0.46.0
0.49.0

View File

@ -102,7 +102,7 @@ gem 'fog-google', '~> 0.5'
gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1'
gem 'fog-aliyun', '~> 0.1.0'
gem 'fog-aliyun', '~> 0.2.0'
# for Google storage
gem 'google-api-client', '~> 0.13.6'
@ -281,7 +281,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~>0.7.0.beta14'
gem 'prometheus-client-mmap', '~>0.7.0.beta17'
gem 'raindrops', '~> 0.18'
end
@ -398,7 +398,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.41.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.45.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false

View File

@ -214,7 +214,7 @@ GEM
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
fog-aliyun (0.1.0)
fog-aliyun (0.2.0)
fog-core (~> 1.27)
fog-json (~> 1.0)
ipaddress (~> 0.8)
@ -273,7 +273,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.41.0)
gitaly-proto (0.45.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@ -324,7 +324,9 @@ GEM
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
google-protobuf (3.4.0.2)
google-protobuf (3.4.1.1)
googleapis-common-protos-types (1.0.0)
google-protobuf (~> 3.0)
googleauth (0.5.3)
faraday (~> 0.12)
jwt (~> 1.4)
@ -351,8 +353,9 @@ GEM
rake
grape_logging (1.7.0)
grape
grpc (1.6.0)
grpc (1.6.6)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
googleauth (~> 0.5.1)
haml (4.0.7)
tilt
@ -620,7 +623,7 @@ GEM
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.7.0.beta14)
prometheus-client-mmap (0.7.0.beta17)
mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4)
coderay (~> 1.1.0)
@ -1012,7 +1015,7 @@ DEPENDENCIES
flay (~> 2.8.0)
flipper (~> 0.10.2)
flipper-active_record (~> 0.10.2)
fog-aliyun (~> 0.1.0)
fog-aliyun (~> 0.2.0)
fog-aws (~> 1.4)
fog-core (~> 1.44)
fog-google (~> 0.5)
@ -1027,7 +1030,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.41.0)
gitaly-proto (~> 0.45.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
@ -1103,7 +1106,7 @@ DEPENDENCIES
pg (~> 0.18.2)
poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.7)
prometheus-client-mmap (~> 0.7.0.beta14)
prometheus-client-mmap (~> 0.7.0.beta17)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)

View File

@ -1,35 +1,3 @@
# GitLab Maintenance Policy
GitLab follows the [Semantic Versioning](http://semver.org/) for its releases:
`(Major).(Minor).(Patch)` in a [pragmatic way].
- **Major version**: Whenever there is something significant or any backwards
incompatible changes are introduced to the public API.
- **Minor version**: When new, backwards compatible functionality is introduced
to the public API or a minor feature is introduced, or when a set of smaller
features is rolled out.
- **Patch number**: When backwards compatible bug fixes are introduced that fix
incorrect behavior.
The current stable release will receive security patches and bug fixes
(eg. `8.9.0` -> `8.9.1`). Feature releases will mark the next supported stable
release where the minor version is increased numerically by increments of one
(eg. `8.9 -> 8.10`).
Our current policy is to support one stable release at any given time, but for
medium-level security issues, we may consider [backporting to the previous two
monthly releases][rel-sec].
We encourage everyone to run the latest stable release to ensure that you can
easily upgrade to the most secure and feature-rich GitLab experience. In order
to make sure you can easily run the most recent stable release, we are working
hard to keep the update process simple and reliable.
More information about the release procedures can be found in our
[release-tools documentation][rel]. You may also want to read our
[Responsible Disclosure Policy][disclosure].
[rel-sec]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/security.md#backporting
[rel]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/
[disclosure]: https://about.gitlab.com/disclosure/
[pragmatic way]: https://gist.github.com/jashkenas/cbd2b088e20279ae2c8e
See [doc/policy/maintenance.md](doc/policy/maintenance.md)

View File

@ -1 +1 @@
10.1.0-pre
10.2.0-pre

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -15,6 +15,7 @@ const Api = {
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath)
@ -123,6 +124,19 @@ const Api = {
});
},
branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', id)
.replace(':branch', branch);
return this.wrapAjaxCall({
url,
type: 'GET',
contentType: 'application/json; charset=utf-8',
dataType: 'json',
});
},
// Return text for a specific license
licenseText(key, data, callback) {
const url = Api.buildUrl(Api.licensePath)

View File

@ -1,6 +1,5 @@
/* eslint-disable func-names, object-shorthand, prefer-arrow-callback */
/* global Dropzone */
import Dropzone from 'dropzone';
import '../lib/utils/url_utility';
import { HIDDEN_CLASS } from '../lib/utils/constants';
import csrf from '../lib/utils/csrf';

View File

@ -9,6 +9,7 @@ import Flash from '../../flash';
import eventHub from '../../sidebar/event_hub';
import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
import Assignees from '../../sidebar/components/assignees/assignees';
import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue';
const Store = gl.issueBoards.BoardsStore;
@ -113,7 +114,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({
mounted () {
new IssuableContext(this.currentUser);
new MilestoneSelect();
new gl.DueDateSelectors();
new DueDateSelectors();
new LabelsSelect();
new Sidebar();
gl.Subscription.bindAll('.subscription');

View File

@ -7,7 +7,7 @@ class BoardService {
this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
issues: {
method: 'GET',
url: `${gon.relative_url_root}/boards/${boardId}/issues.json`,
url: `${gon.relative_url_root}/-/boards/${boardId}/issues.json`,
}
});
this.lists = Vue.resource(`${listsEndpoint}{/id}`, {}, {
@ -16,7 +16,7 @@ class BoardService {
url: `${listsEndpoint}/generate.json`
}
});
this.issue = Vue.resource(`${gon.relative_url_root}/boards/${boardId}/issues{/id}`, {});
this.issue = Vue.resource(`${gon.relative_url_root}/-/boards/${boardId}/issues{/id}`, {});
this.issues = Vue.resource(`${listsEndpoint}{/id}/issues`, {}, {
bulkUpdate: {
method: 'POST',

View File

@ -1,33 +1,28 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, no-else-return, object-shorthand, comma-dangle, max-len */
export default function initBroadcastMessagesForm() {
$('input#broadcast_message_color').on('input', function onMessageColorInput() {
const previewColor = $(this).val();
$('div.broadcast-message-preview').css('background-color', previewColor);
});
$(function() {
var previewPath;
$('input#broadcast_message_color').on('input', function() {
var previewColor;
previewColor = $(this).val();
return $('div.broadcast-message-preview').css('background-color', previewColor);
$('input#broadcast_message_font').on('input', function onMessageFontInput() {
const previewColor = $(this).val();
$('div.broadcast-message-preview').css('color', previewColor);
});
$('input#broadcast_message_font').on('input', function() {
var previewColor;
previewColor = $(this).val();
return $('div.broadcast-message-preview').css('color', previewColor);
});
previewPath = $('textarea#broadcast_message_message').data('preview-path');
return $('textarea#broadcast_message_message').on('input', function() {
var message;
message = $(this).val();
const previewPath = $('textarea#broadcast_message_message').data('preview-path');
$('textarea#broadcast_message_message').on('input', _.debounce(function onMessageInput() {
const message = $(this).val();
if (message === '') {
return $('.js-broadcast-message-preview').text("Your message here");
$('.js-broadcast-message-preview').text('Your message here');
} else {
return $.ajax({
$.ajax({
url: previewPath,
type: "POST",
type: 'POST',
data: {
broadcast_message: {
message: message
}
}
broadcast_message: { message },
},
});
}
});
});
}, 250));
}

View File

@ -3,7 +3,8 @@ import Visibility from 'visibilityjs';
import axios from 'axios';
import Poll from './lib/utils/poll';
import { s__ } from './locale';
import './flash';
import initSettingsPanels from './settings_panels';
import Flash from './flash';
/**
* Cluster page has 2 separate parts:
@ -24,6 +25,8 @@ class ClusterService {
export default class Clusters {
constructor() {
initSettingsPanels();
const dataset = document.querySelector('.js-edit-cluster-form').dataset;
this.state = {

View File

@ -25,6 +25,11 @@
type: String,
required: true,
},
viewType: {
type: String,
required: false,
default: 'child',
},
},
mixins: [
pipelinesMixin,
@ -110,6 +115,7 @@
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
/>
</div>
</div>

View File

@ -1,5 +1,3 @@
/* eslint-disable class-methods-use-this */
import './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
import SingleFileDiff from './single_file_diff';
@ -8,7 +6,7 @@ import imageDiffHelper from './image_diff/helpers/index';
const UNFOLD_COUNT = 20;
let isBound = false;
class Diff {
export default class Diff {
constructor() {
const $diffFile = $('.files .diff-file');
@ -104,7 +102,7 @@ class Diff {
}
this.highlightSelectedLine();
}
// eslint-disable-next-line class-methods-use-this
handleParallelLineDown(e) {
const line = $(e.currentTarget);
const table = line.closest('table');
@ -116,11 +114,11 @@ class Diff {
table.addClass(`${lineClass}-selected`);
}
}
// eslint-disable-next-line class-methods-use-this
diffViewType() {
return $('.inline-parallel-buttons a.active').data('view-type');
}
// eslint-disable-next-line class-methods-use-this
lineNumbers(line) {
const children = line.find('.diff-line-num').toArray();
if (children.length !== 2) {
@ -128,7 +126,7 @@ class Diff {
}
return children.map(elm => parseInt($(elm).data('linenumber'), 10) || 0);
}
// eslint-disable-next-line class-methods-use-this
highlightSelectedLine() {
const hash = gl.utils.getLocationHash();
const $diffFiles = $('.diff-file');
@ -141,6 +139,3 @@ class Diff {
}
}
}
window.gl = window.gl || {};
window.gl.Diff = Diff;

View File

@ -1,8 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
/* global ProjectSelect */
/* global ShortcutsNavigation */
/* global IssuableIndex */
/* global ShortcutsIssuable */
/* global Milestone */
/* global IssuableForm */
/* global LabelsSelect */
@ -10,7 +8,8 @@
/* global NewBranchForm */
/* global NotificationsForm */
/* global NotificationsDropdown */
/* global GroupAvatar */
import groupAvatar from './group_avatar';
import GroupLabelSubscription from './group_label_subscription';
/* global LineHighlighter */
import BuildArtifacts from './build_artifacts';
import CILintEditor from './ci_lint_editor';
@ -31,10 +30,7 @@ import CILintEditor from './ci_lint_editor';
/* global ProjectImport */
import Labels from './labels';
import LabelManager from './label_manager';
/* global Shortcuts */
/* global ShortcutsFindFile */
/* global Sidebar */
/* global ShortcutsWiki */
import CommitsList from './commits';
import Issue from './issue';
@ -70,6 +66,7 @@ import initSettingsPanels from './settings_panels';
import initExperimentalFlags from './experimental_flags';
import OAuthRememberMe from './oauth_remember_me';
import PerformanceBar from './performance_bar';
import initBroadcastMessagesForm from './broadcast_message';
import initNotes from './init_notes';
import initLegacyFilters from './init_legacy_filters';
import initIssuableSidebar from './init_issuable_sidebar';
@ -77,12 +74,21 @@ import initProjectVisibilitySelector from './project_visibility';
import GpgBadges from './gpg_badges';
import UserFeatureHelper from './helpers/user_feature_helper';
import initChangesDropdown from './init_changes_dropdown';
import NewGroupChild from './groups/new_group_child';
import AbuseReports from './abuse_reports';
import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
import AjaxLoadingSpinner from './ajax_loading_spinner';
import GlFieldErrors from './gl_field_errors';
import GLForm from './gl_form';
import Shortcuts from './shortcuts';
import ShortcutsNavigation from './shortcuts_navigation';
import ShortcutsFindFile from './shortcuts_find_file';
import ShortcutsIssuable from './shortcuts_issuable';
import U2FAuthenticate from './u2f/authenticate';
import Members from './members';
import memberExpirationDate from './member_expiration_date';
import DueDateSelectors from './due_date_select';
import Diff from './diff';
(function() {
var Dispatcher;
@ -166,9 +172,6 @@ import U2FAuthenticate from './u2f/authenticate';
const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
filteredSearchManager.setup();
}
if (page === 'projects:merge_requests:index') {
new UserCallout({ setCalloutPerProject: true });
}
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
IssuableIndex.init(pagePrefix);
@ -232,11 +235,11 @@ import U2FAuthenticate from './u2f/authenticate';
case 'groups:milestones:edit':
case 'groups:milestones:update':
new ZenMode();
new gl.DueDateSelectors();
new DueDateSelectors();
new GLForm($('.milestone-form'), true);
break;
case 'projects:compare:show':
new gl.Diff();
new Diff();
initChangesDropdown();
break;
case 'projects:branches:new':
@ -272,7 +275,7 @@ import U2FAuthenticate from './u2f/authenticate';
}
case 'projects:merge_requests:creations:diffs':
case 'projects:merge_requests:edit':
new gl.Diff();
new Diff();
shortcut_handler = new ShortcutsNavigation();
new GLForm($('.merge-request-form'), true);
new IssuableForm($('.merge-request-form'));
@ -306,7 +309,7 @@ import U2FAuthenticate from './u2f/authenticate';
new GLForm($('.release-form'), true);
break;
case 'projects:merge_requests:show':
new gl.Diff();
new Diff();
shortcut_handler = new ShortcutsIssuable(true);
new ZenMode();
@ -322,7 +325,7 @@ import U2FAuthenticate from './u2f/authenticate';
new gl.Activities();
break;
case 'projects:commit:show':
new gl.Diff();
new Diff();
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
new MiniPipelineGraph({
@ -350,7 +353,10 @@ import U2FAuthenticate from './u2f/authenticate';
case 'projects:show':
shortcut_handler = new ShortcutsNavigation();
new NotificationsForm();
new UserCallout({ setCalloutPerProject: true });
new UserCallout({
setCalloutPerProject: true,
className: 'js-autodevops-banner',
});
if ($('#tree-slider').length) new TreeView();
if ($('.blob-viewer').length) new BlobViewer();
@ -370,9 +376,6 @@ import U2FAuthenticate from './u2f/authenticate';
case 'projects:pipelines:new':
new NewBranchForm($('.js-new-pipeline-form'));
break;
case 'projects:pipelines:index':
new UserCallout({ setCalloutPerProject: true });
break;
case 'projects:pipelines:builds':
case 'projects:pipelines:failures':
case 'projects:pipelines:show':
@ -393,21 +396,26 @@ import U2FAuthenticate from './u2f/authenticate';
new gl.Activities();
break;
case 'groups:show':
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
shortcut_handler = new ShortcutsNavigation();
new NotificationsForm();
new NotificationsDropdown();
new ProjectsList();
if (newGroupChildWrapper) {
new NewGroupChild(newGroupChildWrapper);
}
break;
case 'groups:group_members:index':
new gl.MemberExpirationDate();
new gl.Members();
memberExpirationDate();
new Members();
new UsersSelect();
break;
case 'projects:project_members:index':
new gl.MemberExpirationDate('.js-access-expiration-date-groups');
memberExpirationDate('.js-access-expiration-date-groups');
new GroupsSelect();
new gl.MemberExpirationDate();
new gl.Members();
memberExpirationDate();
new Members();
new UsersSelect();
break;
case 'groups:new':
@ -416,11 +424,11 @@ import U2FAuthenticate from './u2f/authenticate';
case 'admin:groups:create':
BindInOut.initAll();
new Group();
new GroupAvatar();
groupAvatar();
break;
case 'groups:edit':
case 'admin:groups:edit':
new GroupAvatar();
groupAvatar();
break;
case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation();
@ -430,7 +438,6 @@ import U2FAuthenticate from './u2f/authenticate';
new TreeView();
new BlobViewer();
new NewCommitForm($('.js-create-dir-form'));
new UserCallout({ setCalloutPerProject: true });
$('#tree-slider').waitForImages(function() {
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
});
@ -468,7 +475,7 @@ import U2FAuthenticate from './u2f/authenticate';
const $el = $(el);
if ($el.find('.dropdown-group-label').length) {
new gl.GroupLabelSubscription($el);
new GroupLabelSubscription($el);
} else {
new gl.ProjectLabelSubscription($el);
}
@ -528,7 +535,7 @@ import U2FAuthenticate from './u2f/authenticate';
break;
case 'profiles:personal_access_tokens:index':
case 'admin:impersonation_tokens:index':
new gl.DueDateSelectors();
new DueDateSelectors();
break;
case 'projects:clusters:show':
import(/* webpackChunkName: "clusters" */ './clusters')
@ -553,6 +560,9 @@ import U2FAuthenticate from './u2f/authenticate';
case 'admin':
new Admin();
switch (path[1]) {
case 'broadcast_messages':
initBroadcastMessagesForm();
break;
case 'cohorts':
new UsagePing();
break;

View File

@ -1,308 +1,276 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, prefer-arrow-callback */
/* global Dropzone */
import Dropzone from 'dropzone';
import _ from 'underscore';
import './preview_markdown';
import csrf from './lib/utils/csrf';
window.DropzoneInput = (function() {
function DropzoneInput(form) {
const divHover = '<div class="div-dropzone-hover"></div>';
const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
const $attachButton = form.find('.button-attach-file');
const $attachingFileMessage = form.find('.attaching-file-message');
const $cancelButton = form.find('.button-cancel-uploading-files');
const $retryLink = form.find('.retry-uploading-link');
const $uploadProgress = form.find('.uploading-progress');
const $uploadingErrorContainer = form.find('.uploading-error-container');
const $uploadingErrorMessage = form.find('.uploading-error-message');
const $uploadingProgressContainer = form.find('.uploading-progress-container');
const uploadsPath = window.uploads_path || null;
const maxFileSize = gon.max_file_size || 10;
const formTextarea = form.find('.js-gfm-input');
let handlePaste;
let pasteText;
let addFileToForm;
let updateAttachingMessage;
let isImage;
let getFilename;
let uploadFile;
export default function dropzoneInput(form) {
const divHover = '<div class="div-dropzone-hover"></div>';
const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>';
const $attachButton = form.find('.button-attach-file');
const $attachingFileMessage = form.find('.attaching-file-message');
const $cancelButton = form.find('.button-cancel-uploading-files');
const $retryLink = form.find('.retry-uploading-link');
const $uploadProgress = form.find('.uploading-progress');
const $uploadingErrorContainer = form.find('.uploading-error-container');
const $uploadingErrorMessage = form.find('.uploading-error-message');
const $uploadingProgressContainer = form.find('.uploading-progress-container');
const uploadsPath = window.uploads_path || null;
const maxFileSize = gon.max_file_size || 10;
const formTextarea = form.find('.js-gfm-input');
let handlePaste;
let pasteText;
let addFileToForm;
let updateAttachingMessage;
let isImage;
let getFilename;
let uploadFile;
formTextarea.wrap('<div class="div-dropzone"></div>');
formTextarea.on('paste', (function(_this) {
return function(event) {
return handlePaste(event);
};
})(this));
formTextarea.wrap('<div class="div-dropzone"></div>');
formTextarea.on('paste', event => handlePaste(event));
// Add dropzone area to the form.
const $mdArea = formTextarea.closest('.md-area');
form.setupMarkdownPreview();
const $formDropzone = form.find('.div-dropzone');
$formDropzone.parent().addClass('div-dropzone-wrapper');
$formDropzone.append(divHover);
$formDropzone.find('.div-dropzone-hover').append(iconPaperclip);
// Add dropzone area to the form.
const $mdArea = formTextarea.closest('.md-area');
form.setupMarkdownPreview();
const $formDropzone = form.find('.div-dropzone');
$formDropzone.parent().addClass('div-dropzone-wrapper');
$formDropzone.append(divHover);
$formDropzone.find('.div-dropzone-hover').append(iconPaperclip);
if (!uploadsPath) return;
if (!uploadsPath) return;
const dropzone = $formDropzone.dropzone({
url: uploadsPath,
dictDefaultMessage: '',
clickable: true,
paramName: 'file',
maxFilesize: maxFileSize,
uploadMultiple: false,
headers: csrf.headers,
previewContainer: false,
processing: function() {
return $('.div-dropzone-alert').alert('close');
},
dragover: function() {
$mdArea.addClass('is-dropzone-hover');
form.find('.div-dropzone-hover').css('opacity', 0.7);
},
dragleave: function() {
$mdArea.removeClass('is-dropzone-hover');
form.find('.div-dropzone-hover').css('opacity', 0);
},
drop: function() {
$mdArea.removeClass('is-dropzone-hover');
form.find('.div-dropzone-hover').css('opacity', 0);
formTextarea.focus();
},
success: function(header, response) {
const processingFileCount = this.getQueuedFiles().length + this.getUploadingFiles().length;
const shouldPad = processingFileCount >= 1;
const dropzone = $formDropzone.dropzone({
url: uploadsPath,
dictDefaultMessage: '',
clickable: true,
paramName: 'file',
maxFilesize: maxFileSize,
uploadMultiple: false,
headers: csrf.headers,
previewContainer: false,
processing: () => $('.div-dropzone-alert').alert('close'),
dragover: () => {
$mdArea.addClass('is-dropzone-hover');
form.find('.div-dropzone-hover').css('opacity', 0.7);
},
dragleave: () => {
$mdArea.removeClass('is-dropzone-hover');
form.find('.div-dropzone-hover').css('opacity', 0);
},
drop: () => {
$mdArea.removeClass('is-dropzone-hover');
form.find('.div-dropzone-hover').css('opacity', 0);
formTextarea.focus();
},
success(header, response) {
const processingFileCount = this.getQueuedFiles().length + this.getUploadingFiles().length;
const shouldPad = processingFileCount >= 1;
pasteText(response.link.markdown, shouldPad);
// Show 'Attach a file' link only when all files have been uploaded.
if (!processingFileCount) $attachButton.removeClass('hide');
addFileToForm(response.link.url);
},
error: function(file, errorMessage = 'Attaching the file failed.', xhr) {
// If 'error' event is fired by dropzone, the second parameter is error message.
// If the 'errorMessage' parameter is empty, the default error message is set.
// If the 'error' event is fired by backend (xhr) error response, the third parameter is
// xhr object (xhr.responseText is error message).
// On error we hide the 'Attach' and 'Cancel' buttons
// and show an error.
pasteText(response.link.markdown, shouldPad);
// Show 'Attach a file' link only when all files have been uploaded.
if (!processingFileCount) $attachButton.removeClass('hide');
addFileToForm(response.link.url);
},
error: (file, errorMessage = 'Attaching the file failed.', xhr) => {
// If 'error' event is fired by dropzone, the second parameter is error message.
// If the 'errorMessage' parameter is empty, the default error message is set.
// If the 'error' event is fired by backend (xhr) error response, the third parameter is
// xhr object (xhr.responseText is error message).
// On error we hide the 'Attach' and 'Cancel' buttons
// and show an error.
// If there's xhr error message, let's show it instead of dropzone's one.
const message = xhr ? xhr.responseText : errorMessage;
// If there's xhr error message, let's show it instead of dropzone's one.
const message = xhr ? xhr.responseText : errorMessage;
$uploadingErrorContainer.removeClass('hide');
$uploadingErrorMessage.html(message);
$attachButton.addClass('hide');
$cancelButton.addClass('hide');
},
totaluploadprogress: function(totalUploadProgress) {
updateAttachingMessage(this.files, $attachingFileMessage);
$uploadProgress.text(Math.round(totalUploadProgress) + '%');
},
sending: function(file) {
// DOM elements already exist.
// Instead of dynamically generating them,
// we just either hide or show them.
$attachButton.addClass('hide');
$uploadingErrorContainer.addClass('hide');
$uploadingProgressContainer.removeClass('hide');
$cancelButton.removeClass('hide');
},
removedfile: function() {
$attachButton.removeClass('hide');
$cancelButton.addClass('hide');
$uploadingProgressContainer.addClass('hide');
$uploadingErrorContainer.addClass('hide');
},
queuecomplete: function() {
$('.dz-preview').remove();
$('.markdown-area').trigger('input');
$uploadingProgressContainer.addClass('hide');
$cancelButton.addClass('hide');
}
});
const child = $(dropzone[0]).children('textarea');
// removeAllFiles(true) stops uploading files (if any)
// and remove them from dropzone files queue.
$cancelButton.on('click', (e) => {
const target = e.target.closest('.js-main-target-form').querySelector('.div-dropzone');
e.preventDefault();
e.stopPropagation();
Dropzone.forElement(target).removeAllFiles(true);
});
// If 'error' event is fired, we store a failed files,
// clear dropzone files queue, change status of failed files to undefined,
// and add that files to the dropzone files queue again.
// addFile() adds file to dropzone files queue and upload it.
$retryLink.on('click', (e) => {
const dropzoneInstance = Dropzone.forElement(e.target.closest('.js-main-target-form').querySelector('.div-dropzone'));
const failedFiles = dropzoneInstance.files;
e.preventDefault();
// 'true' parameter of removeAllFiles() cancels uploading of files that are being uploaded at the moment.
dropzoneInstance.removeAllFiles(true);
failedFiles.map((failedFile, i) => {
const file = failedFile;
if (file.status === Dropzone.ERROR) {
file.status = undefined;
file.accepted = undefined;
}
return dropzoneInstance.addFile(file);
});
});
handlePaste = function(event) {
var filename, image, pasteEvent, text;
pasteEvent = event.originalEvent;
if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) {
image = isImage(pasteEvent);
if (image) {
event.preventDefault();
filename = getFilename(pasteEvent) || 'image.png';
text = `{{${filename}}}`;
pasteText(text);
return uploadFile(image.getAsFile(), filename);
}
}
};
isImage = function(data) {
var i, item;
i = 0;
while (i < data.clipboardData.items.length) {
item = data.clipboardData.items[i];
if (item.type.indexOf('image') !== -1) {
return item;
}
i += 1;
}
return false;
};
pasteText = function(text, shouldPad) {
var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
var formattedText = text;
if (shouldPad) formattedText += "\n\n";
const textarea = child.get(0);
caretStart = textarea.selectionStart;
caretEnd = textarea.selectionEnd;
textEnd = $(child).val().length;
beforeSelection = $(child).val().substring(0, caretStart);
afterSelection = $(child).val().substring(caretEnd, textEnd);
$(child).val(beforeSelection + formattedText + afterSelection);
textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
textarea.style.height = `${textarea.scrollHeight}px`;
formTextarea.get(0).dispatchEvent(new Event('input'));
return formTextarea.trigger('input');
};
addFileToForm = function(path) {
$(form).append('<input type="hidden" name="files[]" value="' + _.escape(path) + '">');
};
getFilename = function(e) {
var value;
if (window.clipboardData && window.clipboardData.getData) {
value = window.clipboardData.getData('Text');
} else if (e.clipboardData && e.clipboardData.getData) {
value = e.clipboardData.getData('text/plain');
}
value = value.split("\r");
return value[0];
};
const showSpinner = function(e) {
return $uploadingProgressContainer.removeClass('hide');
};
const closeSpinner = function() {
return $uploadingProgressContainer.addClass('hide');
};
const showError = function(message) {
$uploadingErrorContainer.removeClass('hide');
$uploadingErrorMessage.html(message);
};
$attachButton.addClass('hide');
$cancelButton.addClass('hide');
},
totaluploadprogress(totalUploadProgress) {
updateAttachingMessage(this.files, $attachingFileMessage);
$uploadProgress.text(`${Math.round(totalUploadProgress)}%`);
},
sending: () => {
// DOM elements already exist.
// Instead of dynamically generating them,
// we just either hide or show them.
$attachButton.addClass('hide');
$uploadingErrorContainer.addClass('hide');
$uploadingProgressContainer.removeClass('hide');
$cancelButton.removeClass('hide');
},
removedfile: () => {
$attachButton.removeClass('hide');
$cancelButton.addClass('hide');
$uploadingProgressContainer.addClass('hide');
$uploadingErrorContainer.addClass('hide');
},
queuecomplete: () => {
$('.dz-preview').remove();
$('.markdown-area').trigger('input');
const closeAlertMessage = function() {
return form.find('.div-dropzone-alert').alert('close');
};
$uploadingProgressContainer.addClass('hide');
$cancelButton.addClass('hide');
},
});
const insertToTextArea = function(filename, url) {
const $child = $(child);
$child.val(function(index, val) {
return val.replace(`{{${filename}}}`, url);
});
const child = $(dropzone[0]).children('textarea');
$child.trigger('change');
};
// removeAllFiles(true) stops uploading files (if any)
// and remove them from dropzone files queue.
$cancelButton.on('click', (e) => {
const target = e.target.closest('.js-main-target-form').querySelector('.div-dropzone');
const appendToTextArea = function(url) {
return $(child).val(function(index, val) {
return val + url + "\n";
});
};
e.preventDefault();
e.stopPropagation();
Dropzone.forElement(target).removeAllFiles(true);
});
uploadFile = function(item, filename) {
var formData;
formData = new FormData();
formData.append('file', item, filename);
return $.ajax({
url: uploadsPath,
type: 'POST',
data: formData,
dataType: 'json',
processData: false,
contentType: false,
headers: csrf.headers,
beforeSend: function() {
showSpinner();
return closeAlertMessage();
},
success: function(e, textStatus, response) {
return insertToTextArea(filename, response.responseJSON.link.markdown);
},
error: function(response) {
return showError(response.responseJSON.message);
},
complete: function() {
return closeSpinner();
}
});
};
// If 'error' event is fired, we store a failed files,
// clear dropzone files queue, change status of failed files to undefined,
// and add that files to the dropzone files queue again.
// addFile() adds file to dropzone files queue and upload it.
$retryLink.on('click', (e) => {
const dropzoneInstance = Dropzone.forElement(e.target.closest('.js-main-target-form').querySelector('.div-dropzone'));
const failedFiles = dropzoneInstance.files;
updateAttachingMessage = (files, messageContainer) => {
let attachingMessage;
const filesCount = files.filter(function(file) {
return file.status === 'uploading' ||
file.status === 'queued';
}).length;
e.preventDefault();
// Dinamycally change uploading files text depending on files number in
// dropzone files queue.
if (filesCount > 1) {
attachingMessage = 'Attaching ' + filesCount + ' files -';
} else {
attachingMessage = 'Attaching a file -';
// 'true' parameter of removeAllFiles() cancels
// uploading of files that are being uploaded at the moment.
dropzoneInstance.removeAllFiles(true);
failedFiles.map((failedFile) => {
const file = failedFile;
if (file.status === Dropzone.ERROR) {
file.status = undefined;
file.accepted = undefined;
}
messageContainer.text(attachingMessage);
};
form.find('.markdown-selector').click(function(e) {
e.preventDefault();
$(this).closest('.gfm-form').find('.div-dropzone').click();
formTextarea.focus();
return dropzoneInstance.addFile(file);
});
}
});
// eslint-disable-next-line consistent-return
handlePaste = (event) => {
const pasteEvent = event.originalEvent;
if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) {
const image = isImage(pasteEvent);
if (image) {
event.preventDefault();
const filename = getFilename(pasteEvent) || 'image.png';
const text = `{{${filename}}}`;
pasteText(text);
return uploadFile(image.getAsFile(), filename);
}
}
};
return DropzoneInput;
})();
isImage = (data) => {
let i = 0;
while (i < data.clipboardData.items.length) {
const item = data.clipboardData.items[i];
if (item.type.indexOf('image') !== -1) {
return item;
}
i += 1;
}
return false;
};
pasteText = (text, shouldPad) => {
let formattedText = text;
if (shouldPad) {
formattedText += '\n\n';
}
const textarea = child.get(0);
const caretStart = textarea.selectionStart;
const caretEnd = textarea.selectionEnd;
const textEnd = $(child).val().length;
const beforeSelection = $(child).val().substring(0, caretStart);
const afterSelection = $(child).val().substring(caretEnd, textEnd);
$(child).val(beforeSelection + formattedText + afterSelection);
textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
textarea.style.height = `${textarea.scrollHeight}px`;
formTextarea.get(0).dispatchEvent(new Event('input'));
return formTextarea.trigger('input');
};
addFileToForm = (path) => {
$(form).append(`<input type="hidden" name="files[]" value="${_.escape(path)}">`);
};
getFilename = (e) => {
let value;
if (window.clipboardData && window.clipboardData.getData) {
value = window.clipboardData.getData('Text');
} else if (e.clipboardData && e.clipboardData.getData) {
value = e.clipboardData.getData('text/plain');
}
value = value.split('\r');
return value[0];
};
const showSpinner = () => $uploadingProgressContainer.removeClass('hide');
const closeSpinner = () => $uploadingProgressContainer.addClass('hide');
const showError = (message) => {
$uploadingErrorContainer.removeClass('hide');
$uploadingErrorMessage.html(message);
};
const closeAlertMessage = () => form.find('.div-dropzone-alert').alert('close');
const insertToTextArea = (filename, url) => {
const $child = $(child);
$child.val((index, val) => val.replace(`{{${filename}}}`, url));
$child.trigger('change');
};
uploadFile = (item, filename) => {
const formData = new FormData();
formData.append('file', item, filename);
return $.ajax({
url: uploadsPath,
type: 'POST',
data: formData,
dataType: 'json',
processData: false,
contentType: false,
headers: csrf.headers,
beforeSend: () => {
showSpinner();
return closeAlertMessage();
},
success: (e, text, response) => {
const md = response.responseJSON.link.markdown;
insertToTextArea(filename, md);
},
error: response => showError(response.responseJSON.message),
complete: () => closeSpinner(),
});
};
updateAttachingMessage = (files, messageContainer) => {
let attachingMessage;
const filesCount = files.filter(file => file.status === 'uploading' || file.status === 'queued').length;
// Dinamycally change uploading files text depending on files number in
// dropzone files queue.
if (filesCount > 1) {
attachingMessage = `Attaching ${filesCount} files -`;
} else {
attachingMessage = 'Attaching a file -';
}
messageContainer.text(attachingMessage);
};
form.find('.markdown-selector').click(function onMarkdownClick(e) {
e.preventDefault();
$(this).closest('.gfm-form').find('.div-dropzone').click();
formTextarea.focus();
});
}

View File

@ -1,8 +1,7 @@
/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */
/* global dateFormat */
import Pikaday from 'pikaday';
import DateFix from './lib/utils/datefix';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
class DueDateSelect {
constructor({ $dropdown, $loading } = {}) {
@ -17,8 +16,8 @@ class DueDateSelect {
this.$value = $block.find('.value');
this.$valueContent = $block.find('.value-content');
this.$sidebarValue = $('.js-due-date-sidebar-value', $block);
this.fieldName = $dropdown.data('field-name'),
this.abilityName = $dropdown.data('ability-name'),
this.fieldName = $dropdown.data('field-name');
this.abilityName = $dropdown.data('ability-name');
this.issueUpdateURL = $dropdown.data('issue-update');
this.rawSelectedDate = null;
@ -39,20 +38,20 @@ class DueDateSelect {
hidden: () => {
this.$selectbox.hide();
this.$value.css('display', '');
}
},
});
}
initDatePicker() {
const $dueDateInput = $(`input[name='${this.fieldName}']`);
const dateFix = DateFix.dashedFix($dueDateInput.val());
const calendar = new Pikaday({
field: $dueDateInput.get(0),
theme: 'gitlab-theme',
format: 'yyyy-mm-dd',
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect: (dateText) => {
const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd');
$dueDateInput.val(formattedDate);
$dueDateInput.val(calendar.toString(dateText));
if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val();
@ -60,10 +59,10 @@ class DueDateSelect {
} else {
this.saveDueDate(true);
}
}
},
});
calendar.setDate(dateFix);
calendar.setDate(parsePikadayDate($dueDateInput.val()));
this.$datePicker.append(calendar.el);
this.$datePicker.data('pikaday', calendar);
}
@ -79,8 +78,8 @@ class DueDateSelect {
gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
this.updateIssueBoardIssue();
} else {
$("input[name='" + this.fieldName + "']").val('');
return this.saveDueDate(false);
$(`input[name='${this.fieldName}']`).val('');
this.saveDueDate(false);
}
});
}
@ -111,7 +110,7 @@ class DueDateSelect {
this.datePayload = datePayload;
}
updateIssueBoardIssue () {
updateIssueBoardIssue() {
this.$loading.fadeIn();
this.$dropdown.trigger('loading.gl.dropdown');
this.$selectbox.hide();
@ -149,8 +148,8 @@ class DueDateSelect {
return selectedDateValue.length ?
$('.js-remove-due-date-holder').removeClass('hidden') :
$('.js-remove-due-date-holder').addClass('hidden');
}
}).done((data) => {
},
}).done(() => {
if (isDropdown) {
this.$dropdown.trigger('loaded.gl.dropdown');
this.$dropdown.dropdown('toggle');
@ -160,27 +159,28 @@ class DueDateSelect {
}
}
class DueDateSelectors {
export default class DueDateSelectors {
constructor() {
this.initMilestoneDatePicker();
this.initIssuableSelect();
}
// eslint-disable-next-line class-methods-use-this
initMilestoneDatePicker() {
$('.datepicker').each(function() {
$('.datepicker').each(function initPikadayMilestone() {
const $datePicker = $(this);
const dateFix = DateFix.dashedFix($datePicker.val());
const calendar = new Pikaday({
field: $datePicker.get(0),
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
container: $datePicker.parent().get(0),
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect(dateText) {
$datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
$datePicker.val(calendar.toString(dateText));
},
});
calendar.setDate(dateFix);
calendar.setDate(parsePikadayDate($datePicker.val()));
$datePicker.data('pikaday', calendar);
});
@ -191,19 +191,17 @@ class DueDateSelectors {
calendar.setDate(null);
});
}
// eslint-disable-next-line class-methods-use-this
initIssuableSelect() {
const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
$('.js-due-date-select').each((i, dropdown) => {
const $dropdown = $(dropdown);
// eslint-disable-next-line no-new
new DueDateSelect({
$dropdown,
$loading
$loading,
});
});
}
}
window.gl = window.gl || {};
window.gl.DueDateSelectors = DueDateSelectors;

View File

@ -1,6 +1,3 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */
/* global notes */
/* Developer beware! Do not add logic to showButton or hideButton
* that will force a reflow. Doing so will create a signficant performance
* bottleneck for pages with large diffs. For a comprehensive list of what
@ -20,8 +17,10 @@ const DIFF_EXPANDED_CLASS = 'diff-expanded';
export default {
init($diffFile) {
/* Caching is used only when the following members are *true*. This is because there are likely to be
* differently configured versions of diffs in the same session. However if these values are true, they
/* Caching is used only when the following members are *true*.
* This is because there are likely to be
* differently configured versions of diffs in the same session.
* However if these values are true, they
* will be true in all cases */
if (!this.userCanCreateNote) {

View File

@ -6,10 +6,11 @@ import _ from 'underscore';
*/
export default class FilterableList {
constructor(form, filter, holder) {
constructor(form, filter, holder, filterInputField = 'filter_groups') {
this.filterForm = form;
this.listFilterElement = filter;
this.listHolderElement = holder;
this.filterInputField = filterInputField;
this.isBusy = false;
}
@ -32,10 +33,10 @@ export default class FilterableList {
onFilterInput() {
const $form = $(this.filterForm);
const queryData = {};
const filterGroupsParam = $form.find('[name="filter_groups"]').val();
const filterGroupsParam = $form.find(`[name="${this.filterInputField}"]`).val();
if (filterGroupsParam) {
queryData.filter_groups = filterGroupsParam;
queryData[this.filterInputField] = filterGroupsParam;
}
this.filterResults(queryData);

View File

@ -123,8 +123,8 @@ class FilteredSearchVisualTokens {
/* eslint-disable no-param-reassign */
tokenValueContainer.dataset.originalValue = tokenValue;
tokenValueElement.innerHTML = `
<img class="avatar s20" src="${user.avatar_url}" alt="${user.name}'s avatar">
${user.name}
<img class="avatar s20" src="${user.avatar_url}" alt="">
${_.escape(user.name)}
`;
/* eslint-enable no-param-reassign */
})

View File

@ -40,6 +40,10 @@ const createFlashEl = (message, type, isInContentWrapper = false) => `
</div>
`;
const removeFlashClickListener = (flashEl, fadeTransition) => {
flashEl.parentNode.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
};
/*
* Flash banner supports different types of Flash configurations
* along with ability to provide actionConfig which can be used to show
@ -70,7 +74,7 @@ const createFlash = function createFlash(
flashContainer.innerHTML = createFlashEl(message, type, isInContentWrapper);
const flashEl = flashContainer.querySelector(`.flash-${type}`);
flashEl.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
removeFlashClickListener(flashEl, fadeTransition);
if (actionConfig) {
flashEl.innerHTML += createAction(actionConfig);
@ -90,5 +94,6 @@ export {
createFlashEl,
createAction,
hideFlash,
removeFlashClickListener,
};
window.Flash = createFlash;

View File

@ -1,7 +1,7 @@
/* global DropzoneInput */
/* global autosize */
import GfmAutoComplete from './gfm_auto_complete';
import dropzoneInput from './dropzone_input';
export default class GLForm {
constructor(form, enableGFM = false) {
@ -41,7 +41,7 @@ export default class GLForm {
mergeRequests: this.enableGFM,
labels: this.enableGFM,
});
new DropzoneInput(this.form); // eslint-disable-line no-new
dropzoneInput(this.form);
autosize(this.textarea);
}
// form and textarea event listeners

View File

@ -1,19 +1,12 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */
window.GroupAvatar = (function() {
function GroupAvatar() {
$('.js-choose-group-avatar-button').on("click", function() {
var form;
form = $(this).closest("form");
return form.find(".js-group-avatar-input").click();
});
$('.js-group-avatar-input').on("change", function() {
var filename, form;
form = $(this).closest("form");
filename = $(this).val().replace(/^.*[\\\/]/, '');
return form.find(".js-avatar-filename").text(filename);
});
}
return GroupAvatar;
})();
export default function groupAvatar() {
$('.js-choose-group-avatar-button').on('click', function onClickGroupAvatar() {
const form = $(this).closest('form');
return form.find('.js-group-avatar-input').click();
});
$('.js-group-avatar-input').on('change', function onChangeAvatarInput() {
const form = $(this).closest('form');
// eslint-disable-next-line no-useless-escape
const filename = $(this).val().replace(/^.*[\\\/]/, '');
return form.find('.js-avatar-filename').text(filename);
});
}

View File

@ -1,6 +1,4 @@
/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */
class GroupLabelSubscription {
export default class GroupLabelSubscription {
constructor(container) {
const $container = $(container);
this.$dropdown = $container.find('.dropdown');
@ -18,7 +16,7 @@ class GroupLabelSubscription {
$.ajax({
type: 'POST',
url: url
url,
}).done(() => {
this.toggleSubscriptionButtons();
this.$unsubscribeButtons.removeAttr('data-url');
@ -35,7 +33,7 @@ class GroupLabelSubscription {
$.ajax({
type: 'POST',
url: url
url,
}).done(() => {
this.toggleSubscriptionButtons();
});
@ -47,6 +45,3 @@ class GroupLabelSubscription {
this.$unsubscribeButtons.toggleClass('hidden');
}
}
window.gl = window.gl || {};
window.gl.GroupLabelSubscription = GroupLabelSubscription;

View File

@ -0,0 +1,194 @@
<script>
/* global Flash */
import eventHub from '../event_hub';
import { getParameterByName } from '../../lib/utils/common_utils';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import { COMMON_STR } from '../constants';
import groupsComponent from './groups.vue';
export default {
components: {
loadingIcon,
groupsComponent,
},
props: {
store: {
type: Object,
required: true,
},
service: {
type: Object,
required: true,
},
hideProjects: {
type: Boolean,
required: true,
},
},
data() {
return {
isLoading: true,
isSearchEmpty: false,
searchEmptyMessage: '',
};
},
computed: {
groups() {
return this.store.getGroups();
},
pageInfo() {
return this.store.getPaginationInfo();
},
},
methods: {
fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) {
return this.service.getGroups(parentId, page, filterGroupsBy, sortBy, archived)
.then((res) => {
if (updatePagination) {
this.updatePagination(res.headers);
}
return res;
})
.then(res => res.json())
.catch(() => {
this.isLoading = false;
$.scrollTo(0);
Flash(COMMON_STR.FAILURE);
});
},
fetchAllGroups() {
const page = getParameterByName('page') || null;
const sortBy = getParameterByName('sort') || null;
const archived = getParameterByName('archived') || null;
const filterGroupsBy = getParameterByName('filter') || null;
this.isLoading = true;
// eslint-disable-next-line promise/catch-or-return
this.fetchGroups({
page,
filterGroupsBy,
sortBy,
archived,
updatePagination: true,
}).then((res) => {
this.isLoading = false;
this.updateGroups(res, Boolean(filterGroupsBy));
});
},
fetchPage(page, filterGroupsBy, sortBy, archived) {
this.isLoading = true;
// eslint-disable-next-line promise/catch-or-return
this.fetchGroups({
page,
filterGroupsBy,
sortBy,
archived,
updatePagination: true,
}).then((res) => {
this.isLoading = false;
$.scrollTo(0);
const currentPath = gl.utils.mergeUrlParams({ page }, window.location.href);
window.history.replaceState({
page: currentPath,
}, document.title, currentPath);
this.updateGroups(res);
});
},
toggleChildren(group) {
const parentGroup = group;
if (!parentGroup.isOpen) {
if (parentGroup.children.length === 0) {
parentGroup.isChildrenLoading = true;
// eslint-disable-next-line promise/catch-or-return
this.fetchGroups({
parentId: parentGroup.id,
}).then((res) => {
this.store.setGroupChildren(parentGroup, res);
}).catch(() => {
parentGroup.isChildrenLoading = false;
});
} else {
parentGroup.isOpen = true;
}
} else {
parentGroup.isOpen = false;
}
},
leaveGroup(group, parentGroup) {
const targetGroup = group;
targetGroup.isBeingRemoved = true;
this.service.leaveGroup(targetGroup.leavePath)
.then(res => res.json())
.then((res) => {
$.scrollTo(0);
this.store.removeGroup(targetGroup, parentGroup);
Flash(res.notice, 'notice');
})
.catch((err) => {
let message = COMMON_STR.FAILURE;
if (err.status === 403) {
message = COMMON_STR.LEAVE_FORBIDDEN;
}
Flash(message);
targetGroup.isBeingRemoved = false;
});
},
updatePagination(headers) {
this.store.setPaginationInfo(headers);
},
updateGroups(groups, fromSearch) {
this.isSearchEmpty = groups ? groups.length === 0 : false;
if (fromSearch) {
this.store.setSearchedGroups(groups);
} else {
this.store.setGroups(groups);
}
},
},
created() {
this.searchEmptyMessage = this.hideProjects ?
COMMON_STR.GROUP_SEARCH_EMPTY : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
eventHub.$on('fetchPage', this.fetchPage);
eventHub.$on('toggleChildren', this.toggleChildren);
eventHub.$on('leaveGroup', this.leaveGroup);
eventHub.$on('updatePagination', this.updatePagination);
eventHub.$on('updateGroups', this.updateGroups);
},
mounted() {
this.fetchAllGroups();
},
beforeDestroy() {
eventHub.$off('fetchPage', this.fetchPage);
eventHub.$off('toggleChildren', this.toggleChildren);
eventHub.$off('leaveGroup', this.leaveGroup);
eventHub.$off('updatePagination', this.updatePagination);
eventHub.$off('updateGroups', this.updateGroups);
},
};
</script>
<template>
<div>
<loading-icon
class="loading-animation prepend-top-20"
size="2"
v-if="isLoading"
:label="s__('GroupsTree|Loading groups')"
/>
<groups-component
v-if="!isLoading"
:groups="groups"
:search-empty="isSearchEmpty"
:search-empty-message="searchEmptyMessage"
:page-info="pageInfo"
/>
</div>
</template>

View File

@ -1,15 +1,27 @@
<script>
import { n__ } from '../../locale';
import { MAX_CHILDREN_COUNT } from '../constants';
export default {
props: {
groups: {
type: Object,
required: true,
},
baseGroup: {
parentGroup: {
type: Object,
required: false,
default: () => ({}),
},
groups: {
type: Array,
required: false,
default: () => ([]),
},
},
computed: {
hasMoreChildren() {
return this.parentGroup.childrenCount > MAX_CHILDREN_COUNT;
},
moreChildrenStats() {
return n__('One more item', '%d more items', this.parentGroup.childrenCount - this.parentGroup.children.length);
},
},
};
</script>
@ -20,8 +32,20 @@ export default {
v-for="(group, index) in groups"
:key="index"
:group="group"
:base-group="baseGroup"
:collection="groups"
:parent-group="parentGroup"
/>
<li
v-if="hasMoreChildren"
class="group-row">
<a
:href="parentGroup.relativePath"
class="group-row-contents has-more-items">
<i
class="fa fa-external-link"
aria-hidden="true"
/>
{{moreChildrenStats}}
</a>
</li>
</ul>
</template>

View File

@ -2,50 +2,29 @@
import identicon from '../../vue_shared/components/identicon.vue';
import eventHub from '../event_hub';
import itemCaret from './item_caret.vue';
import itemTypeIcon from './item_type_icon.vue';
import itemStats from './item_stats.vue';
import itemActions from './item_actions.vue';
export default {
components: {
identicon,
itemCaret,
itemTypeIcon,
itemStats,
itemActions,
},
props: {
parentGroup: {
type: Object,
required: false,
default: () => ({}),
},
group: {
type: Object,
required: true,
},
baseGroup: {
type: Object,
required: false,
default: () => ({}),
},
collection: {
type: Object,
required: false,
default: () => ({}),
},
},
methods: {
onClickRowGroup(e) {
e.stopPropagation();
// Skip for buttons
if (!(e.target.tagName === 'A') && !(e.target.tagName === 'I' && e.target.parentElement.tagName === 'A')) {
if (this.group.hasSubgroups) {
eventHub.$emit('toggleSubGroups', this.group);
} else {
window.location.href = this.group.groupPath;
}
}
},
onLeaveGroup(e) {
e.preventDefault();
// eslint-disable-next-line no-alert
if (confirm(`Are you sure you want to leave the "${this.group.fullName}" group?`)) {
this.leaveGroup();
}
},
leaveGroup() {
eventHub.$emit('leaveGroup', this.group, this.collection);
},
},
computed: {
groupDomId() {
@ -53,51 +32,33 @@ export default {
},
rowClass() {
return {
'group-row': true,
'is-open': this.group.isOpen,
'has-subgroups': this.group.hasSubgroups,
'no-description': !this.group.description,
'has-children': this.hasChildren,
'has-description': this.group.description,
'being-removed': this.group.isBeingRemoved,
};
},
visibilityIcon() {
return {
fa: true,
'fa-globe': this.group.visibility === 'public',
'fa-shield': this.group.visibility === 'internal',
'fa-lock': this.group.visibility === 'private',
};
},
fullPath() {
let fullPath = '';
if (this.group.isOrphan) {
// check if current group is baseGroup
if (Object.keys(this.baseGroup).length > 0 && this.baseGroup !== this.group) {
// Remove baseGroup prefix from our current group.fullName. e.g:
// baseGroup.fullName: `level1`
// group.fullName: `level1 / level2 / level3`
// Result: `level2 / level3`
const gfn = this.group.fullName;
const bfn = this.baseGroup.fullName;
const length = bfn.length;
const start = gfn.indexOf(bfn);
const extraPrefixChars = 3;
fullPath = gfn.substr(start + length + extraPrefixChars);
} else {
fullPath = this.group.fullName;
}
} else {
fullPath = this.group.name;
}
return fullPath;
},
hasGroups() {
return Object.keys(this.group.subGroups).length > 0;
hasChildren() {
return this.group.childrenCount > 0;
},
hasAvatar() {
return this.group.avatarUrl && this.group.avatarUrl.indexOf('/assets/no_group_avatar') === -1;
return this.group.avatarUrl !== null;
},
isGroup() {
return this.group.type === 'group';
},
},
methods: {
onClickRowGroup(e) {
const NO_EXPAND_CLS = 'no-expand';
if (!(e.target.classList.contains(NO_EXPAND_CLS) ||
e.target.parentElement.classList.contains(NO_EXPAND_CLS))) {
if (this.hasChildren) {
eventHub.$emit('toggleChildren', this.group);
} else {
gl.utils.visitUrl(this.group.relativePath);
}
}
},
},
};
@ -108,98 +69,36 @@ export default {
@click.stop="onClickRowGroup"
:id="groupDomId"
:class="rowClass"
class="group-row"
>
<div
class="group-row-contents">
<div
class="controls">
<a
v-if="group.canEdit"
class="edit-group btn"
:href="group.editPath">
<i
class="fa fa-cogs"
aria-hidden="true"
>
</i>
</a>
<a
@click="onLeaveGroup"
:href="group.leavePath"
class="leave-group btn"
title="Leave this group">
<i
class="fa fa-sign-out"
aria-hidden="true"
>
</i>
</a>
</div>
<div
class="stats">
<span
class="number-projects">
<i
class="fa fa-bookmark"
aria-hidden="true"
>
</i>
{{group.numberProjects}}
</span>
<span
class="number-users">
<i
class="fa fa-users"
aria-hidden="true"
>
</i>
{{group.numberUsers}}
</span>
<span
class="group-visibility">
<i
:class="visibilityIcon"
aria-hidden="true"
>
</i>
</span>
</div>
<item-actions
v-if="isGroup"
:group="group"
:parent-group="parentGroup"
/>
<item-stats
:item="group"
/>
<div
class="folder-toggle-wrap">
<span
class="folder-caret"
v-if="group.hasSubgroups">
<i
v-if="group.isOpen"
class="fa fa-caret-down"
aria-hidden="true"
>
</i>
<i
v-if="!group.isOpen"
class="fa fa-caret-right"
aria-hidden="true"
>
</i>
</span>
<span class="folder-icon">
<i
v-if="group.isOpen"
class="fa fa-folder-open"
aria-hidden="true"
>
</i>
<i
v-if="!group.isOpen"
class="fa fa-folder"
aria-hidden="true">
</i>
</span>
<item-caret
:is-group-open="group.isOpen"
/>
<item-type-icon
:item-type="group.type"
:is-group-open="group.isOpen"
/>
</div>
<div
class="avatar-container s40 hidden-xs">
class="avatar-container s40 hidden-xs"
:class="{ 'content-loading': group.isChildrenLoading }"
>
<a
:href="group.groupPath">
:href="group.relativePath"
class="no-expand"
>
<img
v-if="hasAvatar"
class="avatar s40"
@ -215,19 +114,22 @@ export default {
<div
class="title">
<a
:href="group.groupPath">{{fullPath}}</a>
<template v-if="group.permissions.humanGroupAccess">
as
<span class="access-type">{{group.permissions.humanGroupAccess}}</span>
</template>
:href="group.relativePath"
class="no-expand">{{group.fullName}}</a>
<span
v-if="group.permission"
class="access-type"
>
{{s__('GroupsTreeRole|as')}} {{group.permission}}
</span>
</div>
<div
class="description">{{group.description}}</div>
</div>
<group-folder
v-if="group.isOpen && hasGroups"
:groups="group.subGroups"
:baseGroup="group"
v-if="group.isOpen && hasChildren"
:parent-group="group"
:groups="group.children"
/>
</li>
</template>

View File

@ -4,24 +4,33 @@ import eventHub from '../event_hub';
import { getParameterByName } from '../../lib/utils/common_utils';
export default {
components: {
tablePagination,
},
props: {
groups: {
type: Object,
type: Array,
required: true,
},
pageInfo: {
type: Object,
required: true,
},
},
components: {
tablePagination,
searchEmpty: {
type: Boolean,
required: true,
},
searchEmptyMessage: {
type: String,
required: true,
},
},
methods: {
change(page) {
const filterGroupsParam = getParameterByName('filter_groups');
const sortParam = getParameterByName('sort');
eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam);
const archivedParam = getParameterByName('archived');
eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam);
},
},
};
@ -29,10 +38,17 @@ export default {
<template>
<div class="groups-list-tree-container">
<div
v-if="searchEmpty"
class="has-no-search-results">
{{searchEmptyMessage}}
</div>
<group-folder
v-if="!searchEmpty"
:groups="groups"
/>
<table-pagination
v-if="!searchEmpty"
:change="change"
:pageInfo="pageInfo"
/>

View File

@ -0,0 +1,93 @@
<script>
import { s__ } from '../../locale';
import tooltip from '../../vue_shared/directives/tooltip';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import eventHub from '../event_hub';
import { COMMON_STR } from '../constants';
export default {
components: {
PopupDialog,
},
directives: {
tooltip,
},
props: {
parentGroup: {
type: Object,
required: false,
default: () => ({}),
},
group: {
type: Object,
required: true,
},
},
data() {
return {
dialogStatus: false,
};
},
computed: {
leaveBtnTitle() {
return COMMON_STR.LEAVE_BTN_TITLE;
},
editBtnTitle() {
return COMMON_STR.EDIT_BTN_TITLE;
},
leaveConfirmationMessage() {
return s__(`GroupsTree|Are you sure you want to leave the "${this.group.fullName}" group?`);
},
},
methods: {
onLeaveGroup() {
this.dialogStatus = true;
},
leaveGroup(leaveConfirmed) {
this.dialogStatus = false;
if (leaveConfirmed) {
eventHub.$emit('leaveGroup', this.group, this.parentGroup);
}
},
},
};
</script>
<template>
<div class="controls">
<a
v-tooltip
v-if="group.canEdit"
:href="group.editPath"
:title="editBtnTitle"
:aria-label="editBtnTitle"
data-container="body"
class="edit-group btn no-expand">
<i
class="fa fa-cogs"
aria-hidden="true"/>
</a>
<a
v-tooltip
v-if="group.canLeave"
@click.prevent="onLeaveGroup"
:href="group.leavePath"
:title="leaveBtnTitle"
:aria-label="leaveBtnTitle"
data-container="body"
class="leave-group btn no-expand">
<i
class="fa fa-sign-out"
aria-hidden="true"/>
</a>
<popup-dialog
v-show="dialogStatus"
:primary-button-label="__('Leave')"
kind="warning"
:title="__('Are you sure?')"
:text="__('Are you sure you want to leave this group?')"
:body="leaveConfirmationMessage"
@submit="leaveGroup"
/>
</div>
</template>

View File

@ -0,0 +1,25 @@
<script>
export default {
props: {
isGroupOpen: {
type: Boolean,
required: true,
default: false,
},
},
computed: {
iconClass() {
return this.isGroupOpen ? 'fa-caret-down' : 'fa-caret-right';
},
},
};
</script>
<template>
<span class="folder-caret">
<i
:class="iconClass"
class="fa"
aria-hidden="true"/>
</span>
</template>

View File

@ -0,0 +1,98 @@
<script>
import tooltip from '../../vue_shared/directives/tooltip';
import { ITEM_TYPE, VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE, PROJECT_VISIBILITY_TYPE } from '../constants';
export default {
directives: {
tooltip,
},
props: {
item: {
type: Object,
required: true,
},
},
computed: {
visibilityIcon() {
return VISIBILITY_TYPE_ICON[this.item.visibility];
},
visibilityTooltip() {
if (this.item.type === ITEM_TYPE.GROUP) {
return GROUP_VISIBILITY_TYPE[this.item.visibility];
}
return PROJECT_VISIBILITY_TYPE[this.item.visibility];
},
isProject() {
return this.item.type === ITEM_TYPE.PROJECT;
},
isGroup() {
return this.item.type === ITEM_TYPE.GROUP;
},
},
};
</script>
<template>
<div class="stats">
<span
v-tooltip
v-if="isGroup"
:title="s__('Subgroups')"
class="number-subgroups"
data-placement="top"
data-container="body">
<i
class="fa fa-folder"
aria-hidden="true"
/>
{{item.subgroupCount}}
</span>
<span
v-tooltip
v-if="isGroup"
:title="s__('Projects')"
class="number-projects"
data-placement="top"
data-container="body">
<i
class="fa fa-bookmark"
aria-hidden="true"
/>
{{item.projectCount}}
</span>
<span
v-tooltip
v-if="isGroup"
:title="s__('Members')"
class="number-users"
data-placement="top"
data-container="body">
<i
class="fa fa-users"
aria-hidden="true"
/>
{{item.memberCount}}
</span>
<span
v-if="isProject"
class="project-stars">
<i
class="fa fa-star"
aria-hidden="true"
/>
{{item.starCount}}
</span>
<span
v-tooltip
:title="visibilityTooltip"
data-placement="left"
data-container="body"
class="item-visibility">
<i
:class="visibilityIcon"
class="fa"
aria-hidden="true"
/>
</span>
</div>
</template>

View File

@ -0,0 +1,34 @@
<script>
import { ITEM_TYPE } from '../constants';
export default {
props: {
itemType: {
type: String,
required: true,
},
isGroupOpen: {
type: Boolean,
required: true,
default: false,
},
},
computed: {
iconClass() {
if (this.itemType === ITEM_TYPE.GROUP) {
return this.isGroupOpen ? 'fa-folder-open' : 'fa-folder';
}
return 'fa-bookmark';
},
},
};
</script>
<template>
<span class="item-type-icon">
<i
:class="iconClass"
class="fa"
aria-hidden="true"/>
</span>
</template>

View File

@ -0,0 +1,35 @@
import { __, s__ } from '../locale';
export const MAX_CHILDREN_COUNT = 20;
export const COMMON_STR = {
FAILURE: __('An error occurred. Please try again.'),
LEAVE_FORBIDDEN: s__('GroupsTree|Failed to leave the group. Please make sure you are not the only owner.'),
LEAVE_BTN_TITLE: s__('GroupsTree|Leave this group'),
EDIT_BTN_TITLE: s__('GroupsTree|Edit group'),
GROUP_SEARCH_EMPTY: s__('GroupsTree|Sorry, no groups matched your search'),
GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|Sorry, no groups or projects matched your search'),
};
export const ITEM_TYPE = {
PROJECT: 'project',
GROUP: 'group',
};
export const GROUP_VISIBILITY_TYPE = {
public: __('Public - The group and any public projects can be viewed without any authentication.'),
internal: __('Internal - The group and any internal projects can be viewed by any logged in user.'),
private: __('Private - The group and its projects can only be viewed by members.'),
};
export const PROJECT_VISIBILITY_TYPE = {
public: __('Public - The project can be accessed without any authentication.'),
internal: __('Internal - The project can be accessed by any logged in user.'),
private: __('Private - Project access must be granted explicitly to each user.'),
};
export const VISIBILITY_TYPE_ICON = {
public: 'fa-globe',
internal: 'fa-shield',
private: 'fa-lock',
};

View File

@ -3,12 +3,13 @@ import eventHub from './event_hub';
import { getParameterByName } from '../lib/utils/common_utils';
export default class GroupFilterableList extends FilterableList {
constructor({ form, filter, holder, filterEndpoint, pagePath }) {
super(form, filter, holder);
constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) {
super(form, filter, holder, filterInputField);
this.form = form;
this.filterEndpoint = filterEndpoint;
this.pagePath = pagePath;
this.$dropdown = $('.js-group-filter-dropdown-wrap');
this.filterInputField = filterInputField;
this.$dropdown = $(dropdownSel);
}
getFilterEndpoint() {
@ -24,30 +25,34 @@ export default class GroupFilterableList extends FilterableList {
bindEvents() {
super.bindEvents();
this.onFormSubmitWrapper = this.onFormSubmit.bind(this);
this.onFilterOptionClikWrapper = this.onOptionClick.bind(this);
this.filterForm.addEventListener('submit', this.onFormSubmitWrapper);
this.$dropdown.on('click', 'a', this.onFilterOptionClikWrapper);
}
onFormSubmit(e) {
e.preventDefault();
const $form = $(this.form);
const filterGroupsParam = $form.find('[name="filter_groups"]').val();
onFilterInput() {
const queryData = {};
const $form = $(this.form);
const archivedParam = getParameterByName('archived', window.location.href);
const filterGroupsParam = $form.find(`[name="${this.filterInputField}"]`).val();
if (filterGroupsParam) {
queryData.filter_groups = filterGroupsParam;
queryData[this.filterInputField] = filterGroupsParam;
}
if (archivedParam) {
queryData.archived = archivedParam;
}
this.filterResults(queryData);
this.setDefaultFilterOption();
if (this.setDefaultFilterOption) {
this.setDefaultFilterOption();
}
}
setDefaultFilterOption() {
const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu a:first-child').text());
const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').first().text());
this.$dropdown.find('.dropdown-label').text(defaultOption);
}
@ -55,23 +60,42 @@ export default class GroupFilterableList extends FilterableList {
e.preventDefault();
const queryData = {};
const sortParam = getParameterByName('sort', e.currentTarget.href);
// Get type of option selected from dropdown
const currentTargetClassList = e.currentTarget.parentElement.classList;
const isOptionFilterBySort = currentTargetClassList.contains('js-filter-sort-order');
const isOptionFilterByArchivedProjects = currentTargetClassList.contains('js-filter-archived-projects');
// Get option query param, also preserve currently applied query param
const sortParam = getParameterByName('sort', isOptionFilterBySort ? e.currentTarget.href : window.location.href);
const archivedParam = getParameterByName('archived', isOptionFilterByArchivedProjects ? e.currentTarget.href : window.location.href);
if (sortParam) {
queryData.sort = sortParam;
}
if (archivedParam) {
queryData.archived = archivedParam;
}
this.filterResults(queryData);
// Active selected option
this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text));
if (isOptionFilterBySort) {
this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text));
this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').removeClass('is-active');
} else if (isOptionFilterByArchivedProjects) {
this.$dropdown.find('.dropdown-menu li.js-filter-archived-projects a').removeClass('is-active');
}
$(e.target).addClass('is-active');
// Clear current value on search form
this.form.querySelector('[name="filter_groups"]').value = '';
this.form.querySelector(`[name="${this.filterInputField}"]`).value = '';
}
onFilterSuccess(data, xhr, queryData) {
super.onFilterSuccess(data, xhr, queryData);
const currentPath = this.getPagePath(queryData);
const paginationData = {
'X-Per-Page': xhr.getResponseHeader('X-Per-Page'),
@ -82,7 +106,11 @@ export default class GroupFilterableList extends FilterableList {
'X-Prev-Page': xhr.getResponseHeader('X-Prev-Page'),
};
eventHub.$emit('updateGroups', data);
window.history.replaceState({
page: currentPath,
}, document.title, currentPath);
eventHub.$emit('updateGroups', data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField));
eventHub.$emit('updatePagination', paginationData);
}
}

View File

@ -1,16 +1,17 @@
import Vue from 'vue';
import Flash from '../flash';
import Translate from '../vue_shared/translate';
import GroupFilterableList from './groups_filterable_list';
import GroupsComponent from './components/groups.vue';
import GroupFolder from './components/group_folder.vue';
import GroupItem from './components/group_item.vue';
import GroupsStore from './stores/groups_store';
import GroupsService from './services/groups_service';
import eventHub from './event_hub';
import { getParameterByName } from '../lib/utils/common_utils';
import GroupsStore from './store/groups_store';
import GroupsService from './service/groups_service';
import groupsApp from './components/app.vue';
import groupFolderComponent from './components/group_folder.vue';
import groupItemComponent from './components/group_item.vue';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => {
const el = document.getElementById('dashboard-group-app');
const el = document.getElementById('js-groups-tree');
// Don't do anything if element doesn't exist (No groups)
// This is for when the user enters directly to the page via URL
@ -18,176 +19,56 @@ document.addEventListener('DOMContentLoaded', () => {
return;
}
Vue.component('groups-component', GroupsComponent);
Vue.component('group-folder', GroupFolder);
Vue.component('group-item', GroupItem);
Vue.component('group-folder', groupFolderComponent);
Vue.component('group-item', groupItemComponent);
// eslint-disable-next-line no-new
new Vue({
el,
components: {
groupsApp,
},
data() {
this.store = new GroupsStore();
this.service = new GroupsService(el.dataset.endpoint);
const dataset = this.$options.el.dataset;
const hideProjects = dataset.hideProjects === 'true';
const store = new GroupsStore(hideProjects);
const service = new GroupsService(dataset.endpoint);
return {
store: this.store,
isLoading: true,
state: this.store.state,
store,
service,
hideProjects,
loading: true,
};
},
computed: {
isEmpty() {
return Object.keys(this.state.groups).length === 0;
},
},
methods: {
fetchGroups(parentGroup) {
let parentId = null;
let getGroups = null;
let page = null;
let sort = null;
let pageParam = null;
let sortParam = null;
let filterGroups = null;
let filterGroupsParam = null;
if (parentGroup) {
parentId = parentGroup.id;
} else {
this.isLoading = true;
}
pageParam = getParameterByName('page');
if (pageParam) {
page = pageParam;
}
filterGroupsParam = getParameterByName('filter_groups');
if (filterGroupsParam) {
filterGroups = filterGroupsParam;
}
sortParam = getParameterByName('sort');
if (sortParam) {
sort = sortParam;
}
getGroups = this.service.getGroups(parentId, page, filterGroups, sort);
getGroups
.then(response => response.json())
.then((response) => {
this.isLoading = false;
this.updateGroups(response, parentGroup);
})
.catch(this.handleErrorResponse);
return getGroups;
},
fetchPage(page, filterGroups, sort) {
this.isLoading = true;
return this.service
.getGroups(null, page, filterGroups, sort)
.then((response) => {
this.isLoading = false;
$.scrollTo(0);
const currentPath = gl.utils.mergeUrlParams({ page }, window.location.href);
window.history.replaceState({
page: currentPath,
}, document.title, currentPath);
return response.json().then((data) => {
this.updateGroups(data);
this.updatePagination(response.headers);
});
})
.catch(this.handleErrorResponse);
},
toggleSubGroups(parentGroup = null) {
if (!parentGroup.isOpen) {
this.store.resetGroups(parentGroup);
this.fetchGroups(parentGroup);
}
this.store.toggleSubGroups(parentGroup);
},
leaveGroup(group, collection) {
this.service.leaveGroup(group.leavePath)
.then(resp => resp.json())
.then((response) => {
$.scrollTo(0);
this.store.removeGroup(group, collection);
// eslint-disable-next-line no-new
new Flash(response.notice, 'notice');
})
.catch((error) => {
let message = 'An error occurred. Please try again.';
if (error.status === 403) {
message = 'Failed to leave the group. Please make sure you are not the only owner';
}
// eslint-disable-next-line no-new
new Flash(message);
});
},
updateGroups(groups, parentGroup) {
this.store.setGroups(groups, parentGroup);
},
updatePagination(headers) {
this.store.storePagination(headers);
},
handleErrorResponse() {
this.isLoading = false;
$.scrollTo(0);
// eslint-disable-next-line no-new
new Flash('An error occurred. Please try again.');
},
},
created() {
eventHub.$on('fetchPage', this.fetchPage);
eventHub.$on('toggleSubGroups', this.toggleSubGroups);
eventHub.$on('leaveGroup', this.leaveGroup);
eventHub.$on('updateGroups', this.updateGroups);
eventHub.$on('updatePagination', this.updatePagination);
},
beforeMount() {
const dataset = this.$options.el.dataset;
let groupFilterList = null;
const form = document.querySelector('form#group-filter-form');
const filter = document.querySelector('.js-groups-list-filter');
const holder = document.querySelector('.js-groups-list-holder');
const form = document.querySelector(dataset.formSel);
const filter = document.querySelector(dataset.filterSel);
const holder = document.querySelector(dataset.holderSel);
const opts = {
form,
filter,
holder,
filterEndpoint: el.dataset.endpoint,
pagePath: el.dataset.path,
filterEndpoint: dataset.endpoint,
pagePath: dataset.path,
dropdownSel: dataset.dropdownSel,
filterInputField: 'filter',
};
groupFilterList = new GroupFilterableList(opts);
groupFilterList.initSearch();
},
mounted() {
this.fetchGroups()
.then((response) => {
this.updatePagination(response.headers);
this.isLoading = false;
})
.catch(this.handleErrorResponse);
},
beforeDestroy() {
eventHub.$off('fetchPage', this.fetchPage);
eventHub.$off('toggleSubGroups', this.toggleSubGroups);
eventHub.$off('leaveGroup', this.leaveGroup);
eventHub.$off('updateGroups', this.updateGroups);
eventHub.$off('updatePagination', this.updatePagination);
render(createElement) {
return createElement('groups-app', {
props: {
store: this.store,
service: this.service,
hideProjects: this.hideProjects,
},
});
},
});
});

View File

@ -0,0 +1,62 @@
import DropLab from '../droplab/drop_lab';
import ISetter from '../droplab/plugins/input_setter';
const InputSetter = Object.assign({}, ISetter);
const NEW_PROJECT = 'new-project';
const NEW_SUBGROUP = 'new-subgroup';
export default class NewGroupChild {
constructor(buttonWrapper) {
this.buttonWrapper = buttonWrapper;
this.newGroupChildButton = this.buttonWrapper.querySelector('.js-new-group-child');
this.dropdownToggle = this.buttonWrapper.querySelector('.js-dropdown-toggle');
this.dropdownList = this.buttonWrapper.querySelector('.dropdown-menu');
this.newGroupPath = this.buttonWrapper.dataset.projectPath;
this.subgroupPath = this.buttonWrapper.dataset.subgroupPath;
this.init();
}
init() {
this.initDroplab();
this.bindEvents();
}
initDroplab() {
this.droplab = new DropLab();
this.droplab.init(
this.dropdownToggle,
this.dropdownList,
[InputSetter],
this.getDroplabConfig(),
);
}
getDroplabConfig() {
return {
InputSetter: [{
input: this.newGroupChildButton,
valueAttribute: 'data-value',
inputAttribute: 'data-action',
}, {
input: this.newGroupChildButton,
valueAttribute: 'data-text',
}],
};
}
bindEvents() {
this.newGroupChildButton
.addEventListener('click', this.onClickNewGroupChildButton.bind(this));
}
onClickNewGroupChildButton(e) {
if (e.target.dataset.action === NEW_PROJECT) {
gl.utils.visitUrl(this.newGroupPath);
} else if (e.target.dataset.action === NEW_SUBGROUP) {
gl.utils.visitUrl(this.subgroupPath);
}
}
}

View File

@ -8,7 +8,7 @@ export default class GroupsService {
this.groups = Vue.resource(endpoint);
}
getGroups(parentId, page, filterGroups, sort) {
getGroups(parentId, page, filterGroups, sort, archived) {
const data = {};
if (parentId) {
@ -20,12 +20,16 @@ export default class GroupsService {
}
if (filterGroups) {
data.filter_groups = filterGroups;
data.filter = filterGroups;
}
if (sort) {
data.sort = sort;
}
if (archived) {
data.archived = archived;
}
}
return this.groups.get(data);

View File

@ -0,0 +1,105 @@
import { normalizeHeaders, parseIntPagination } from '../../lib/utils/common_utils';
export default class GroupsStore {
constructor(hideProjects) {
this.state = {};
this.state.groups = [];
this.state.pageInfo = {};
this.hideProjects = hideProjects;
}
setGroups(rawGroups) {
if (rawGroups && rawGroups.length) {
this.state.groups = rawGroups.map(rawGroup => this.formatGroupItem(rawGroup));
} else {
this.state.groups = [];
}
}
setSearchedGroups(rawGroups) {
const formatGroups = groups => groups.map((group) => {
const formattedGroup = this.formatGroupItem(group);
if (formattedGroup.children && formattedGroup.children.length) {
formattedGroup.children = formatGroups(formattedGroup.children);
}
return formattedGroup;
});
if (rawGroups && rawGroups.length) {
this.state.groups = formatGroups(rawGroups);
} else {
this.state.groups = [];
}
}
setGroupChildren(parentGroup, children) {
const updatedParentGroup = parentGroup;
updatedParentGroup.children = children.map(rawChild => this.formatGroupItem(rawChild));
updatedParentGroup.isOpen = true;
updatedParentGroup.isChildrenLoading = false;
}
getGroups() {
return this.state.groups;
}
setPaginationInfo(pagination = {}) {
let paginationInfo;
if (Object.keys(pagination).length) {
const normalizedHeaders = normalizeHeaders(pagination);
paginationInfo = parseIntPagination(normalizedHeaders);
} else {
paginationInfo = pagination;
}
this.state.pageInfo = paginationInfo;
}
getPaginationInfo() {
return this.state.pageInfo;
}
formatGroupItem(rawGroupItem) {
const groupChildren = rawGroupItem.children || [];
const groupIsOpen = (groupChildren.length > 0) || false;
const childrenCount = this.hideProjects ?
rawGroupItem.subgroup_count :
rawGroupItem.children_count;
return {
id: rawGroupItem.id,
name: rawGroupItem.name,
fullName: rawGroupItem.full_name,
description: rawGroupItem.description,
visibility: rawGroupItem.visibility,
avatarUrl: rawGroupItem.avatar_url,
relativePath: rawGroupItem.relative_path,
editPath: rawGroupItem.edit_path,
leavePath: rawGroupItem.leave_path,
canEdit: rawGroupItem.can_edit,
canLeave: rawGroupItem.can_leave,
type: rawGroupItem.type,
permission: rawGroupItem.permission,
children: groupChildren,
isOpen: groupIsOpen,
isChildrenLoading: false,
isBeingRemoved: false,
parentId: rawGroupItem.parent_id,
childrenCount,
projectCount: rawGroupItem.project_count,
subgroupCount: rawGroupItem.subgroup_count,
memberCount: rawGroupItem.number_users_with_delimiter,
starCount: rawGroupItem.star_count,
};
}
removeGroup(group, parentGroup) {
const updatedParentGroup = parentGroup;
if (updatedParentGroup.children && updatedParentGroup.children.length) {
updatedParentGroup.children = parentGroup.children.filter(child => group.id !== child.id);
} else {
this.state.groups = this.state.groups.filter(child => group.id !== child.id);
}
}
}

View File

@ -1,167 +0,0 @@
import Vue from 'vue';
import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils';
export default class GroupsStore {
constructor() {
this.state = {};
this.state.groups = {};
this.state.pageInfo = {};
}
setGroups(rawGroups, parent) {
const parentGroup = parent;
const tree = this.buildTree(rawGroups, parentGroup);
if (parentGroup) {
parentGroup.subGroups = tree;
} else {
this.state.groups = tree;
}
return tree;
}
// eslint-disable-next-line class-methods-use-this
resetGroups(parent) {
const parentGroup = parent;
parentGroup.subGroups = {};
}
storePagination(pagination = {}) {
let paginationInfo;
if (Object.keys(pagination).length) {
const normalizedHeaders = normalizeHeaders(pagination);
paginationInfo = parseIntPagination(normalizedHeaders);
} else {
paginationInfo = pagination;
}
this.state.pageInfo = paginationInfo;
}
buildTree(rawGroups, parentGroup) {
const groups = this.decorateGroups(rawGroups);
const tree = {};
const mappedGroups = {};
const orphans = [];
// Map groups to an object
groups.map((group) => {
mappedGroups[`id${group.id}`] = group;
mappedGroups[`id${group.id}`].subGroups = {};
return group;
});
Object.keys(mappedGroups).map((key) => {
const currentGroup = mappedGroups[key];
if (currentGroup.parentId) {
// If the group is not at the root level, add it to its parent array of subGroups.
const findParentGroup = mappedGroups[`id${currentGroup.parentId}`];
if (findParentGroup) {
mappedGroups[`id${currentGroup.parentId}`].subGroups[`id${currentGroup.id}`] = currentGroup;
mappedGroups[`id${currentGroup.parentId}`].isOpen = true; // Expand group if it has subgroups
} else if (parentGroup && parentGroup.id === currentGroup.parentId) {
tree[`id${currentGroup.id}`] = currentGroup;
} else {
// No parent found. We save it for later processing
orphans.push(currentGroup);
// Add to tree to preserve original order
tree[`id${currentGroup.id}`] = currentGroup;
}
} else {
// If the group is at the top level, add it to first level elements array.
tree[`id${currentGroup.id}`] = currentGroup;
}
return key;
});
if (orphans.length) {
orphans.map((orphan) => {
let found = false;
const currentOrphan = orphan;
Object.keys(tree).map((key) => {
const group = tree[key];
if (
group &&
currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0 &&
// Make sure the currently selected orphan is not the same as the group
// we are checking here otherwise it will end up in an infinite loop
currentOrphan.id !== group.id
) {
group.subGroups[currentOrphan.id] = currentOrphan;
group.isOpen = true;
currentOrphan.isOrphan = true;
found = true;
// Delete if group was put at the top level. If not the group will be displayed twice.
if (tree[`id${currentOrphan.id}`]) {
delete tree[`id${currentOrphan.id}`];
}
}
return key;
});
if (!found) {
currentOrphan.isOrphan = true;
tree[`id${currentOrphan.id}`] = currentOrphan;
}
return orphan;
});
}
return tree;
}
decorateGroups(rawGroups) {
this.groups = rawGroups.map(this.decorateGroup);
return this.groups;
}
// eslint-disable-next-line class-methods-use-this
decorateGroup(rawGroup) {
return {
id: rawGroup.id,
fullName: rawGroup.full_name,
fullPath: rawGroup.full_path,
avatarUrl: rawGroup.avatar_url,
name: rawGroup.name,
hasSubgroups: rawGroup.has_subgroups,
canEdit: rawGroup.can_edit,
description: rawGroup.description,
webUrl: rawGroup.web_url,
groupPath: rawGroup.group_path,
parentId: rawGroup.parent_id,
visibility: rawGroup.visibility,
leavePath: rawGroup.leave_path,
editPath: rawGroup.edit_path,
isOpen: false,
isOrphan: false,
numberProjects: rawGroup.number_projects_with_delimiter,
numberUsers: rawGroup.number_users_with_delimiter,
permissions: {
humanGroupAccess: rawGroup.permissions.human_group_access,
},
subGroups: {},
};
}
// eslint-disable-next-line class-methods-use-this
removeGroup(group, collection) {
Vue.delete(collection, `id${group.id}`);
}
// eslint-disable-next-line class-methods-use-this
toggleSubGroups(toggleGroup) {
const group = toggleGroup;
group.isOpen = !group.isOpen;
return group;
}
}

View File

@ -4,6 +4,8 @@
/* global IssuableContext */
/* global Sidebar */
import DueDateSelectors from './due_date_select';
export default () => {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
@ -13,6 +15,6 @@ export default () => {
new LabelsSelect();
new IssuableContext(sidebarOptions.currentUser);
gl.Subscription.bindAll('.subscription');
new gl.DueDateSelectors();
new DueDateSelectors();
window.sidebar = new Sidebar();
};

View File

@ -51,20 +51,19 @@ const PARTICIPANTS_ROW_COUNT = 7;
}
IssuableContext.prototype.initParticipants = function() {
$(document).on("click", ".js-participants-more", this.toggleHiddenParticipants);
return $(".js-participants-author").each(function(i) {
$(document).on('click', '.js-participants-more', this.toggleHiddenParticipants);
return $('.js-participants-author').each(function(i) {
if (i >= PARTICIPANTS_ROW_COUNT) {
return $(this).addClass("js-participants-hidden").hide();
return $(this).addClass('js-participants-hidden').hide();
}
});
};
IssuableContext.prototype.toggleHiddenParticipants = function(e) {
var currentText, lessText, originalText;
e.preventDefault();
currentText = $(this).text().trim();
lessText = $(this).data("less-text");
originalText = $(this).data("original-text");
IssuableContext.prototype.toggleHiddenParticipants = function() {
const currentText = $(this).text().trim();
const lessText = $(this).data('less-text');
const originalText = $(this).data('original-text');
if (currentText === originalText) {
$(this).text(lessText);
@ -73,7 +72,7 @@ const PARTICIPANTS_ROW_COUNT = 7;
$(this).text(originalText);
}
$(".js-participants-hidden").toggle();
$('.js-participants-hidden').toggle();
};
return IssuableContext;

View File

@ -1,12 +1,12 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
/* global GitLab */
/* global dateFormat */
import Pikaday from 'pikaday';
import Autosave from './autosave';
import UsersSelect from './users_select';
import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
(function() {
this.IssuableForm = (function() {
@ -38,11 +38,13 @@ import ZenMode from './zen_mode';
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
container: $issuableDueDate.parent().get(0),
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect: function(dateText) {
$issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
$issuableDueDate.val(calendar.toString(dateText));
}
});
calendar.setDate(new Date($issuableDueDate.val()));
calendar.setDate(parsePikadayDate($issuableDueDate.val()));
}
}

View File

@ -24,6 +24,11 @@ export default {
required: true,
type: Boolean,
},
showInlineEditButton: {
type: Boolean,
required: false,
default: false,
},
issuableRef: {
type: String,
required: true,
@ -222,20 +227,25 @@ export default {
<div v-else>
<title-component
:issuable-ref="issuableRef"
:can-update="canUpdate"
:title-html="state.titleHtml"
:title-text="state.titleText" />
:title-text="state.titleText"
:show-inline-edit-button="showInlineEditButton"
/>
<description-component
v-if="state.descriptionHtml"
:can-update="canUpdate"
:description-html="state.descriptionHtml"
:description-text="state.descriptionText"
:updated-at="state.updatedAt"
:task-status="state.taskStatus" />
:task-status="state.taskStatus"
/>
<edited-component
v-if="hasUpdated"
:updated-at="state.updatedAt"
:updated-by-name="state.updatedByName"
:updated-by-path="state.updatedByPath" />
:updated-by-path="state.updatedByPath"
/>
</div>
</div>
</template>

View File

@ -16,15 +16,15 @@
<fieldset>
<label
class="sr-only"
for="issue-title">
for="issuable-title">
Title
</label>
<input
id="issue-title"
id="issuable-title"
class="form-control"
type="text"
placeholder="Issue title"
aria-label="Issue title"
placeholder="Title"
aria-label="Title"
v-model="formState.title"
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable" />

View File

@ -1,5 +1,8 @@
<script>
import animateMixin from '../mixins/animate';
import eventHub from '../event_hub';
import tooltip from '../../vue_shared/directives/tooltip';
import { spriteIcon } from '../../lib/utils/common_utils';
export default {
mixins: [animateMixin],
@ -15,6 +18,11 @@
type: String,
required: true,
},
canUpdate: {
required: false,
type: Boolean,
default: false,
},
titleHtml: {
type: String,
required: true,
@ -23,6 +31,14 @@
type: String,
required: true,
},
showInlineEditButton: {
type: Boolean,
required: false,
default: false,
},
},
directives: {
tooltip,
},
watch: {
titleHtml() {
@ -30,24 +46,46 @@
this.animateChange();
},
},
computed: {
pencilIcon() {
return spriteIcon('pencil', 'link-highlight');
},
},
methods: {
setPageTitle() {
const currentPageTitleScope = this.titleEl.innerText.split('·');
currentPageTitleScope[0] = `${this.titleText} (${this.issuableRef}) `;
this.titleEl.textContent = currentPageTitleScope.join('·');
},
edit() {
eventHub.$emit('open.form');
},
},
};
</script>
<template>
<h2
class="title"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation
}"
v-html="titleHtml"
>
</h2>
<div class="title-container">
<h2
class="title"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation
}"
v-html="titleHtml"
>
</h2>
<button
v-tooltip
v-if="showInlineEditButton && canUpdate"
type="button"
class="btn-blank btn-edit note-action-button"
v-html="pencilIcon"
title="Edit title and description"
data-placement="bottom"
data-container="body"
@click="edit"
>
</button>
</div>
</template>

View File

@ -43,16 +43,6 @@
type: 'link',
});
}
if (this.job.retry_path) {
actions.push({
label: 'Retry',
path: this.job.retry_path,
cssClass: 'js-retry-button btn btn-inverted-secondary visible-md-block visible-lg-block',
type: 'ujs-link',
});
}
return actions;
},
},

View File

@ -403,7 +403,11 @@ export const setCiStatusFavicon = (pageUrl) => {
});
};
export const spriteIcon = icon => `<svg><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`;
export const spriteIcon = (icon, className = '') => {
const classAttribute = className.length > 0 ? `class="${className}"` : '';
return `<svg ${classAttribute}><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`;
};
export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;

View File

@ -1,8 +1,29 @@
const DateFix = {
dashedFix(val) {
const [y, m, d] = val.split('-');
return new Date(y, m - 1, d);
},
export const pad = (val, len = 2) => (`0${val}`).slice(-len);
/**
* Formats dates in Pickaday
* @param {String} dateString Date in yyyy-mm-dd format
* @return {Date} UTC format
*/
export const parsePikadayDate = (dateString) => {
const parts = dateString.split('-');
const year = parseInt(parts[0], 10);
const month = parseInt(parts[1] - 1, 10);
const day = parseInt(parts[2], 10);
return new Date(year, month, day);
};
export default DateFix;
/**
* Used `onSelect` method in pickaday
* @param {Date} date UTC format
* @return {String} Date formated in yyyy-mm-dd
*/
export const pikadayToString = (date) => {
const day = pad(date.getDate());
const month = pad(date.getMonth() + 1);
const year = date.getFullYear();
return `${year}-${month}-${day}`;
};

View File

@ -85,7 +85,7 @@ w.gl.utils.getLocationHash = function(url) {
return hashIndex === -1 ? null : url.substring(hashIndex + 1);
};
w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(window.location.href);
// eslint-disable-next-line import/prefer-default-export
export function visitUrl(url, external = false) {
@ -96,7 +96,7 @@ export function visitUrl(url, external = false) {
otherWindow.opener = null;
otherWindow.location = url;
} else {
document.location.href = url;
window.location.href = url;
}
}

View File

@ -21,15 +21,6 @@ window._ = _;
window.Dropzone = Dropzone;
window.Sortable = Sortable;
// shortcuts
import './shortcuts';
import './shortcuts_blob';
import './shortcuts_dashboard_navigation';
import './shortcuts_navigation';
import './shortcuts_find_file';
import './shortcuts_issuable';
import './shortcuts_network';
// templates
import './templates/issuable_template_selector';
import './templates/issuable_template_selectors';
@ -52,24 +43,17 @@ import './admin';
import './aside';
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
import './broadcast_message';
import './commits';
import './compare';
import './compare_autocomplete';
import './confirm_danger_modal';
import './copy_as_gfm';
import './copy_to_clipboard';
import './diff';
import './dropzone_input';
import './due_date_select';
import './files_comment_button';
import Flash from './flash';
import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown';
import './gl_field_error';
import './gl_field_errors';
import './gl_form';
import './group_avatar';
import './group_label_subscription';
import './groups_select';
import './header';
import './importer_status';
@ -83,8 +67,6 @@ import './layout_nav';
import LazyLoader from './lazy_loader';
import './line_highlighter';
import './logo';
import './member_expiration_date';
import './members';
import './merge_request';
import './merge_request_tabs';
import './milestone';
@ -338,4 +320,10 @@ $(function () {
event.preventDefault();
gl.utils.visitUrl(`${action}${$(this).serialize()}`);
});
const flashContainer = document.querySelector('.flash-container');
if (flashContainer && flashContainer.children.length) {
removeFlashClickListener(flashContainer.children[0]);
}
});

View File

@ -1,55 +1,53 @@
/* global dateFormat */
import Pikaday from 'pikaday';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
(() => {
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling
// `js-clear-input` element, then show that element when there is a value in the
// datepicker, and make clicking on that element clear the field.
//
window.gl = window.gl || {};
gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => {
function toggleClearInput() {
$(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
}
const inputs = $(selector);
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling
// `js-clear-input` element, then show that element when there is a value in the
// datepicker, and make clicking on that element clear the field.
//
export default function memberExpirationDate(selector = '.js-access-expiration-date') {
function toggleClearInput() {
$(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
}
const inputs = $(selector);
inputs.each((i, el) => {
const $input = $(el);
inputs.each((i, el) => {
const $input = $(el);
const calendar = new Pikaday({
field: $input.get(0),
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
minDate: new Date(),
container: $input.parent().get(0),
onSelect(dateText) {
$input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
const calendar = new Pikaday({
field: $input.get(0),
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
minDate: new Date(),
container: $input.parent().get(0),
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect(dateText) {
$input.val(calendar.toString(dateText));
$input.trigger('change');
$input.trigger('change');
toggleClearInput.call($input);
},
});
calendar.setDate(new Date($input.val()));
$input.data('pikaday', calendar);
toggleClearInput.call($input);
},
});
inputs.next('.js-clear-input').on('click', function clicked(event) {
event.preventDefault();
calendar.setDate(parsePikadayDate($input.val()));
$input.data('pikaday', calendar);
});
const input = $(this).closest('.clearable-input').find(selector);
const calendar = input.data('pikaday');
inputs.next('.js-clear-input').on('click', function clicked(event) {
event.preventDefault();
calendar.setDate(null);
input.trigger('change');
toggleClearInput.call(input);
});
const input = $(this).closest('.clearable-input').find(selector);
const calendar = input.data('pikaday');
inputs.on('blur', toggleClearInput);
calendar.setDate(null);
input.trigger('change');
toggleClearInput.call(input);
});
inputs.each(toggleClearInput);
};
}).call(window);
inputs.on('blur', toggleClearInput);
inputs.each(toggleClearInput);
}

View File

@ -1,81 +1,74 @@
/* eslint-disable class-methods-use-this */
(() => {
window.gl = window.gl || {};
export default class Members {
constructor() {
this.addListeners();
this.initGLDropdown();
}
class Members {
constructor() {
this.addListeners();
this.initGLDropdown();
}
addListeners() {
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
$('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
}
addListeners() {
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
$('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
}
initGLDropdown() {
$('.js-member-permissions-dropdown').each((i, btn) => {
const $btn = $(btn);
initGLDropdown() {
$('.js-member-permissions-dropdown').each((i, btn) => {
const $btn = $(btn);
$btn.glDropdown({
selectable: true,
isSelectable(selected, $el) {
return !$el.hasClass('is-active');
},
fieldName: $btn.data('field-name'),
id(selected, $el) {
return $el.data('id');
},
toggleLabel(selected, $el) {
return $el.text();
},
clicked: (options) => {
this.formSubmit(null, options.$el);
},
});
$btn.glDropdown({
selectable: true,
isSelectable(selected, $el) {
return !$el.hasClass('is-active');
},
fieldName: $btn.data('field-name'),
id(selected, $el) {
return $el.data('id');
},
toggleLabel(selected, $el) {
return $el.text();
},
clicked: (options) => {
this.formSubmit(null, options.$el);
},
});
}
});
}
// eslint-disable-next-line class-methods-use-this
removeRow(e) {
const $target = $(e.target);
removeRow(e) {
const $target = $(e.target);
if ($target.hasClass('btn-remove')) {
$target.closest('.member')
.fadeOut(function fadeOutMemberRow() {
$(this).remove();
});
}
}
formSubmit(e, $el = null) {
const $this = e ? $(e.currentTarget) : $el;
const { $toggle, $dateInput } = this.getMemberListItems($this);
$this.closest('form').trigger('submit.rails');
$toggle.disable();
$dateInput.disable();
}
formSuccess(e) {
const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member'));
$toggle.enable();
$dateInput.enable();
}
getMemberListItems($el) {
const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`);
return {
$memberListItem,
$toggle: $memberListItem.find('.dropdown-menu-toggle'),
$dateInput: $memberListItem.find('.js-access-expiration-date'),
};
if ($target.hasClass('btn-remove')) {
$target.closest('.member')
.fadeOut(function fadeOutMemberRow() {
$(this).remove();
});
}
}
gl.Members = Members;
})();
formSubmit(e, $el = null) {
const $this = e ? $(e.currentTarget) : $el;
const { $toggle, $dateInput } = this.getMemberListItems($this);
$this.closest('form').trigger('submit.rails');
$toggle.disable();
$dateInput.disable();
}
formSuccess(e) {
const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member'));
$toggle.enable();
$dateInput.enable();
}
// eslint-disable-next-line class-methods-use-this
getMemberListItems($el) {
const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`);
return {
$memberListItem,
$toggle: $memberListItem.find('.dropdown-menu-toggle'),
$dateInput: $memberListItem.find('.js-access-expiration-date'),
};
}
}

View File

@ -11,8 +11,8 @@ import {
handleLocationHash,
isMetaClick,
} from './lib/utils/common_utils';
import initDiscussionTab from './image_diff/init_discussion_tab';
import Diff from './diff';
/* eslint-disable max-len */
// MergeRequestTabs
@ -292,7 +292,7 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
}
this.diffsLoaded = true;
new gl.Diff();
new Diff();
this.scrollToElement('#diffs');
$('.diff-file').each((i, el) => {

View File

@ -146,7 +146,9 @@ import _ from 'underscore';
clicked: function(options) {
const { $el, e } = options;
let selected = options.selectedObj;
var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
if (!selected) return;
page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');

View File

@ -1,6 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */
/* global ShortcutsNetwork */
import ShortcutsNetwork from '../shortcuts_network';
import Network from './network';
$(function() {

View File

@ -13,7 +13,6 @@ import $ from 'jquery';
import _ from 'underscore';
import Cookies from 'js-cookie';
import autosize from 'vendor/autosize';
import Dropzone from 'dropzone';
import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
@ -22,13 +21,11 @@ import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
import Autosave from './autosave';
import './dropzone_input';
import TaskList from './task_list';
import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
window.autosize = autosize;
window.Dropzone = Dropzone;
function normalizeNewlines(str) {
return str.replace(/\r\n/g, '\n');
@ -1283,10 +1280,12 @@ export default class Notes {
* Get data from Form attributes to use for saving/submitting comment.
*/
getFormData($form) {
const content = $form.find('.js-note-text').val();
return {
formData: $form.serialize(),
formContent: _.escape($form.find('.js-note-text').val()),
formContent: _.escape(content),
formAction: $form.attr('action'),
formContentOriginal: content,
};
}
@ -1418,7 +1417,7 @@ export default class Notes {
const isMainForm = $form.hasClass('js-main-target-form');
const isDiscussionForm = $form.hasClass('js-discussion-note-form');
const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button');
const { formData, formContent, formAction } = this.getFormData($form);
const { formData, formContent, formAction, formContentOriginal } = this.getFormData($form);
let noteUniqueId;
let systemNoteUniqueId;
let hasQuickActions = false;
@ -1577,7 +1576,7 @@ export default class Notes {
$form = $notesContainer.parent().find('form');
}
$form.find('.js-note-text').val(formContent);
$form.find('.js-note-text').val(formContentOriginal);
this.reenableTargetFormSubmitButton(e);
this.addNoteError($form);
});

View File

@ -12,6 +12,15 @@
type: Object,
required: true,
},
// Can be rendered in 3 different places, with some visual differences
// Accepts root | child
// `root` -> main view
// `child` -> rendered inside MR or Commit View
viewType: {
type: String,
required: false,
default: 'root',
},
},
components: {
tablePagination,
@ -187,7 +196,7 @@
:empty-state-svg-path="emptyStateSvgPath"
/>
<error-state
<error-state
v-if="shouldRenderErrorState"
:error-state-svg-path="errorStateSvgPath"
/>
@ -206,6 +215,7 @@
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsPath"
:view-type="viewType"
/>
</div>

View File

@ -21,6 +21,10 @@
type: String,
required: true,
},
viewType: {
type: String,
required: true,
},
},
components: {
pipelinesTableRowComponent,
@ -59,6 +63,7 @@
:pipeline="model"
:update-graph-dropdown="updateGraphDropdown"
:auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
/>
</div>
</template>

View File

@ -29,6 +29,10 @@ export default {
type: String,
required: true,
},
viewType: {
type: String,
required: true,
},
},
components: {
asyncButtonComponent,
@ -203,9 +207,13 @@ export default {
displayPipelineActions() {
return this.pipeline.flags.retryable ||
this.pipeline.flags.cancelable ||
this.pipeline.details.manual_actions.length ||
this.pipeline.details.artifacts.length;
this.pipeline.flags.cancelable ||
this.pipeline.details.manual_actions.length ||
this.pipeline.details.artifacts.length;
},
isChildView() {
return this.viewType === 'child';
},
},
};
@ -218,7 +226,10 @@ export default {
Status
</div>
<div class="table-mobile-content">
<ci-badge :status="pipelineStatus"/>
<ci-badge
:status="pipelineStatus"
:show-text="!isChildView"
/>
</div>
</div>
@ -240,7 +251,9 @@ export default {
:commit-url="commitUrl"
:short-sha="commitShortSha"
:title="commitTitle"
:author="commitAuthor"/>
:author="commitAuthor"
:show-branch="!isChildView"
/>
</div>
</div>

View File

@ -57,7 +57,7 @@
},
showError(message) {
Flash((errorMessages[message]));
Flash(errorMessages[message]);
},
},
};

View File

@ -57,7 +57,7 @@
},
showError(message) {
Flash((errorMessages[message]));
Flash(errorMessages[message]);
},
},
};

View File

@ -29,11 +29,9 @@ export const fetchList = ({ commit }, { repo, page }) => {
});
};
export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath)
.then(res => res.json());
export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath);
export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath)
.then(res => res.json());
export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath);
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);

View File

@ -38,7 +38,7 @@ export default {
tag: element.name,
revision: element.revision,
shortRevision: element.short_revision,
size: element.size,
size: element.total_size,
layers: element.layers,
location: element.location,
createdAt: element.created_at,

View File

@ -0,0 +1,115 @@
<script>
import flash, { hideFlash } from '../../flash';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import eventHub from '../event_hub';
export default {
components: {
loadingIcon,
},
props: {
currentBranch: {
type: String,
required: true,
},
},
data() {
return {
branchName: '',
loading: false,
};
},
computed: {
btnDisabled() {
return this.loading || this.branchName === '';
},
},
methods: {
toggleDropdown() {
this.$dropdown.dropdown('toggle');
},
submitNewBranch() {
// need to query as the element is appended outside of Vue
const flashEl = this.$refs.flashContainer.querySelector('.flash-alert');
this.loading = true;
if (flashEl) {
hideFlash(flashEl, false);
}
eventHub.$emit('createNewBranch', this.branchName);
},
showErrorMessage(message) {
this.loading = false;
flash(message, 'alert', this.$el);
},
createdNewBranch(newBranchName) {
this.loading = false;
this.branchName = '';
if (this.dropdownText) {
this.dropdownText.textContent = newBranchName;
}
},
},
created() {
// Dropdown is outside of Vue instance & is controlled by Bootstrap
this.$dropdown = $('.git-revision-dropdown');
// text element is outside Vue app
this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text');
eventHub.$on('createNewBranchSuccess', this.createdNewBranch);
eventHub.$on('createNewBranchError', this.showErrorMessage);
eventHub.$on('toggleNewBranchDropdown', this.toggleDropdown);
},
destroyed() {
eventHub.$off('createNewBranchSuccess', this.createdNewBranch);
eventHub.$off('toggleNewBranchDropdown', this.toggleDropdown);
eventHub.$off('createNewBranchError', this.showErrorMessage);
},
};
</script>
<template>
<div>
<div
class="flash-container"
ref="flashContainer"
>
</div>
<p>
Create from:
<code>{{ currentBranch }}</code>
</p>
<input
class="form-control js-new-branch-name"
type="text"
placeholder="Name new branch"
v-model="branchName"
@keyup.enter.stop.prevent="submitNewBranch"
/>
<div class="prepend-top-default clearfix">
<button
type="button"
class="btn btn-primary pull-left"
:disabled="btnDisabled"
@click.stop.prevent="submitNewBranch"
>
<loading-icon
v-if="loading"
:inline="true"
/>
<span>Create</span>
</button>
<button
type="button"
class="btn btn-default pull-right"
@click.stop.prevent="toggleDropdown"
>
Cancel
</button>
</div>
</div>
</template>

View File

@ -8,10 +8,14 @@ import RepoMixin from '../mixins/repo_mixin';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
import eventHub from '../event_hub';
export default {
data: () => Store,
data() {
return Store;
},
mixins: [RepoMixin],
components: {
RepoSidebar,
@ -22,12 +26,19 @@ export default {
PopupDialog,
RepoPreview,
},
created() {
eventHub.$on('createNewBranch', this.createNewBranch);
},
mounted() {
Helper.getContent().catch(Helper.loadingError);
},
destroyed() {
eventHub.$off('createNewBranch', this.createNewBranch);
},
methods: {
getCurrentLocation() {
return location.href;
},
toggleDialogOpen(toggle) {
this.dialog.open = toggle;
},
@ -36,8 +47,25 @@ export default {
this.toggleDialogOpen(false);
this.dialog.status = status;
},
toggleBlobView: Store.toggleBlobView,
createNewBranch(branch) {
Service.createBranch({
branch,
ref: Store.currentBranch,
}).then((res) => {
const newBranchName = res.data.name;
const newUrl = this.getCurrentLocation().replace(Store.currentBranch, newBranchName);
Store.currentBranch = newBranchName;
history.pushState({ key: Helper.key }, '', newUrl);
eventHub.$emit('createNewBranchSuccess', newBranchName);
eventHub.$emit('toggleNewBranchDropdown');
}).catch((err) => {
eventHub.$emit('createNewBranchError', err.response.data.message);
});
},
},
};
</script>

View File

@ -3,12 +3,20 @@ import Flash from '../../flash';
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
import Service from '../services/repo_service';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import { visitUrl } from '../../lib/utils/url_utility';
export default {
data: () => Store,
mixins: [RepoMixin],
data() {
return Store;
},
components: {
PopupDialog,
},
computed: {
showCommitable() {
return this.isCommitable && this.changedFiles.length;
@ -28,7 +36,16 @@ export default {
},
methods: {
makeCommit() {
commitToNewBranch(status) {
if (status) {
this.showNewBranchDialog = false;
this.tryCommit(null, true, true);
} else {
// reset the state
}
},
makeCommit(newBranch) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const commitMessage = this.commitMessage;
const actions = this.changedFiles.map(f => ({
@ -36,19 +53,63 @@ export default {
file_path: f.path,
content: f.newContent,
}));
const branch = newBranch ? `${this.currentBranch}-${this.currentShortHash}` : this.currentBranch;
const payload = {
branch: Store.currentBranch,
branch,
commit_message: commitMessage,
actions,
};
Store.submitCommitsLoading = true;
if (newBranch) {
payload.start_branch = this.currentBranch;
}
this.submitCommitsLoading = true;
Service.commitFiles(payload)
.then(this.resetCommitState)
.catch(() => Flash('An error occurred while committing your changes'));
.then(() => {
this.resetCommitState();
if (this.startNewMR) {
this.redirectToNewMr(branch);
} else {
this.redirectToBranch(branch);
}
})
.catch(() => {
Flash('An error occurred while committing your changes');
});
},
tryCommit(e, skipBranchCheck = false, newBranch = false) {
if (skipBranchCheck) {
this.makeCommit(newBranch);
} else {
Store.setBranchHash()
.then(() => {
if (Store.branchChanged) {
Store.showNewBranchDialog = true;
return;
}
this.makeCommit(newBranch);
})
.catch(() => {
Flash('An error occurred while committing your changes');
});
}
},
redirectToNewMr(branch) {
visitUrl(this.newMrTemplateUrl.replace('{{source_branch}}', branch));
},
redirectToBranch(branch) {
visitUrl(this.customBranchURL.replace('{{branch}}', branch));
},
resetCommitState() {
this.submitCommitsLoading = false;
this.openedFiles = this.openedFiles.map((file) => {
const f = file;
f.changed = false;
return f;
});
this.changedFiles = [];
this.commitMessage = '';
this.editMode = false;
@ -62,9 +123,17 @@ export default {
<div
v-if="showCommitable"
id="commit-area">
<popup-dialog
v-if="showNewBranchDialog"
:primary-button-label="__('Create new branch')"
kind="primary"
:title="__('Branch has changed')"
:text="__('This branch has changed since you started editing. Would you like to create a new branch?')"
@submit="commitToNewBranch"
/>
<form
class="form-horizontal"
@submit.prevent="makeCommit">
@submit.prevent="tryCommit">
<fieldset>
<div class="form-group">
<label class="col-md-4 control-label staged-files">
@ -117,7 +186,7 @@ export default {
class="btn btn-success">
<i
v-if="submitCommitsLoading"
class="fa fa-spinner fa-spin"
class="js-commit-loading-icon fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="loading">
</i>
@ -126,6 +195,14 @@ export default {
</span>
</button>
</div>
<div class="col-md-offset-4 col-md-6">
<div class="checkbox">
<label>
<input type="checkbox" v-model="startNewMR">
<span>Start a <strong>new merge request</strong> with these changes</span>
</label>
</div>
</div>
</fieldset>
</form>
</div>

View File

@ -3,7 +3,9 @@ import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
export default {
data: () => Store,
data() {
return Store;
},
mixins: [RepoMixin],
computed: {
buttonLabel() {

View File

@ -5,7 +5,9 @@ import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper';
const RepoEditor = {
data: () => Store,
data() {
return Store;
},
destroyed() {
if (Helper.monacoInstance) {
@ -22,7 +24,8 @@ const RepoEditor = {
const monacoInstance = Helper.monaco.editor.create(this.$el, {
model: null,
readOnly: false,
contextmenu: false,
contextmenu: true,
scrollBeyondLastLine: false,
});
Helper.monacoInstance = monacoInstance;
@ -92,7 +95,7 @@ const RepoEditor = {
},
blobRaw() {
if (Helper.monacoInstance && !this.isTree) {
if (Helper.monacoInstance) {
this.setupEditor();
}
},

View File

@ -1,107 +1,95 @@
<script>
import TimeAgoMixin from '../../vue_shared/mixins/timeago';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
import eventHub from '../event_hub';
import repoMixin from '../mixins/repo_mixin';
const RepoFile = {
mixins: [TimeAgoMixin],
props: {
file: {
type: Object,
required: true,
export default {
mixins: [
repoMixin,
timeAgoMixin,
],
props: {
file: {
type: Object,
required: true,
},
},
isMini: {
type: Boolean,
required: false,
default: false,
computed: {
fileIcon() {
const classObj = {
'fa-spinner fa-spin': this.file.loading,
[this.file.icon]: !this.file.loading,
'fa-folder-open': !this.file.loading && this.file.opened,
};
return classObj;
},
levelIndentation() {
return {
marginLeft: `${this.file.level * 16}px`,
};
},
shortId() {
return this.file.id.substr(0, 8);
},
},
loading: {
type: Object,
required: false,
default() { return { tree: false }; },
methods: {
linkClicked(file) {
eventHub.$emit('fileNameClicked', file);
},
},
hasFiles: {
type: Boolean,
required: false,
default: false,
},
activeFile: {
type: Object,
required: true,
},
},
computed: {
canShowFile() {
return !this.loading.tree || this.hasFiles;
},
fileIcon() {
const classObj = {
'fa-spinner fa-spin': this.file.loading,
[this.file.icon]: !this.file.loading,
};
return classObj;
},
fileIndentation() {
return {
'margin-left': `${this.file.level * 10}px`,
};
},
activeFileClass() {
return {
active: this.activeFile.url === this.file.url,
};
},
},
methods: {
linkClicked(file) {
this.$emit('linkclicked', file);
},
},
};
export default RepoFile;
};
</script>
<template>
<tr
v-if="canShowFile"
class="file"
:class="activeFileClass"
@click.prevent="linkClicked(file)">
<td>
<i
class="fa fa-fw file-icon"
:class="fileIcon"
:style="fileIndentation"
aria-label="file icon">
</i>
<a
:href="file.url"
class="repo-file-name"
:title="file.url">
{{file.name}}
</a>
</td>
<tr
class="file"
@click.prevent="linkClicked(file)">
<td>
<i
class="fa fa-fw file-icon"
:class="fileIcon"
:style="levelIndentation"
aria-hidden="true"
>
</i>
<a
:href="file.url"
class="repo-file-name"
>
{{ file.name }}
</a>
<template v-if="file.type === 'submodule' && file.id">
@
<span class="commit-sha">
<a
@click.stop
:href="file.tree_url"
>
{{ shortId }}
</a>
</span>
</template>
</td>
<template v-if="!isMini">
<td class="hidden-sm hidden-xs">
<div class="commit-message">
<a @click.stop :href="file.lastCommitUrl">
{{file.lastCommitMessage}}
<template v-if="!isMini">
<td class="hidden-sm hidden-xs">
<a
@click.stop
:href="file.lastCommit.url"
class="commit-message"
>
{{ file.lastCommit.message }}
</a>
</div>
</td>
</td>
<td class="hidden-xs text-right">
<span
class="commit-update"
:title="tooltipTitle(file.lastCommitUpdate)">
{{timeFormated(file.lastCommitUpdate)}}
</span>
</td>
</template>
</tr>
<td class="commit-update hidden-xs text-right">
<span
v-if="file.lastCommit.updatedAt"
:title="tooltipTitle(file.lastCommit.updatedAt)"
>
{{ timeFormated(file.lastCommit.updatedAt) }}
</span>
</td>
</template>
</tr>
</template>

View File

@ -4,7 +4,9 @@ import Helper from '../helpers/repo_helper';
import RepoMixin from '../mixins/repo_mixin';
const RepoFileButtons = {
data: () => Store,
data() {
return Store;
},
mixins: [RepoMixin],

View File

@ -1,25 +0,0 @@
<script>
const RepoFileOptions = {
props: {
isMini: {
type: Boolean,
required: false,
default: false,
},
projectName: {
type: String,
required: true,
},
},
};
export default RepoFileOptions;
</script>
<template>
<tr v-if="isMini" class="repo-file-options">
<td>
<span class="title">{{projectName}}</span>
</td>
</tr>
</template>

View File

@ -1,43 +1,23 @@
<script>
const RepoLoadingFile = {
props: {
loading: {
type: Object,
required: false,
default: {},
},
hasFiles: {
type: Boolean,
required: false,
default: false,
},
isMini: {
type: Boolean,
required: false,
default: false,
},
},
import repoMixin from '../mixins/repo_mixin';
computed: {
showGhostLines() {
return this.loading.tree && !this.hasFiles;
export default {
mixins: [
repoMixin,
],
methods: {
lineOfCode(n) {
return `skeleton-line-${n}`;
},
},
},
methods: {
lineOfCode(n) {
return `skeleton-line-${n}`;
},
},
};
export default RepoLoadingFile;
};
</script>
<template>
<tr
v-if="showGhostLines"
class="loading-file">
class="loading-file"
aria-label="Loading files"
>
<td>
<div
class="animation-container animation-container-small">
@ -48,29 +28,28 @@ export default RepoLoadingFile;
</div>
</div>
</td>
<td
v-if="!isMini"
class="hidden-sm hidden-xs">
<div class="animation-container">
<div
v-for="n in 6"
:key="n"
:class="lineOfCode(n)">
<template v-if="!isMini">
<td
class="hidden-sm hidden-xs">
<div class="animation-container">
<div
v-for="n in 6"
:key="n"
:class="lineOfCode(n)">
</div>
</div>
</div>
</td>
</td>
<td
v-if="!isMini"
class="hidden-xs">
<div class="animation-container animation-container-small">
<div
v-for="n in 6"
:key="n"
:class="lineOfCode(n)">
<td
class="hidden-xs">
<div class="animation-container animation-container-small animation-container-right">
<div
v-for="n in 6"
:key="n"
:class="lineOfCode(n)">
</div>
</div>
</div>
</td>
</td>
</template>
</tr>
</template>

View File

@ -1,38 +1,38 @@
<script>
import RepoMixin from '../mixins/repo_mixin';
import eventHub from '../event_hub';
import repoMixin from '../mixins/repo_mixin';
const RepoPreviousDirectory = {
props: {
prevUrl: {
type: String,
required: true,
export default {
mixins: [
repoMixin,
],
props: {
prevUrl: {
type: String,
required: true,
},
},
},
mixins: [RepoMixin],
computed: {
colSpanCondition() {
return this.isMini ? undefined : 3;
computed: {
colSpanCondition() {
return this.isMini ? undefined : 3;
},
},
},
methods: {
linkClicked(file) {
this.$emit('linkclicked', file);
methods: {
linkClicked(file) {
eventHub.$emit('goToPreviousDirectoryClicked', file);
},
},
},
};
export default RepoPreviousDirectory;
};
</script>
<template>
<tr class="prev-directory">
<td
:colspan="colSpanCondition"
@click.prevent="linkClicked(prevUrl)">
<a :href="prevUrl">..</a>
</td>
</tr>
<tr class="file prev-directory">
<td
:colspan="colSpanCondition"
class="table-cell"
@click.prevent="linkClicked(prevUrl)"
>
<a :href="prevUrl">...</a>
</td>
</tr>
</template>

View File

@ -4,7 +4,9 @@
import Store from '../stores/repo_store';
export default {
data: () => Store,
data() {
return Store;
},
computed: {
html() {
return this.activeFile.html;

View File

@ -1,9 +1,10 @@
<script>
import _ from 'underscore';
import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper';
import Store from '../stores/repo_store';
import eventHub from '../event_hub';
import RepoPreviousDirectory from './repo_prev_directory.vue';
import RepoFileOptions from './repo_file_options.vue';
import RepoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue';
import RepoMixin from '../mixins/repo_mixin';
@ -11,21 +12,35 @@ import RepoMixin from '../mixins/repo_mixin';
export default {
mixins: [RepoMixin],
components: {
'repo-file-options': RepoFileOptions,
'repo-previous-directory': RepoPreviousDirectory,
'repo-file': RepoFile,
'repo-loading-file': RepoLoadingFile,
},
created() {
window.addEventListener('popstate', this.checkHistory);
},
destroyed() {
eventHub.$off('fileNameClicked', this.fileClicked);
eventHub.$off('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
window.removeEventListener('popstate', this.checkHistory);
},
mounted() {
eventHub.$on('fileNameClicked', this.fileClicked);
eventHub.$on('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
},
data() {
return Store;
},
computed: {
flattendFiles() {
const mapFiles = arr => (!arr.files.length ? [] : _.map(arr.files, a => [a, mapFiles(a)]));
data: () => Store,
return _.chain(this.files)
.map(arr => [arr, mapFiles(arr)])
.flatten()
.value();
},
},
methods: {
checkHistory() {
let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1);
@ -52,21 +67,25 @@ export default {
},
fileClicked(clickedFile, lineNumber) {
let file = clickedFile;
const file = clickedFile;
if (file.loading) return;
file.loading = true;
if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file);
file.loading = false;
Helper.setDirectoryToClosed(file);
Store.setActiveLine(lineNumber);
} else if (file.type === 'submodule') {
file.loading = true;
gl.utils.visitUrl(file.url);
} else {
const openFile = Helper.getFileFromPath(file.url);
if (openFile) {
file.loading = false;
Store.setActiveFiles(openFile);
Store.setActiveLine(lineNumber);
} else {
file.loading = true;
Service.url = file.url;
Helper.getContent(file)
.then(() => {
@ -81,7 +100,7 @@ export default {
goToPreviousDirectoryClicked(prevURL) {
Service.url = prevURL;
Helper.getContent(null)
Helper.getContent(null, true)
.then(() => Helper.scrollTabsRight())
.catch(Helper.loadingError);
},
@ -92,38 +111,43 @@ export default {
<template>
<div id="sidebar" :class="{'sidebar-mini' : isMini}">
<table class="table">
<thead v-if="!isMini">
<thead>
<tr>
<th class="name">Name</th>
<th class="hidden-sm hidden-xs last-commit">Last commit</th>
<th class="hidden-xs last-update text-right">Last update</th>
<th
v-if="isMini"
class="repo-file-options title"
>
<strong class="clgray">
{{ projectName }}
</strong>
</th>
<template v-else>
<th class="name">
Name
</th>
<th class="hidden-sm hidden-xs last-commit">
Last commit
</th>
<th class="hidden-xs last-update text-right">
Last update
</th>
</template>
</tr>
</thead>
<tbody>
<repo-file-options
:is-mini="isMini"
:project-name="projectName"
/>
<repo-previous-directory
v-if="isRoot"
v-if="!isRoot && !loading.tree"
:prev-url="prevURL"
@linkclicked="goToPreviousDirectoryClicked(prevURL)"/>
/>
<repo-loading-file
v-if="!flattendFiles.length && loading.tree"
v-for="n in 5"
:key="n"
:loading="loading"
:has-files="!!files.length"
:is-mini="isMini"
/>
<repo-file
v-for="file in files"
v-for="file in flattendFiles"
:key="file.id"
:file="file"
:is-mini="isMini"
@linkclicked="fileClicked(file)"
:is-tree="isTree"
:has-files="!!files.length"
:active-file="activeFile"
/>
</tbody>
</table>

View File

@ -26,11 +26,13 @@ const RepoTab = {
},
methods: {
tabClicked: Store.setActiveFiles,
tabClicked(file) {
Store.setActiveFiles(file);
},
closeTab(file) {
if (file.changed) return;
this.$emit('tabclosed', file);
Store.removeFromOpenedFiles(file);
},
},
};
@ -39,25 +41,28 @@ export default RepoTab;
</script>
<template>
<li @click="tabClicked(tab)">
<a
href="#0"
class="close"
@click.stop.prevent="closeTab(tab)"
:aria-label="closeLabel">
<i
class="fa"
:class="changedClass"
aria-hidden="true">
</i>
</a>
<li
:class="{ active : tab.active }"
@click="tabClicked(tab)"
>
<button
type="button"
class="close-btn"
@click.stop.prevent="closeTab(tab)"
:aria-label="closeLabel">
<i
class="fa"
:class="changedClass"
aria-hidden="true">
</i>
</button>
<a
href="#"
class="repo-tab"
:title="tab.url"
@click.prevent="tabClicked(tab)">
{{tab.name}}
</a>
</li>
<a
href="#"
class="repo-tab"
:title="tab.url"
@click.prevent="tabClicked(tab)">
{{tab.name}}
</a>
</li>
</template>

View File

@ -1,36 +1,29 @@
<script>
import Store from '../stores/repo_store';
import RepoTab from './repo_tab.vue';
import RepoMixin from '../mixins/repo_mixin';
import Store from '../stores/repo_store';
import RepoTab from './repo_tab.vue';
import RepoMixin from '../mixins/repo_mixin';
const RepoTabs = {
mixins: [RepoMixin],
components: {
'repo-tab': RepoTab,
},
data: () => Store,
methods: {
tabClosed(file) {
Store.removeFromOpenedFiles(file);
export default {
mixins: [RepoMixin],
components: {
'repo-tab': RepoTab,
},
},
};
export default RepoTabs;
data() {
return Store;
},
};
</script>
<template>
<ul id="tabs">
<repo-tab
v-for="tab in openedFiles"
:key="tab.id"
:tab="tab"
:class="{'active' : tab.active}"
@tabclosed="tabClosed"
/>
<li class="tabs-divider" />
</ul>
<ul
id="tabs"
class="list-unstyled"
>
<repo-tab
v-for="tab in openedFiles"
:key="tab.id"
:tab="tab"
/>
<li class="tabs-divider" />
</ul>
</template>

View File

@ -0,0 +1,3 @@
import Vue from 'vue';
export default new Vue();

View File

@ -1,3 +1,4 @@
import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
import Service from '../services/repo_service';
import Store from '../stores/repo_store';
import Flash from '../../flash';
@ -25,10 +26,6 @@ const RepoHelper = {
key: '',
isTree(data) {
return Object.hasOwnProperty.call(data, 'blobs');
},
Time: window.performance
&& window.performance.now
? window.performance
@ -58,13 +55,20 @@ const RepoHelper = {
},
setDirectoryOpen(tree, title) {
const file = tree;
if (!file) return undefined;
if (!tree) return;
file.opened = true;
file.icon = 'fa-folder-open';
RepoHelper.updateHistoryEntry(file.url, title);
return file;
Object.assign(tree, {
opened: true,
});
RepoHelper.updateHistoryEntry(tree.url, title);
},
setDirectoryToClosed(entry) {
Object.assign(entry, {
opened: false,
files: [],
});
},
isRenderable() {
@ -81,63 +85,23 @@ const RepoHelper = {
.catch(RepoHelper.loadingError);
},
// when you open a directory you need to put the directory files under
// the directory... This will merge the list of the current directory and the new list.
getNewMergedList(inDirectory, currentList, newList) {
const newListSorted = newList.sort(this.compareFilesCaseInsensitive);
if (!inDirectory) return newListSorted;
const indexOfFile = currentList.findIndex(file => file.url === inDirectory.url);
if (!indexOfFile) return newListSorted;
return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile);
},
// within the get new merged list this does the merging of the current list of files
// and the new list of files. The files are never "in" another directory they just
// appear like they are because of the margin.
mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) {
newList.reverse().forEach((newFile) => {
const fileIndex = indexOfFile + 1;
const file = newFile;
file.level = inDirectory.level + 1;
oldList.splice(fileIndex, 0, file);
});
return oldList;
},
compareFilesCaseInsensitive(a, b) {
const aName = a.name.toLowerCase();
const bName = b.name.toLowerCase();
if (a.level > 0) return 0;
if (aName < bName) { return -1; }
if (aName > bName) { return 1; }
return 0;
},
isRoot(url) {
// the url we are requesting -> split by the project URL. Grab the right side.
const isRoot = !!url.split(Store.projectUrl)[1]
// remove the first "/"
.slice(1)
// split this by "/"
.split('/')
// remove the first two items of the array... usually /tree/master.
.slice(2)
// we want to know the length of the array.
// If greater than 0 not root.
.length;
return isRoot;
},
getContent(treeOrFile) {
getContent(treeOrFile, emptyFiles = false) {
let file = treeOrFile;
if (!Store.files.length) {
Store.loading.tree = true;
}
return Service.getContent()
.then((response) => {
const data = response.data;
if (response.headers && response.headers['page-title']) data.pageTitle = response.headers['page-title'];
if (response.headers && response.headers['page-title']) data.pageTitle = decodeURI(response.headers['page-title']);
if (response.headers && response.headers['is-root'] && !Store.isInitialRoot) {
Store.isRoot = convertPermissionToBoolean(response.headers['is-root']);
Store.isInitialRoot = Store.isRoot;
}
Store.isTree = RepoHelper.isTree(data);
if (!Store.isTree) {
if (file && file.type === 'blob') {
if (!file) file = data;
Store.binary = data.binary;
@ -145,38 +109,40 @@ const RepoHelper = {
// file might be undefined
RepoHelper.setBinaryDataAsBase64(data);
Store.setViewToPreview();
} else if (!Store.isPreviewView()) {
if (!data.render_error) {
Service.getRaw(data.raw_path)
.then((rawResponse) => {
Store.blobRaw = rawResponse.data;
data.plain = rawResponse.data;
RepoHelper.setFile(data, file);
}).catch(RepoHelper.loadingError);
}
} else if (!Store.isPreviewView() && !data.render_error) {
Service.getRaw(data.raw_path)
.then((rawResponse) => {
Store.blobRaw = rawResponse.data;
data.plain = rawResponse.data;
RepoHelper.setFile(data, file);
}).catch(RepoHelper.loadingError);
}
if (Store.isPreviewView()) {
RepoHelper.setFile(data, file);
}
// if the file tree is empty
if (Store.files.length === 0) {
const parentURL = Service.blobURLtoParentTree(Service.url);
Service.url = parentURL;
RepoHelper.getContent();
}
} else {
// it's a tree
if (!file) Store.isRoot = RepoHelper.isRoot(Service.url);
file = RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name);
const newDirectory = RepoHelper.dataToListOfFiles(data);
Store.addFilesToDirectory(file, Store.files, newDirectory);
Store.loading.tree = false;
RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name);
if (emptyFiles) {
Store.files = [];
}
this.addToDirectory(file, data);
Store.prevURL = Service.blobURLtoParentTree(Service.url);
}
}).catch(RepoHelper.loadingError);
},
addToDirectory(file, data) {
const tree = file || Store;
const files = tree.files.concat(this.dataToListOfFiles(data, file ? file.level + 1 : 0));
tree.files = files;
},
setFile(data, file) {
const newFile = data;
newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh.
@ -190,57 +156,41 @@ const RepoHelper = {
Store.setActiveFiles(newFile);
},
serializeBlob(blob) {
const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob);
simpleBlob.lastCommitMessage = blob.last_commit.message;
simpleBlob.lastCommitUpdate = blob.last_commit.committed_date;
simpleBlob.loading = false;
serializeRepoEntity(type, entity, level = 0) {
const { id, url, name, icon, last_commit, tree_url } = entity;
return simpleBlob;
},
serializeTree(tree) {
return RepoHelper.serializeRepoEntity('tree', tree);
},
serializeSubmodule(submodule) {
return RepoHelper.serializeRepoEntity('submodule', submodule);
},
serializeRepoEntity(type, entity) {
const { url, name, icon, last_commit } = entity;
const returnObj = {
return {
id,
type,
name,
url,
tree_url,
level,
icon: `fa-${icon}`,
level: 0,
files: [],
loading: false,
opened: false,
// eslint-disable-next-line camelcase
lastCommit: last_commit ? {
url: `${Store.projectUrl}/commit/${last_commit.id}`,
message: last_commit.message,
updatedAt: last_commit.committed_date,
} : {},
};
if (entity.last_commit) {
returnObj.lastCommitUrl = `${Store.projectUrl}/commit/${last_commit.id}`;
} else {
returnObj.lastCommitUrl = '';
}
return returnObj;
},
scrollTabsRight() {
// wait for the transition. 0.1 seconds.
setTimeout(() => {
const tabs = document.getElementById('tabs');
if (!tabs) return;
tabs.scrollLeft = tabs.scrollWidth;
}, 200);
const tabs = document.getElementById('tabs');
if (!tabs) return;
tabs.scrollLeft = tabs.scrollWidth;
},
dataToListOfFiles(data) {
dataToListOfFiles(data, level) {
const { blobs, trees, submodules } = data;
return [
...blobs.map(blob => RepoHelper.serializeBlob(blob)),
...trees.map(tree => RepoHelper.serializeTree(tree)),
...submodules.map(submodule => RepoHelper.serializeSubmodule(submodule)),
...trees.map(tree => RepoHelper.serializeRepoEntity('tree', tree, level)),
...submodules.map(submodule => RepoHelper.serializeRepoEntity('submodule', submodule, level)),
...blobs.map(blob => RepoHelper.serializeRepoEntity('blob', blob, level)),
];
},

View File

@ -1,9 +1,11 @@
import $ from 'jquery';
import Vue from 'vue';
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
import Service from './services/repo_service';
import Store from './stores/repo_store';
import Repo from './components/repo.vue';
import RepoEditButton from './components/repo_edit_button.vue';
import newBranchForm from './components/new_branch_form.vue';
import Translate from '../vue_shared/translate';
function initDropdowns() {
@ -31,8 +33,13 @@ function setInitialStore(data) {
Store.projectUrl = data.projectUrl;
Store.canCommit = data.canCommit;
Store.onTopOfBranch = data.onTopOfBranch;
Store.newMrTemplateUrl = decodeURIComponent(data.newMrTemplateUrl);
Store.customBranchURL = decodeURIComponent(data.blobUrl);
Store.isRoot = convertPermissionToBoolean(data.root);
Store.isInitialRoot = convertPermissionToBoolean(data.root);
Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
Store.checkIsCommitable();
Store.setBranchHash();
}
function initRepo(el) {
@ -56,6 +63,26 @@ function initRepoEditButton(el) {
});
}
function initNewBranchForm() {
const el = document.querySelector('.js-new-branch-dropdown');
if (!el) return null;
return new Vue({
el,
components: {
newBranchForm,
},
render(createElement) {
return createElement('new-branch-form', {
props: {
currentBranch: Store.currentBranch,
},
});
},
});
}
function initRepoBundle() {
const repo = document.getElementById('repo');
const editButton = document.querySelector('.editable-mode');
@ -67,6 +94,7 @@ function initRepoBundle() {
initRepo(repo);
initRepoEditButton(editButton);
initNewBranchForm();
}
$(initRepoBundle);

View File

@ -1,8 +1,11 @@
import axios from 'axios';
import csrf from '../../lib/utils/csrf';
import Store from '../stores/repo_store';
import Api from '../../api';
import Helper from '../helpers/repo_helper';
axios.defaults.headers.common[csrf.headerKey] = csrf.token;
const RepoService = {
url: '',
options: {
@ -10,6 +13,7 @@ const RepoService = {
format: 'json',
},
},
createBranchPath: '/api/:version/projects/:id/repository/branches',
richExtensionRegExp: /md/,
getRaw(url) {
@ -64,11 +68,21 @@ const RepoService = {
return urlArray.join('/');
},
getBranch() {
return Api.branchSingle(Store.projectId, Store.currentBranch);
},
commitFiles(payload) {
return Api.commitMultiple(Store.projectId, payload)
.then(this.commitFlash);
},
createBranch(payload) {
const url = Api.buildUrl(this.createBranchPath)
.replace(':id', Store.projectId);
return axios.post(url, payload);
},
commitFlash(data) {
if (data.short_id && data.stats) {
window.Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');

View File

@ -2,18 +2,18 @@ import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
const RepoStore = {
monaco: {},
monacoLoading: false,
service: '',
canCommit: false,
onTopOfBranch: false,
editMode: false,
isTree: false,
isRoot: false,
isRoot: null,
isInitialRoot: null,
prevURL: '',
projectId: '',
projectName: '',
projectUrl: '',
branchUrl: '',
blobRaw: '',
currentBlobView: 'repo-preview',
openedFiles: [],
@ -23,6 +23,7 @@ const RepoStore = {
title: '',
status: false,
},
showNewBranchDialog: false,
activeFile: Helper.getDefaultActiveFile(),
activeFileIndex: 0,
activeLine: -1,
@ -31,22 +32,27 @@ const RepoStore = {
isCommitable: false,
binary: false,
currentBranch: '',
startNewMR: false,
currentHash: '',
currentShortHash: '',
customBranchURL: '',
newMrTemplateUrl: '',
branchChanged: false,
commitMessage: '',
binaryTypes: {
png: false,
md: false,
svg: false,
unknown: false,
},
loading: {
tree: false,
blob: false,
},
resetBinaryTypes() {
Object.keys(RepoStore.binaryTypes).forEach((key) => {
RepoStore.binaryTypes[key] = false;
});
setBranchHash() {
return Service.getBranch()
.then((data) => {
if (RepoStore.currentHash !== '' && data.commit.id !== RepoStore.currentHash) {
RepoStore.branchChanged = true;
}
RepoStore.currentHash = data.commit.id;
RepoStore.currentShortHash = data.commit.short_id;
});
},
// mutations
@ -54,10 +60,6 @@ const RepoStore = {
RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit;
},
addFilesToDirectory(inDirectory, currentList, newList) {
RepoStore.files = Helper.getNewMergedList(inDirectory, currentList, newList);
},
toggleRawPreview() {
RepoStore.activeFile.raw = !RepoStore.activeFile.raw;
RepoStore.activeFileLabel = RepoStore.activeFile.raw ? 'Display rendered file' : 'Display source';
@ -111,30 +113,6 @@ const RepoStore = {
RepoStore.activeFileLabel = 'Display source';
},
removeChildFilesOfTree(tree) {
let foundTree = false;
const treeToClose = tree;
let canStopSearching = false;
RepoStore.files = RepoStore.files.filter((file) => {
const isItTheTreeWeWant = file.url === treeToClose.url;
// if it's the next tree
if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) {
canStopSearching = true;
return true;
}
if (canStopSearching) return true;
if (isItTheTreeWeWant) foundTree = true;
if (foundTree) return file.level <= treeToClose.level;
return true;
});
treeToClose.opened = false;
treeToClose.icon = 'fa-folder';
return treeToClose;
},
removeFromOpenedFiles(file) {
if (file.type === 'tree') return;
let foundIndex;
@ -168,6 +146,7 @@ const RepoStore = {
if (openedFilesAlreadyExists) return;
openFile.changed = false;
openFile.active = true;
RepoStore.openedFiles.push(openFile);
},

View File

@ -1,128 +1,116 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */
/* global Mousetrap */
import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap';
import findAndFollowLink from './shortcuts_dashboard_navigation';
(function() {
this.Shortcuts = (function() {
function Shortcuts(skipResetBindings) {
this.onToggleHelp = this.onToggleHelp.bind(this);
this.enabledHelp = [];
if (!skipResetBindings) {
Mousetrap.reset();
}
Mousetrap.bind('?', this.onToggleHelp);
Mousetrap.bind('s', Shortcuts.focusSearch);
Mousetrap.bind('f', (e => this.focusFilter(e)));
Mousetrap.bind('p b', this.onTogglePerfBar);
const defaultStopCallback = Mousetrap.stopCallback;
Mousetrap.stopCallback = (e, element, combo) => {
if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) {
return false;
}
const findFileURL = document.body.dataset.findFile;
return defaultStopCallback(e, element, combo);
};
Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity'));
Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues'));
Mousetrap.bind('shift+m', () => findAndFollowLink('.dashboard-shortcuts-merge_requests'));
Mousetrap.bind('shift+p', () => findAndFollowLink('.dashboard-shortcuts-projects'));
Mousetrap.bind('shift+g', () => findAndFollowLink('.dashboard-shortcuts-groups'));
Mousetrap.bind('shift+l', () => findAndFollowLink('.dashboard-shortcuts-milestones'));
Mousetrap.bind('shift+s', () => findAndFollowLink('.dashboard-shortcuts-snippets'));
export default class Shortcuts {
constructor(skipResetBindings) {
this.onToggleHelp = this.onToggleHelp.bind(this);
this.enabledHelp = [];
if (!skipResetBindings) {
Mousetrap.reset();
}
Mousetrap.bind('?', this.onToggleHelp);
Mousetrap.bind('s', Shortcuts.focusSearch);
Mousetrap.bind('f', this.focusFilter.bind(this));
Mousetrap.bind('p b', Shortcuts.onTogglePerfBar);
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview);
if (typeof findFileURL !== "undefined" && findFileURL !== null) {
Mousetrap.bind('t', function() {
return gl.utils.visitUrl(findFileURL);
});
}
const findFileURL = document.body.dataset.findFile;
Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity'));
Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues'));
Mousetrap.bind('shift+m', () => findAndFollowLink('.dashboard-shortcuts-merge_requests'));
Mousetrap.bind('shift+p', () => findAndFollowLink('.dashboard-shortcuts-projects'));
Mousetrap.bind('shift+g', () => findAndFollowLink('.dashboard-shortcuts-groups'));
Mousetrap.bind('shift+l', () => findAndFollowLink('.dashboard-shortcuts-milestones'));
Mousetrap.bind('shift+s', () => findAndFollowLink('.dashboard-shortcuts-snippets'));
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], Shortcuts.toggleMarkdownPreview);
if (typeof findFileURL !== 'undefined' && findFileURL !== null) {
Mousetrap.bind('t', () => {
gl.utils.visitUrl(findFileURL);
});
}
Shortcuts.prototype.onToggleHelp = function(e) {
$(document).on('click.more_help', '.js-more-help-button', function clickMoreHelp(e) {
$(this).remove();
$('.hidden-shortcut').show();
e.preventDefault();
return Shortcuts.toggleHelp(this.enabledHelp);
};
});
}
Shortcuts.prototype.onTogglePerfBar = function(e) {
e.preventDefault();
const performanceBarCookieName = 'perf_bar_enabled';
if (Cookies.get(performanceBarCookieName) === 'true') {
Cookies.remove(performanceBarCookieName, { path: '/' });
} else {
Cookies.set(performanceBarCookieName, 'true', { path: '/' });
}
gl.utils.refreshCurrentPage();
};
onToggleHelp(e) {
e.preventDefault();
Shortcuts.toggleHelp(this.enabledHelp);
}
Shortcuts.prototype.toggleMarkdownPreview = function(e) {
// Check if short-cut was triggered while in Write Mode
const $target = $(e.target);
const $form = $target.closest('form');
static onTogglePerfBar(e) {
e.preventDefault();
const performanceBarCookieName = 'perf_bar_enabled';
if (Cookies.get(performanceBarCookieName) === 'true') {
Cookies.remove(performanceBarCookieName, { path: '/' });
} else {
Cookies.set(performanceBarCookieName, 'true', { path: '/' });
}
gl.utils.refreshCurrentPage();
}
if ($target.hasClass('js-note-text')) {
$('.js-md-preview-button', $form).focus();
}
return $(document).triggerHandler('markdown-preview:toggle', [e]);
};
static toggleMarkdownPreview(e) {
// Check if short-cut was triggered while in Write Mode
const $target = $(e.target);
const $form = $target.closest('form');
Shortcuts.toggleHelp = function(location) {
var $modal;
$modal = $('#modal-shortcuts');
if ($modal.length) {
$modal.modal('toggle');
return;
}
return $.ajax({
url: gon.shortcuts_path,
dataType: 'script',
success: function(e) {
var i, l, len, results;
if (location && location.length > 0) {
results = [];
for (i = 0, len = location.length; i < len; i += 1) {
l = location[i];
results.push($(l).show());
}
return results;
} else {
$('.hidden-shortcut').show();
return $('.js-more-help-button').remove();
if ($target.hasClass('js-note-text')) {
$('.js-md-preview-button', $form).focus();
}
$(document).triggerHandler('markdown-preview:toggle', [e]);
}
static toggleHelp(location) {
const $modal = $('#modal-shortcuts');
if ($modal.length) {
$modal.modal('toggle');
}
$.ajax({
url: gon.shortcuts_path,
dataType: 'script',
success() {
if (location && location.length > 0) {
const results = [];
for (let i = 0, len = location.length; i < len; i += 1) {
results.push($(location[i]).show());
}
return results;
}
});
};
Shortcuts.prototype.focusFilter = function(e) {
if (this.filterInput == null) {
this.filterInput = $('input[type=search]', '.nav-controls');
}
this.filterInput.focus();
return e.preventDefault();
};
$('.hidden-shortcut').show();
return $('.js-more-help-button').remove();
},
});
}
Shortcuts.focusSearch = function(e) {
$('#search').focus();
return e.preventDefault();
};
focusFilter(e) {
if (!this.filterInput) {
this.filterInput = $('input[type=search]', '.nav-controls');
}
this.filterInput.focus();
e.preventDefault();
}
return Shortcuts;
})();
$(document).on('click.more_help', '.js-more-help-button', function(e) {
$(this).remove();
$('.hidden-shortcut').show();
return e.preventDefault();
});
Mousetrap.stopCallback = (function() {
var defaultStopCallback;
defaultStopCallback = Mousetrap.stopCallback;
return function(e, element, combo) {
// allowed shortcuts if textarea, input, contenteditable are focused
if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) {
return false;
} else {
return defaultStopCallback.apply(this, arguments);
}
};
})();
}).call(window);
static focusSearch(e) {
$('#search').focus();
e.preventDefault();
}
}

View File

@ -1,7 +1,6 @@
/* global Mousetrap */
/* global Shortcuts */
import './shortcuts';
import Shortcuts from './shortcuts';
const defaults = {
skipResetBindings: false,

View File

@ -1,38 +1,30 @@
/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife */
/* global Mousetrap */
/* global ShortcutsNavigation */
import './shortcuts_navigation';
import ShortcutsNavigation from './shortcuts_navigation';
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
export default class ShortcutsFindFile extends ShortcutsNavigation {
constructor(projectFindFile) {
super();
this.ShortcutsFindFile = (function(superClass) {
extend(ShortcutsFindFile, superClass);
const oldStopCallback = Mousetrap.stopCallback;
this.projectFindFile = projectFindFile;
function ShortcutsFindFile(projectFindFile) {
var _oldStopCallback;
this.projectFindFile = projectFindFile;
ShortcutsFindFile.__super__.constructor.call(this);
_oldStopCallback = Mousetrap.stopCallback;
Mousetrap.stopCallback = (function(_this) {
// override to fire shortcuts action when focus in textbox
return function(event, element, combo) {
if (element === _this.projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')) {
// when press up/down key in textbox, cusor prevent to move to home/end
event.preventDefault();
return false;
}
return _oldStopCallback(event, element, combo);
};
})(this);
Mousetrap.bind('up', this.projectFindFile.selectRowUp);
Mousetrap.bind('down', this.projectFindFile.selectRowDown);
Mousetrap.bind('esc', this.projectFindFile.goToTree);
Mousetrap.bind('enter', this.projectFindFile.goToBlob);
}
Mousetrap.stopCallback = (e, element, combo) => {
if (
element === this.projectFindFile.inputElement[0] &&
(combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')
) {
// when press up/down key in textbox, cusor prevent to move to home/end
event.preventDefault();
return false;
}
return ShortcutsFindFile;
})(ShortcutsNavigation);
}).call(window);
return oldStopCallback(e, element, combo);
};
Mousetrap.bind('up', this.projectFindFile.selectRowUp);
Mousetrap.bind('down', this.projectFindFile.selectRowDown);
Mousetrap.bind('esc', this.projectFindFile.goToTree);
Mousetrap.bind('enter', this.projectFindFile.goToBlob);
}
}

View File

@ -1,100 +1,74 @@
/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators */
/* global Mousetrap */
/* global ShortcutsNavigation */
/* global sidebar */
import _ from 'underscore';
import 'mousetrap';
import './shortcuts_navigation';
import ShortcutsNavigation from './shortcuts_navigation';
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
export default class ShortcutsIssuable extends ShortcutsNavigation {
constructor(isMergeRequest) {
super();
this.ShortcutsIssuable = (function(superClass) {
extend(ShortcutsIssuable, superClass);
this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form');
this.editBtn = document.querySelector('.issuable-edit');
function ShortcutsIssuable(isMergeRequest) {
ShortcutsIssuable.__super__.constructor.call(this);
Mousetrap.bind('a', this.openSidebarDropdown.bind(this, 'assignee'));
Mousetrap.bind('m', this.openSidebarDropdown.bind(this, 'milestone'));
Mousetrap.bind('r', (function(_this) {
return function() {
_this.replyWithSelectedText(isMergeRequest);
return false;
};
})(this));
Mousetrap.bind('e', (function(_this) {
return function() {
_this.editIssue();
return false;
};
})(this));
Mousetrap.bind('l', this.openSidebarDropdown.bind(this, 'labels'));
if (isMergeRequest) {
this.enabledHelp.push('.hidden-shortcut.merge_requests');
} else {
this.enabledHelp.push('.hidden-shortcut.issues');
}
Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee'));
Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone'));
Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels'));
Mousetrap.bind('r', this.replyWithSelectedText.bind(this));
Mousetrap.bind('e', this.editIssue.bind(this));
if (isMergeRequest) {
this.enabledHelp.push('.hidden-shortcut.merge_requests');
} else {
this.enabledHelp.push('.hidden-shortcut.issues');
}
}
replyWithSelectedText() {
const documentFragment = window.gl.utils.getSelectedFragment();
if (!documentFragment) {
this.$replyField.focus();
return false;
}
ShortcutsIssuable.prototype.replyWithSelectedText = function(isMergeRequest) {
var quote, documentFragment, el, selected, separator;
let replyField;
const el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
const selected = window.gl.CopyAsGFM.nodeToGFM(el);
if (isMergeRequest) {
replyField = $('.js-main-target-form #note_note');
} else {
replyField = $('.js-main-target-form .js-vue-comment-form');
}
documentFragment = window.gl.utils.getSelectedFragment();
if (!documentFragment) {
replyField.focus();
return;
}
el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
selected = window.gl.CopyAsGFM.nodeToGFM(el);
if (selected.trim() === "") {
return;
}
quote = _.map(selected.split("\n"), function(val) {
return ("> " + val).trim() + "\n";
});
// If replyField already has some content, add a newline before our quote
separator = replyField.val().trim() !== "" && "\n\n" || '';
replyField.val(function(a, current) {
return current + separator + quote.join('') + "\n";
});
// Trigger autosave
replyField.trigger('input').trigger('change');
// Trigger autosize
var event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
replyField.get(0).dispatchEvent(event);
// Focus the input field
return replyField.focus();
};
ShortcutsIssuable.prototype.editIssue = function() {
var $editBtn;
$editBtn = $('.issuable-edit');
// Need to click the element as on issues, editing is inline
// on merge request, editing is on a different page
$editBtn.get(0).click();
};
ShortcutsIssuable.prototype.openSidebarDropdown = function(name) {
sidebar.openDropdown(name);
if (selected.trim() === '') {
return false;
};
}
return ShortcutsIssuable;
})(ShortcutsNavigation);
}).call(window);
const quote = _.map(selected.split('\n'), val => `${(`> ${val}`).trim()}\n`);
// If replyField already has some content, add a newline before our quote
const separator = (this.$replyField.val().trim() !== '' && '\n\n') || '';
this.$replyField.val((a, current) => `${current}${separator}${quote.join('')}\n`)
.trigger('input')
.trigger('change');
// Trigger autosize
const event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
this.$replyField.get(0).dispatchEvent(event);
// Focus the input field
this.$replyField.focus();
return false;
}
editIssue() {
// Need to click the element as on issues, editing is inline
// on merge request, editing is on a different page
this.editBtn.click();
return false;
}
static openSidebarDropdown(name) {
sidebar.openDropdown(name);
return false;
}
}

View File

@ -1,36 +1,27 @@
/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */
/* global Mousetrap */
/* global Shortcuts */
import findAndFollowLink from './shortcuts_dashboard_navigation';
import './shortcuts';
import Shortcuts from './shortcuts';
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
export default class ShortcutsNavigation extends Shortcuts {
constructor() {
super();
this.ShortcutsNavigation = (function(superClass) {
extend(ShortcutsNavigation, superClass);
Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project'));
Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity'));
Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree'));
Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
Mousetrap.bind('g n', () => findAndFollowLink('.shortcuts-network'));
Mousetrap.bind('g d', () => findAndFollowLink('.shortcuts-repository-charts'));
Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues'));
Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards'));
Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests'));
Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki'));
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
function ShortcutsNavigation() {
ShortcutsNavigation.__super__.constructor.call(this);
Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project'));
Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity'));
Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree'));
Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
Mousetrap.bind('g n', () => findAndFollowLink('.shortcuts-network'));
Mousetrap.bind('g d', () => findAndFollowLink('.shortcuts-repository-charts'));
Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues'));
Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards'));
Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests'));
Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki'));
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
this.enabledHelp.push('.hidden-shortcut.project');
}
return ShortcutsNavigation;
})(Shortcuts);
}).call(window);
this.enabledHelp.push('.hidden-shortcut.project');
}
}

View File

@ -1,28 +1,17 @@
/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, max-len */
/* global Mousetrap */
/* global ShortcutsNavigation */
import ShortcutsNavigation from './shortcuts_navigation';
import './shortcuts_navigation';
export default class ShortcutsNetwork extends ShortcutsNavigation {
constructor(graph) {
super();
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
Mousetrap.bind(['left', 'h'], graph.scrollLeft);
Mousetrap.bind(['right', 'l'], graph.scrollRight);
Mousetrap.bind(['up', 'k'], graph.scrollUp);
Mousetrap.bind(['down', 'j'], graph.scrollDown);
Mousetrap.bind(['shift+up', 'shift+k'], graph.scrollTop);
Mousetrap.bind(['shift+down', 'shift+j'], graph.scrollBottom);
this.ShortcutsNetwork = (function(superClass) {
extend(ShortcutsNetwork, superClass);
function ShortcutsNetwork(graph) {
this.graph = graph;
ShortcutsNetwork.__super__.constructor.call(this);
Mousetrap.bind(['left', 'h'], this.graph.scrollLeft);
Mousetrap.bind(['right', 'l'], this.graph.scrollRight);
Mousetrap.bind(['up', 'k'], this.graph.scrollUp);
Mousetrap.bind(['down', 'j'], this.graph.scrollDown);
Mousetrap.bind(['shift+up', 'shift+k'], this.graph.scrollTop);
Mousetrap.bind(['shift+down', 'shift+j'], this.graph.scrollBottom);
this.enabledHelp.push('.hidden-shortcut.network');
}
return ShortcutsNetwork;
})(ShortcutsNavigation);
}).call(window);
this.enabledHelp.push('.hidden-shortcut.network');
}
}

View File

@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
/* global Mousetrap */
/* global ShortcutsNavigation */
import ShortcutsNavigation from './shortcuts_navigation';
import findAndFollowLink from './shortcuts_dashboard_navigation';
export default class ShortcutsWiki extends ShortcutsNavigation {

View File

@ -1,52 +1,64 @@
<script>
import ciIcon from './ci_icon.vue';
/**
* 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 {
props: {
status: {
type: Object,
required: true,
export default {
props: {
status: {
type: Object,
required: true,
},
showText: {
type: Boolean,
required: false,
default: true,
},
},
},
components: {
ciIcon,
},
computed: {
cssClass() {
const className = this.status.group;
return className ? `ci-status ci-${this.status.group}` : 'ci-status';
components: {
ciIcon,
},
},
};
directives: {
tooltip,
},
computed: {
cssClass() {
const className = this.status.group;
return className ? `ci-status ci-${className}` : 'ci-status';
},
},
};
</script>
<template>
<a
:href="status.details_path"
:class="cssClass">
:class="cssClass"
v-tooltip
:title="!showText ? status.text : ''">
<ci-icon :status="status" />
{{status.text}}
<template v-if="showText">
{{status.text}}
</template>
</a>
</template>

View File

@ -63,14 +63,17 @@
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.
*
* TODO: Improve this! Use lodash _.has when we have it.
*
* @returns {Boolean}
*/
hasCommitRef() {
@ -80,8 +83,6 @@
* Used to verify if all the properties needed to render the commit
* author section were provided.
*
* TODO: Improve this! Use lodash _.has when we have it.
*
* @returns {Boolean}
*/
hasAuthor() {
@ -114,31 +115,30 @@
</script>
<template>
<div class="branch-commit">
<div
v-if="hasCommitRef"
class="icon-container hidden-xs">
<i
v-if="tag"
class="fa fa-tag"
aria-hidden="true">
</i>
<i
v-if="!tag"
class="fa fa-code-fork"
aria-hidden="true">
</i>
</div>
<a
v-if="hasCommitRef"
class="ref-name hidden-xs"
:href="commitRef.ref_url"
v-tooltip
data-container="body"
:title="commitRef.name">
{{commitRef.name}}
</a>
<template v-if="hasCommitRef && showBranch">
<div
class="icon-container hidden-xs">
<i
v-if="tag"
class="fa fa-tag"
aria-hidden="true">
</i>
<i
v-if="!tag"
class="fa fa-code-fork"
aria-hidden="true">
</i>
</div>
<a
class="ref-name hidden-xs"
:href="commitRef.ref_url"
v-tooltip
data-container="body"
:title="commitRef.name">
{{commitRef.name}}
</a>
</template>
<div
v-html="commitIconSvg"
class="commit-icon js-commit-icon">

View File

@ -0,0 +1,71 @@
<script>
/* This is a re-usable vue component for rendering a button
that will probably be sending off ajax requests and need
to show the loading status by setting the `loading` option.
This can also be used for initial page load when you don't
know the action of the button yet by setting
`loading: true, label: undefined`.
Sample configuration:
<loading-button
:loading="true"
:label="Hello"
@click="..."
/>
*/
import loadingIcon from './loading_icon.vue';
export default {
props: {
loading: {
type: Boolean,
required: false,
default: false,
},
label: {
type: String,
required: false,
},
},
components: {
loadingIcon,
},
methods: {
onClick(e) {
this.$emit('click', e);
},
},
};
</script>
<template>
<button
class="btn btn-align-content"
@click="onClick"
type="button"
:disabled="loading"
>
<transition name="fade">
<loading-icon
v-if="loading"
:inline="true"
class="js-loading-button-icon"
:class="{
'append-right-5': label
}"
/>
</transition>
<transition name="fade">
<span
v-if="label"
class="js-loading-button-label"
>
{{ label }}
</span>
</transition>
</button>
</template>

View File

@ -16,6 +16,11 @@ export default {
required: false,
default: 'primary',
},
closeKind: {
type: String,
required: false,
default: 'default',
},
closeButtonLabel: {
type: String,
required: false,
@ -33,6 +38,11 @@ export default {
[`btn-${this.kind}`]: true,
};
},
btnCancelKindClass() {
return {
[`btn-${this.closeKind}`]: true,
};
},
},
methods: {
@ -70,7 +80,8 @@ export default {
<div class="modal-footer">
<button
type="button"
class="btn btn-default"
class="btn"
:class="btnCancelKindClass"
@click="emitSubmit(false)">
{{closeButtonLabel}}
</button>

View File

@ -7,6 +7,7 @@
Sample configuration:
<user-avatar-image
:lazy="true"
:img-src="userAvatarSrc"
:img-alt="tooltipText"
:tooltip-text="tooltipText"
@ -16,11 +17,17 @@
*/
import defaultAvatarUrl from 'images/no_avatar.png';
import { placeholderImage } from '../../../lazy_loader';
import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarImage',
props: {
lazy: {
type: Boolean,
required: false,
default: false,
},
imgSrc: {
type: String,
required: false,
@ -56,18 +63,21 @@ export default {
tooltip,
},
computed: {
// API response sends null when gravatar is disabled and
// we provide an empty string when we use it inside user avatar link.
// In both cases we should render the defaultAvatarUrl
sanitizedSource() {
return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
},
resultantSrcAttribute() {
return this.lazy ? placeholderImage : this.sanitizedSource;
},
tooltipContainer() {
return this.tooltipText ? 'body' : null;
},
avatarSizeClass() {
return `s${this.size}`;
},
// API response sends null when gravatar is disabled and
// we provide an empty string when we use it inside user avatar link.
// In both cases we should render the defaultAvatarUrl
imageSource() {
return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
},
},
};
</script>
@ -76,11 +86,16 @@ export default {
<img
v-tooltip
class="avatar"
:class="[avatarSizeClass, cssClasses]"
:src="imageSource"
:class="{
lazy,
[avatarSizeClass]: true,
[cssClasses]: true
}"
:src="resultantSrcAttribute"
:width="size"
:height="size"
:alt="imgAlt"
:data-src="sanitizedSource"
:data-container="tooltipContainer"
:data-placement="tooltipPlacement"
:title="tooltipText"

View File

@ -11,8 +11,6 @@ import Dropzone from 'dropzone';
import 'mousetrap';
import 'mousetrap/plugins/pause/mousetrap-pause';
window.Dropzone = Dropzone;
//
// ### Events
//

View File

@ -5,8 +5,10 @@
@import "framework/layout";
@import "framework/animations";
@import "framework/vue_transitions";
@import "framework/avatar";
@import "framework/asciidoctor";
@import "framework/banner";
@import "framework/blocks";
@import "framework/buttons";
@import "framework/badges";

View File

@ -23,6 +23,11 @@
@include webkit-prefix(animation-duration, 2s);
}
&.spin {
transform-origin: center;
animation: spin 4s linear infinite;
}
&.flipOutX,
&.flipOutY,
&.bounceIn,
@ -198,6 +203,13 @@ a {
height: 12px;
}
&.animation-container-right {
.skeleton-line-2 {
left: 0;
right: 150px;
}
}
&::before {
animation-duration: 1s;
animation-fill-mode: forwards;
@ -264,3 +276,9 @@ a {
transform: translateX(468px);
}
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}

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