Merge branch 'refactor-clusters' into 38464-k8s-apps

This commit is contained in:
Kamil Trzcinski 2017-11-02 14:01:30 +01:00
commit 461c385ebc
1207 changed files with 31633 additions and 13415 deletions

View File

@ -1,7 +1,7 @@
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-phantomjs-2.1-node-8.x-yarn-1.0-postgresql-9.6"
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.13-phantomjs-2.1-node-8.x-yarn-1.0-postgresql-9.6"
.default-cache: &default-cache
key: "ruby-233-with-yarn"
key: "ruby-235-with-yarn"
paths:
- vendor/ruby
- .yarn-cache/
@ -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
@ -467,7 +455,7 @@ db:migrate:reset-mysql:
variables:
SETUP_DB: "false"
script:
- git fetch origin v8.14.10
- git fetch origin v9.3.0
- git checkout -f FETCH_HEAD
- bundle install $BUNDLE_INSTALL_FLAGS
- cp config/gitlab.yml.example config/gitlab.yml
@ -563,7 +551,7 @@ karma:
<<: *dedicated-runner
<<: *except-docs
<<: *pull-cache
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-chrome-61.0-node-8.x-yarn-1.0-postgresql-9.6"
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.13-chrome-61.0-node-8.x-yarn-1.0-postgresql-9.6"
stage: test
variables:
BABEL_ENV: "coverage"

3
.gitlab/route-map.yml Normal file
View File

@ -0,0 +1,3 @@
# Documentation
- source: /doc/(.+?)\.md/ # doc/administration/build_artifacts.md
public: '\1.html' # doc/administration/build_artifacts.html

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

@ -1 +1 @@
2.3.3
2.3.5

View File

@ -121,7 +121,8 @@ linters:
# Avoid nesting selectors too deeply.
NestingDepth:
enabled: false
enabled: true
max_depth: 6
# Always use placeholder selectors in @extend.
PlaceholderInExtend:

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

@ -21,10 +21,10 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Workflow labels](#workflow-labels)
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-ci-discussion-edge-platform-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
- [Priority labels (~Deliverable and ~Stretch)](#priority-labels-deliverable-and-stretch)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design--ui-elements)
- [Implement design & UI elements](#implement-design-ui-elements)
- [Issue tracker](#issue-tracker)
- [Issue triaging](#issue-triaging)
- [Feature proposals](#feature-proposals)

View File

@ -1 +1 @@
0.47.0
0.51.0

View File

@ -90,7 +90,7 @@ gem 'kaminari', '~> 1.0'
gem 'hamlit', '~> 2.6.1'
# Files attachments
gem 'carrierwave', '~> 1.1'
gem 'carrierwave', '~> 1.2'
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
@ -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.beta18'
gem 'raindrops', '~> 0.18'
end
@ -398,7 +398,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.42.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.51.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false

View File

@ -107,7 +107,7 @@ GEM
capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
carrierwave (1.1.0)
carrierwave (1.2.1)
activemodel (>= 4.0.0)
activesupport (>= 4.0.0)
mime-types (>= 1.16)
@ -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.42.0)
gitaly-proto (0.51.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@ -623,7 +623,7 @@ GEM
parser
unparser
procto (0.0.3)
prometheus-client-mmap (0.7.0.beta14)
prometheus-client-mmap (0.7.0.beta18)
mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4)
coderay (~> 1.1.0)
@ -990,7 +990,7 @@ DEPENDENCIES
bundler-audit (~> 0.5.0)
capybara (~> 2.15.0)
capybara-screenshot (~> 1.0.0)
carrierwave (~> 1.1)
carrierwave (~> 1.2)
charlock_holmes (~> 0.7.5)
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
@ -1015,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)
@ -1030,7 +1030,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.42.0)
gitaly-proto (~> 0.51.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
@ -1106,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.beta18)
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

@ -16,6 +16,7 @@ const Api = {
usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath)

View File

@ -1,8 +1,9 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */
/* eslint-disable no-param-reassign, prefer-template, no-var, no-void, consistent-return */
import AccessorUtilities from './lib/utils/accessor';
window.Autosave = (function() {
function Autosave(field, key, resource) {
export default class Autosave {
constructor(field, key, resource) {
this.field = field;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.resource = resource;
@ -12,14 +13,10 @@ window.Autosave = (function() {
this.key = 'autosave/' + key;
this.field.data('autosave', this);
this.restore();
this.field.on('input', (function(_this) {
return function() {
return _this.save();
};
})(this));
this.field.on('input', () => this.save());
}
Autosave.prototype.restore = function() {
restore() {
var text;
if (!this.isLocalStorageAvailable) return;
@ -40,9 +37,9 @@ window.Autosave = (function() {
field.dispatchEvent(event);
}
}
};
}
Autosave.prototype.save = function() {
save() {
var text;
text = this.field.val();
@ -51,15 +48,11 @@ window.Autosave = (function() {
}
return this.reset();
};
}
Autosave.prototype.reset = function() {
reset() {
if (!this.isLocalStorageAvailable) return;
return window.localStorage.removeItem(this.key);
};
return Autosave;
})();
export default window.Autosave;
}
}

View File

@ -1,8 +1,8 @@
import autosize from 'vendor/autosize';
import Autosize from 'autosize';
document.addEventListener('DOMContentLoaded', () => {
const autosizeEls = document.querySelectorAll('.js-autosize');
autosize(autosizeEls);
autosize.update(autosizeEls);
Autosize(autosizeEls);
Autosize.update(autosizeEls);
});

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

@ -1,5 +1,4 @@
/* eslint-disable comma-dangle, space-before-function-paren, no-new */
/* global IssuableContext */
/* global MilestoneSelect */
/* global LabelsSelect */
/* global Sidebar */
@ -9,7 +8,9 @@ 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';
import IssuableContext from '../../issuable_context';
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

@ -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 = {
@ -61,19 +64,16 @@ export default class Clusters {
this.poll = new Poll({
resource: this.service,
method: 'fetchData',
successCallback: (data) => {
const { status, status_reason } = data.data;
this.updateContainer(status, status_reason);
},
errorCallback: () => {
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
},
successCallback: data => this.handleSuccess(data),
errorCallback: () => Clusters.handleError(),
});
if (!Visibility.hidden()) {
this.poll.makeRequest();
} else {
this.service.fetchData();
this.service.fetchData()
.then(data => this.handleSuccess(data))
.catch(() => Clusters.handleError());
}
Visibility.change(() => {
@ -85,6 +85,15 @@ export default class Clusters {
});
}
static handleError() {
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
}
handleSuccess(data) {
const { status, status_reason } = data.data;
this.updateContainer(status, status_reason);
}
hideAll() {
this.errorContainer.classList.add('hidden');
this.successContainer.classList.add('hidden');

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

@ -2,7 +2,7 @@ import Cookies from 'js-cookie';
import _ from 'underscore';
import bp from './breakpoints';
export default class NewNavSidebar {
export default class ContextualSidebar {
constructor() {
this.initDomElements();
this.render();
@ -55,7 +55,7 @@ export default class NewNavSidebar {
this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
}
NewNavSidebar.setCollapsedCookie(collapsed);
ContextualSidebar.setCollapsedCookie(collapsed);
this.toggleSidebarOverflow();
}

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,18 +1,19 @@
/* 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 IssuableIndex */
import IssuableIndex from './issuable_index';
/* global Milestone */
/* global IssuableForm */
import IssuableForm from './issuable_form';
/* global LabelsSelect */
/* global MilestoneSelect */
/* 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';
/* global GroupsSelect */
import groupsSelect from './groups_select';
/* global Search */
/* global Admin */
/* global NamespaceSelects */
@ -73,6 +74,7 @@ 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';
@ -85,6 +87,8 @@ 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;
@ -168,11 +172,8 @@ import memberExpirationDate from './member_expiration_date';
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);
new IssuableIndex(pagePrefix);
shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
@ -230,16 +231,21 @@ import memberExpirationDate from './member_expiration_date';
case 'projects:milestones:new':
case 'projects:milestones:edit':
case 'projects:milestones:update':
new ZenMode();
new DueDateSelectors();
new GLForm($('.milestone-form'), true);
break;
case 'groups:milestones:new':
case 'groups:milestones:edit':
case 'groups:milestones:update':
new ZenMode();
new gl.DueDateSelectors();
new GLForm($('.milestone-form'), true);
new DueDateSelectors();
new GLForm($('.milestone-form'), false);
break;
case 'projects:compare:show':
new gl.Diff();
initChangesDropdown();
new Diff();
const paddingTop = 16;
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
break;
case 'projects:branches:new':
case 'projects:branches:create':
@ -274,7 +280,7 @@ import memberExpirationDate from './member_expiration_date';
}
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'));
@ -308,7 +314,7 @@ import memberExpirationDate from './member_expiration_date';
new GLForm($('.release-form'), true);
break;
case 'projects:merge_requests:show':
new gl.Diff();
new Diff();
shortcut_handler = new ShortcutsIssuable(true);
new ZenMode();
@ -324,7 +330,7 @@ import memberExpirationDate from './member_expiration_date';
new gl.Activities();
break;
case 'projects:commit:show':
new gl.Diff();
new Diff();
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
new MiniPipelineGraph({
@ -352,7 +358,10 @@ import memberExpirationDate from './member_expiration_date';
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();
@ -372,9 +381,6 @@ import memberExpirationDate from './member_expiration_date';
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':
@ -395,10 +401,15 @@ import memberExpirationDate from './member_expiration_date';
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':
memberExpirationDate();
@ -407,7 +418,7 @@ import memberExpirationDate from './member_expiration_date';
break;
case 'projects:project_members:index':
memberExpirationDate('.js-access-expiration-date-groups');
new GroupsSelect();
groupsSelect();
memberExpirationDate();
new Members();
new UsersSelect();
@ -418,11 +429,11 @@ import memberExpirationDate from './member_expiration_date';
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();
@ -432,7 +443,6 @@ import memberExpirationDate from './member_expiration_date';
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);
});
@ -470,7 +480,7 @@ import memberExpirationDate from './member_expiration_date';
const $el = $(el);
if ($el.find('.dropdown-group-label').length) {
new gl.GroupLabelSubscription($el);
new GroupLabelSubscription($el);
} else {
new gl.ProjectLabelSubscription($el);
}
@ -530,7 +540,7 @@ import memberExpirationDate from './member_expiration_date';
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')

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

@ -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

@ -1,121 +1,86 @@
/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var,
camelcase, one-var-declaration-per-line, quotes, object-shorthand,
prefer-arrow-callback, comma-dangle, consistent-return, yoda,
prefer-rest-params, prefer-spread, no-unused-vars, prefer-template,
promise/catch-or-return */
import Api from './api';
import { normalizeCRLFHeaders } from './lib/utils/common_utils';
var slice = [].slice;
export default function groupsSelect() {
// Needs to be accessible in rspec
window.GROUP_SELECT_PER_PAGE = 20;
$('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
const $select = $(this);
const allAvailable = $select.data('all-available');
const skipGroups = $select.data('skip-groups') || [];
$select.select2({
placeholder: 'Search for a group',
multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
ajax: {
url: Api.buildUrl(Api.groupsPath),
dataType: 'json',
quietMillis: 250,
transport(params) {
return $.ajax(params)
.then((data, status, xhr) => {
const results = data || [];
window.GroupsSelect = (function() {
function GroupsSelect() {
$('.ajax-groups-select').each((function(_this) {
const self = _this;
return function(i, select) {
var all_available, skip_groups;
const $select = $(select);
all_available = $select.data('all-available');
skip_groups = $select.data('skip-groups') || [];
$select.select2({
placeholder: "Search for a group",
multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
ajax: {
url: Api.buildUrl(Api.groupsPath),
dataType: 'json',
quietMillis: 250,
transport: function (params) {
$.ajax(params).then((data, status, xhr) => {
const results = data || [];
const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders());
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const more = currentPage < totalPages;
return {
results,
pagination: {
more,
},
};
}).then(params.success).fail(params.error);
},
data: function (search, page) {
return {
search,
page,
per_page: GroupsSelect.PER_PAGE,
all_available,
};
},
results: function (data, page) {
if (data.length) return { results: [] };
const groups = data.length ? data : data.results || [];
const more = data.pagination ? data.pagination.more : false;
const results = groups.filter(group => skip_groups.indexOf(group.id) === -1);
const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders());
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const more = currentPage < totalPages;
return {
results,
page,
more,
pagination: {
more,
},
};
},
},
initSelection: function(element, callback) {
var id;
id = $(element).val();
if (id !== "") {
return Api.group(id, callback);
}
},
formatResult: function() {
var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
return self.formatResult.apply(self, args);
},
formatSelection: function() {
var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
return self.formatSelection.apply(self, args);
},
dropdownCssClass: "ajax-groups-dropdown select2-infinite",
// we do not want to escape markup since we are displaying html in results
escapeMarkup: function(m) {
return m;
}
});
})
.then(params.success)
.fail(params.error);
},
data(search, page) {
return {
search,
page,
per_page: window.GROUP_SELECT_PER_PAGE,
all_available: allAvailable,
};
},
results(data, page) {
if (data.length) return { results: [] };
self.dropdown = document.querySelector('.select2-infinite .select2-results');
const groups = data.length ? data : data.results || [];
const more = data.pagination ? data.pagination.more : false;
const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
$select.on('select2-loaded', self.forceOverflow.bind(self));
};
})(this));
}
return {
results,
page,
more,
};
},
},
// eslint-disable-next-line consistent-return
initSelection(element, callback) {
const id = $(element).val();
if (id !== '') {
return Api.group(id, callback);
}
},
formatResult(object) {
return `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`;
},
formatSelection(object) {
return object.full_name;
},
dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
// we do not want to escape markup since we are displaying html in results
escapeMarkup(m) {
return m;
},
});
GroupsSelect.prototype.formatResult = function(group) {
var avatar;
if (group.avatar_url) {
avatar = group.avatar_url;
} else {
avatar = gon.default_avatar_url;
}
return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>";
};
GroupsSelect.prototype.formatSelection = function(group) {
return group.full_name;
};
GroupsSelect.prototype.forceOverflow = function (e) {
this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight)}px`;
};
GroupsSelect.PER_PAGE = 20;
return GroupsSelect;
})();
$select.on('select2-loaded', () => {
const dropdown = document.querySelector('.select2-infinite .select2-results');
dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
});
});
}

View File

@ -7,10 +7,12 @@ import { highCountTrim } from '~/lib/utils/text_utility';
* @param {jQuery.Event} e
* @param {String} count
*/
$(document).on('todo:toggle', (e, count) => {
const parsedCount = parseInt(count, 10);
const $todoPendingCount = $('.todos-count');
export default function initTodoToggle() {
$(document).on('todo:toggle', (e, count) => {
const parsedCount = parseInt(count, 10);
const $todoPendingCount = $('.todos-count');
$todoPendingCount.text(highCountTrim(parsedCount));
$todoPendingCount.toggleClass('hidden', parsedCount === 0);
});
$todoPendingCount.text(highCountTrim(parsedCount));
$todoPendingCount.toggleClass('hidden', parsedCount === 0);
});
}

View File

@ -1,83 +1,81 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, vars-on-top, no-new, max-len */
class ImporterStatus {
constructor(jobsUrl, importUrl) {
this.jobsUrl = jobsUrl;
this.importUrl = importUrl;
this.initStatusPage();
this.setAutoUpdate();
}
(function() {
window.ImporterStatus = (function() {
function ImporterStatus(jobs_url, import_url) {
this.jobs_url = jobs_url;
this.import_url = import_url;
this.initStatusPage();
this.setAutoUpdate();
}
ImporterStatus.prototype.initStatusPage = function() {
$('.js-add-to-import').off('click').on('click', (function(_this) {
return function(e) {
var $btn, $namespace_input, $target_field, $tr, id, target_namespace, newName;
$btn = $(e.currentTarget);
$tr = $btn.closest('tr');
$target_field = $tr.find('.import-target');
$namespace_input = $target_field.find('.js-select-namespace option:selected');
id = $tr.attr('id').replace('repo_', '');
target_namespace = null;
newName = null;
if ($namespace_input.length > 0) {
target_namespace = $namespace_input[0].innerHTML;
newName = $target_field.find('#path').prop('value');
$target_field.empty().append(target_namespace + "/" + newName);
}
$btn.disable().addClass('is-loading');
return $.post(_this.import_url, {
repo_id: id,
target_namespace: target_namespace,
new_name: newName
}, {
dataType: 'script'
});
};
})(this));
return $('.js-import-all').off('click').on('click', function(e) {
var $btn;
$btn = $(this);
initStatusPage() {
$('.js-add-to-import')
.off('click')
.on('click', (event) => {
const $btn = $(event.currentTarget);
const $tr = $btn.closest('tr');
const $targetField = $tr.find('.import-target');
const $namespaceInput = $targetField.find('.js-select-namespace option:selected');
const id = $tr.attr('id').replace('repo_', '');
let targetNamespace;
let newName;
if ($namespaceInput.length > 0) {
targetNamespace = $namespaceInput[0].innerHTML;
newName = $targetField.find('#path').prop('value');
$targetField.empty().append(`${targetNamespace}/${newName}`);
}
$btn.disable().addClass('is-loading');
return $('.js-add-to-import').each(function() {
return $.post(this.importUrl, {
repo_id: id,
target_namespace: targetNamespace,
new_name: newName,
}, {
dataType: 'script',
});
});
$('.js-import-all')
.off('click')
.on('click', function onClickImportAll() {
const $btn = $(this);
$btn.disable().addClass('is-loading');
return $('.js-add-to-import').each(function triggerAddImport() {
return $(this).trigger('click');
});
});
};
}
ImporterStatus.prototype.setAutoUpdate = function() {
return setInterval(((function(_this) {
return function() {
return $.get(_this.jobs_url, function(data) {
return $.each(data, function(i, job) {
var job_item, status_field;
job_item = $("#project_" + job.id);
status_field = job_item.find(".job-status");
if (job.import_status === 'finished') {
job_item.removeClass("active").addClass("success");
return status_field.html('<span><i class="fa fa-check"></i> done</span>');
} else if (job.import_status === 'scheduled') {
return status_field.html("<i class='fa fa-spinner fa-spin'></i> scheduled");
} else if (job.import_status === 'started') {
return status_field.html("<i class='fa fa-spinner fa-spin'></i> started");
} else {
return status_field.html(job.import_status);
}
});
});
};
})(this)), 4000);
};
setAutoUpdate() {
return setInterval(() => $.get(this.jobsUrl, data => $.each(data, (i, job) => {
const jobItem = $(`#project_${job.id}`);
const statusField = jobItem.find('.job-status');
return ImporterStatus;
})();
const spinner = '<i class="fa fa-spinner fa-spin"></i>';
$(function() {
if ($('.js-importer-status').length) {
var jobsImportPath = $('.js-importer-status').data('jobs-import-path');
var importPath = $('.js-importer-status').data('import-path');
switch (job.import_status) {
case 'finished':
jobItem.removeClass('active').addClass('success');
statusField.html('<span><i class="fa fa-check"></i> done</span>');
break;
case 'scheduled':
statusField.html(`${spinner} scheduled`);
break;
case 'started':
statusField.html(`${spinner} started`);
break;
default:
statusField.html(job.import_status);
break;
}
})), 4000);
}
}
new window.ImporterStatus(jobsImportPath, importPath);
}
});
}).call(window);
// eslint-disable-next-line consistent-return
export default function initImporterStatus() {
const importerStatus = document.querySelector('.js-importer-status');
if (importerStatus) {
const data = importerStatus.dataset;
return new ImporterStatus(data.jobsImportPath, data.importPath);
}
}

View File

@ -1,7 +1,7 @@
import stickyMonitor from './lib/utils/sticky';
export default () => {
stickyMonitor(document.querySelector('.js-diff-files-changed'));
export default (stickyTop) => {
stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
$('.js-diff-stats-dropdown').glDropdown({
filterable: true,

View File

@ -1,9 +1,11 @@
/* eslint-disable no-new */
/* global MilestoneSelect */
/* global LabelsSelect */
/* global IssuableContext */
import IssuableContext from './issuable_context';
/* 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

@ -1,15 +1,15 @@
/* eslint-disable no-new */
/* global LabelsSelect */
/* global MilestoneSelect */
/* global IssueStatusSelect */
/* global SubscriptionSelect */
import UsersSelect from './users_select';
import issueStatusSelect from './issue_status_select';
export default () => {
new UsersSelect();
new LabelsSelect();
new MilestoneSelect();
new IssueStatusSelect();
issueStatusSelect();
new SubscriptionSelect();
};

View File

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

View File

@ -1,10 +1,13 @@
/* eslint-disable class-methods-use-this, no-new */
/* global LabelsSelect */
/* global MilestoneSelect */
/* global IssueStatusSelect */
/* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import './milestone_select';
import issueStatusSelect from './issue_status_select';
import './subscription_select';
import './labels_select';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
@ -45,7 +48,7 @@ export default class IssuableBulkUpdateSidebar {
initDropdowns() {
new LabelsSelect();
new MilestoneSelect();
new IssueStatusSelect();
issueStatusSelect();
new SubscriptionSelect();
}

View File

@ -1,33 +1,32 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */
import Cookies from 'js-cookie';
import bp from './breakpoints';
import UsersSelect from './users_select';
const PARTICIPANTS_ROW_COUNT = 7;
export default class IssuableContext {
constructor(currentUser) {
this.userSelect = new UsersSelect(currentUser);
(function() {
this.IssuableContext = (function() {
function IssuableContext(currentUser) {
this.initParticipants();
new UsersSelect(currentUser);
$('select.select2').select2({
width: 'resolve',
dropdownAutoWidth: true
});
$(".issuable-sidebar .inline-update").on("change", "select", function() {
return $(this).submit();
});
$(".issuable-sidebar .inline-update").on("change", ".js-assignee", function() {
return $(this).submit();
});
$(document).off('click', '.issuable-sidebar .dropdown-content a').on('click', '.issuable-sidebar .dropdown-content a', function(e) {
return e.preventDefault();
});
$(document).off('click', '.edit-link').on('click', '.edit-link', function(e) {
var $block, $selectbox;
$('select.select2').select2({
width: 'resolve',
dropdownAutoWidth: true,
});
$('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() {
return $(this).submit();
});
$('.issuable-sidebar .inline-update').on('change', '.js-assignee', function onClickAssignee() {
return $(this).submit();
});
$(document)
.off('click', '.issuable-sidebar .dropdown-content a')
.on('click', '.issuable-sidebar .dropdown-content a', e => e.preventDefault());
$(document)
.off('click', '.edit-link')
.on('click', '.edit-link', function onClickEdit(e) {
e.preventDefault();
$block = $(this).parents('.block');
$selectbox = $block.find('.selectbox');
const $block = $(this).parents('.block');
const $selectbox = $block.find('.selectbox');
if ($selectbox.is(':visible')) {
$selectbox.hide();
$block.find('.value').show();
@ -35,47 +34,18 @@ const PARTICIPANTS_ROW_COUNT = 7;
$selectbox.show();
$block.find('.value').hide();
}
if ($selectbox.is(':visible')) {
return setTimeout(function() {
return $block.find('.dropdown-menu-toggle').trigger('click');
}, 0);
setTimeout(() => $block.find('.dropdown-menu-toggle').trigger('click'), 0);
}
});
window.addEventListener('beforeunload', function() {
// collapsed_gutter cookie hides the sidebar
var bpBreakpoint = bp.getBreakpointSize();
if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') {
Cookies.set('collapsed_gutter', true);
}
});
}
IssuableContext.prototype.initParticipants = function() {
$(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();
}
});
};
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");
if (currentText === originalText) {
$(this).text(lessText);
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
} else {
$(this).text(originalText);
window.addEventListener('beforeunload', () => {
// collapsed_gutter cookie hides the sidebar
const bpBreakpoint = bp.getBreakpointSize();
if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') {
Cookies.set('collapsed_gutter', true);
}
$(".js-participants-hidden").toggle();
};
return IssuableContext;
})();
}).call(window);
});
}
}

View File

@ -1,108 +1,107 @@
/* 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 */
/* eslint-disable func-names, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
/* global GitLab */
/* global Autosave */
/* 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() {
IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
export default class IssuableForm {
constructor(form) {
this.form = form;
this.toggleWip = this.toggleWip.bind(this);
this.renderWipExplanation = this.renderWipExplanation.bind(this);
this.resetAutosave = this.resetAutosave.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
function IssuableForm(form) {
var $issuableDueDate, calendar;
this.form = form;
this.toggleWip = this.toggleWip.bind(this);
this.renderWipExplanation = this.renderWipExplanation.bind(this);
this.resetAutosave = this.resetAutosave.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
new UsersSelect();
new ZenMode();
this.titleField = this.form.find("input[name*='[title]']");
this.descriptionField = this.form.find("textarea[name*='[description]']");
if (!(this.titleField.length && this.descriptionField.length)) {
return;
}
this.initAutosave();
this.form.on("submit", this.handleSubmit);
this.form.on("click", ".btn-cancel", this.resetAutosave);
this.initWip();
$issuableDueDate = $('#issuable-due-date');
if ($issuableDueDate.length) {
calendar = new Pikaday({
field: $issuableDueDate.get(0),
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
container: $issuableDueDate.parent().get(0),
onSelect: function(dateText) {
$issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
});
calendar.setDate(new Date($issuableDueDate.val()));
}
new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
new UsersSelect();
new ZenMode();
this.titleField = this.form.find('input[name*="[title]"]');
this.descriptionField = this.form.find('textarea[name*="[description]"]');
if (!(this.titleField.length && this.descriptionField.length)) {
return;
}
IssuableForm.prototype.initAutosave = function() {
new Autosave(this.titleField, [document.location.pathname, document.location.search, "title"]);
return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, "description"]);
};
this.initAutosave();
this.form.on('submit', this.handleSubmit);
this.form.on('click', '.btn-cancel', this.resetAutosave);
this.initWip();
IssuableForm.prototype.handleSubmit = function() {
return this.resetAutosave();
};
const $issuableDueDate = $('#issuable-due-date');
IssuableForm.prototype.resetAutosave = function() {
this.titleField.data("autosave").reset();
return this.descriptionField.data("autosave").reset();
};
if ($issuableDueDate.length) {
const calendar = new Pikaday({
field: $issuableDueDate.get(0),
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
container: $issuableDueDate.parent().get(0),
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect: dateText => $issuableDueDate.val(calendar.toString(dateText)),
});
calendar.setDate(parsePikadayDate($issuableDueDate.val()));
}
}
IssuableForm.prototype.initWip = function() {
this.$wipExplanation = this.form.find(".js-wip-explanation");
this.$noWipExplanation = this.form.find(".js-no-wip-explanation");
if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) {
return;
}
this.form.on("click", ".js-toggle-wip", this.toggleWip);
this.titleField.on("keyup blur", this.renderWipExplanation);
return this.renderWipExplanation();
};
initAutosave() {
new Autosave(this.titleField, [document.location.pathname, document.location.search, 'title']);
return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, 'description']);
}
IssuableForm.prototype.workInProgress = function() {
return this.wipRegex.test(this.titleField.val());
};
handleSubmit() {
return this.resetAutosave();
}
IssuableForm.prototype.renderWipExplanation = function() {
if (this.workInProgress()) {
this.$wipExplanation.show();
return this.$noWipExplanation.hide();
} else {
this.$wipExplanation.hide();
return this.$noWipExplanation.show();
}
};
resetAutosave() {
this.titleField.data('autosave').reset();
return this.descriptionField.data('autosave').reset();
}
IssuableForm.prototype.toggleWip = function(event) {
event.preventDefault();
if (this.workInProgress()) {
this.removeWip();
} else {
this.addWip();
}
return this.renderWipExplanation();
};
initWip() {
this.$wipExplanation = this.form.find('.js-wip-explanation');
this.$noWipExplanation = this.form.find('.js-no-wip-explanation');
if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) {
return;
}
this.form.on('click', '.js-toggle-wip', this.toggleWip);
this.titleField.on('keyup blur', this.renderWipExplanation);
return this.renderWipExplanation();
}
IssuableForm.prototype.removeWip = function() {
return this.titleField.val(this.titleField.val().replace(this.wipRegex, ""));
};
workInProgress() {
return this.wipRegex.test(this.titleField.val());
}
IssuableForm.prototype.addWip = function() {
return this.titleField.val("WIP: " + (this.titleField.val()));
};
renderWipExplanation() {
if (this.workInProgress()) {
this.$wipExplanation.show();
return this.$noWipExplanation.hide();
} else {
this.$wipExplanation.hide();
return this.$noWipExplanation.show();
}
}
return IssuableForm;
})();
}).call(window);
toggleWip(event) {
event.preventDefault();
if (this.workInProgress()) {
this.removeWip();
} else {
this.addWip();
}
return this.renderWipExplanation();
}
removeWip() {
return this.titleField.val(this.titleField.val().replace(this.wipRegex, ''));
}
addWip() {
this.titleField.val(`WIP: ${(this.titleField.val())}`);
}
}

View File

@ -1,171 +1,42 @@
/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */
/* global IssuableIndex */
import _ from 'underscore';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
((global) => {
var issuable_created;
export default class IssuableIndex {
constructor(pagePrefix) {
this.initBulkUpdate(pagePrefix);
IssuableIndex.resetIncomingEmailToken();
}
initBulkUpdate(pagePrefix) {
const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
const alreadyInitialized = !!this.bulkUpdateSidebar;
issuable_created = false;
global.IssuableIndex = {
init: function(pagePrefix) {
IssuableIndex.initTemplates();
IssuableIndex.initSearch();
IssuableIndex.initBulkUpdate(pagePrefix);
IssuableIndex.initResetFilters();
IssuableIndex.resetIncomingEmailToken();
IssuableIndex.initLabelFilterRemove();
},
initTemplates: function() {
return IssuableIndex.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
},
initSearch: function() {
const $searchInput = $('#issuable_search');
IssuableIndex.initSearchState($searchInput);
// `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
const debouncedExecSearch = _.debounce(IssuableIndex.executeSearch, 1000, false);
$searchInput.off('keyup').on('keyup', debouncedExecSearch);
// ensures existing filters are preserved when manually submitted
$('#issuable_search_form').on('submit', (e) => {
e.preventDefault();
debouncedExecSearch(e);
if (userCanBulkUpdate && !alreadyInitialized) {
IssuableBulkUpdateActions.init({
prefixId: pagePrefix,
});
},
initSearchState: function($searchInput) {
const currentSearchVal = $searchInput.val();
IssuableIndex.searchState = {
elem: $searchInput,
current: currentSearchVal
};
IssuableIndex.maybeFocusOnSearch();
},
accessSearchPristine: function(set) {
// store reference to previous value to prevent search on non-mutating keyup
const state = IssuableIndex.searchState;
const currentSearchVal = state.elem.val();
if (set) {
state.current = currentSearchVal;
} else {
return state.current === currentSearchVal;
}
},
maybeFocusOnSearch: function() {
const currentSearchVal = IssuableIndex.searchState.current;
if (currentSearchVal && currentSearchVal !== '') {
const queryLength = currentSearchVal.length;
const $searchInput = IssuableIndex.searchState.elem;
/* The following ensures that the cursor is initially placed at
* the end of search input when focus is applied. It accounts
* for differences in browser implementations of `setSelectionRange`
* and cursor placement for elements in focus.
*/
$searchInput.focus();
if ($searchInput.setSelectionRange) {
$searchInput.setSelectionRange(queryLength, queryLength);
} else {
$searchInput.val(currentSearchVal);
}
}
},
executeSearch: function(e) {
const $search = $('#issuable_search');
const $searchName = $search.attr('name');
const $searchValue = $search.val();
const $filtersForm = $('.js-filter-form');
const $input = $(`input[name='${$searchName}']`, $filtersForm);
const isPristine = IssuableIndex.accessSearchPristine();
if (isPristine) {
return;
}
if (!$input.length) {
$filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`);
} else {
$input.val($searchValue);
}
IssuableIndex.filterResults($filtersForm);
},
initLabelFilterRemove: function() {
return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
var $button;
$button = $(this);
// Remove the label input box
$('input[name="label_name[]"]').filter(function() {
return this.value === $button.data('label');
}).remove();
// Submit the form to get new data
IssuableIndex.filterResults($('.filter-form'));
});
},
filterResults: (function(_this) {
return function(form) {
var formAction, formData, issuesUrl;
formData = form.serializeArray();
formData = formData.filter(function(data) {
return data.value !== '';
});
formData = $.param(formData);
formAction = form.attr('action');
issuesUrl = formAction;
issuesUrl += "" + (formAction.indexOf('?') === -1 ? '?' : '&');
issuesUrl += formData;
return gl.utils.visitUrl(issuesUrl);
};
})(this),
initResetFilters: function() {
$('.reset-filters').on('click', function(e) {
e.preventDefault();
const target = e.target;
const $form = $(target).parents('.js-filter-form');
const baseIssuesUrl = target.href;
$form.attr('action', baseIssuesUrl);
gl.utils.visitUrl(baseIssuesUrl);
});
},
initBulkUpdate: function(pagePrefix) {
const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
const alreadyInitialized = !!this.bulkUpdateSidebar;
if (userCanBulkUpdate && !alreadyInitialized) {
IssuableBulkUpdateActions.init({
prefixId: pagePrefix,
});
this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
}
},
resetIncomingEmailToken: function() {
$('.incoming-email-token-reset').on('click', function(e) {
e.preventDefault();
$.ajax({
type: 'PUT',
url: $('.incoming-email-token-reset').attr('href'),
dataType: 'json',
success: function(response) {
$('#issue_email').val(response.new_issue_address).focus();
},
beforeSend: function() {
$('.incoming-email-token-reset').text('resetting...');
},
complete: function() {
$('.incoming-email-token-reset').text('reset it');
}
});
});
this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
}
};
})(window);
}
static resetIncomingEmailToken() {
$('.incoming-email-token-reset').on('click', (e) => {
e.preventDefault();
$.ajax({
type: 'PUT',
url: $('.incoming-email-token-reset').attr('href'),
dataType: 'json',
success(response) {
$('#issue_email').val(response.new_issue_address).focus();
},
beforeSend() {
$('.incoming-email-token-reset').text('resetting...');
},
complete() {
$('.incoming-email-token-reset').text('reset it');
},
});
});
}
}

View File

@ -6,7 +6,7 @@ import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
class Issue {
export default class Issue {
constructor() {
if ($('a.btn-close').length) {
this.taskList = new TaskList({
@ -147,5 +147,3 @@ class Issue {
});
}
}
export default Issue;

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

@ -1,34 +1,23 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
(function() {
this.IssueStatusSelect = (function() {
function IssueStatusSelect() {
$('.js-issue-status').each(function(i, el) {
var fieldName;
fieldName = $(el).data("field-name");
return $(el).glDropdown({
selectable: true,
fieldName: fieldName,
toggleLabel: (function(_this) {
return function(selected, el, instance) {
var $item, label;
label = 'Author';
$item = instance.dropdown.find('.is-active');
if ($item.length) {
label = $item.text();
}
return label;
};
})(this),
clicked: function(options) {
return options.e.preventDefault();
},
id: function(obj, el) {
return $(el).data("id");
}
});
});
}
return IssueStatusSelect;
})();
}).call(window);
export default function issueStatusSelect() {
$('.js-issue-status').each((i, el) => {
const fieldName = $(el).data('field-name');
return $(el).glDropdown({
selectable: true,
fieldName,
toggleLabel(selected, element, instance) {
let label = 'Author';
const $item = instance.dropdown.find('.is-active');
if ($item.length) {
label = $item.text();
}
return label;
},
clicked(options) {
return options.e.preventDefault();
},
id(obj, element) {
return $(element).data('id');
},
});
});
}

View File

@ -1,7 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */
import _ from 'underscore';
import Cookies from 'js-cookie';
import NewNavSidebar from './new_sidebar';
import ContextualSidebar from './contextual_sidebar';
import initFlyOutNav from './fly_out_nav';
(function() {
@ -51,8 +51,8 @@ import initFlyOutNav from './fly_out_nav';
});
$(() => {
const newNavSidebar = new NewNavSidebar();
newNavSidebar.bindEvents();
const contextualSidebar = new ContextualSidebar();
contextualSidebar.bindEvents();
initFlyOutNav();
});

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

@ -28,14 +28,10 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => {
}
};
export default (el, insertPlaceholder = true) => {
export default (el, stickyTop, insertPlaceholder = true) => {
if (!el) return;
const computedStyle = window.getComputedStyle(el);
if (!/sticky/.test(computedStyle.position)) return;
const stickyTop = parseInt(computedStyle.top, 10);
if (typeof CSS === 'undefined' || !(CSS.supports('(position: -webkit-sticky) or (position: sticky)'))) return;
document.addEventListener('scroll', () => isSticky(el, window.scrollY, stickyTop, insertPlaceholder), {
passive: true,

View File

@ -41,7 +41,6 @@ import './behaviors/';
import './activities';
import './admin';
import './aside';
import './autosave';
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
import './commits';
@ -50,25 +49,13 @@ 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, { 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';
import './issuable_index';
import './issuable_context';
import './issuable_form';
import './issue';
import './issue_status_select';
import initTodoToggle from './header';
import initImporterStatus from './importer_status';
import './labels_select';
import './layout_nav';
import LazyLoader from './lazy_loader';
@ -146,6 +133,8 @@ $(function () {
var fitSidebarForSize;
initBreadcrumbs();
initImporterStatus();
initTodoToggle();
// Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/';

View File

@ -1,6 +1,5 @@
/* 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
@ -22,8 +21,10 @@ export default function memberExpirationDate(selector = '.js-access-expiration-d
format: 'yyyy-mm-dd',
minDate: new Date(),
container: $input.parent().get(0),
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect(dateText) {
$input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
$input.val(calendar.toString(dateText));
$input.trigger('change');
@ -31,7 +32,7 @@ export default function memberExpirationDate(selector = '.js-access-expiration-d
},
});
calendar.setDate(new Date($input.val()));
calendar.setDate(parsePikadayDate($input.val()));
$input.data('pikaday', calendar);
});

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
@ -67,6 +67,10 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
class MergeRequestTabs {
constructor({ action, setUrl, stubLocation } = {}) {
const mergeRequestTabs = document.querySelector('.js-tabs-affix');
const navbar = document.querySelector('.navbar-gitlab');
const paddingTop = 16;
this.diffsLoaded = false;
this.pipelinesLoaded = false;
this.commitsLoaded = false;
@ -76,6 +80,11 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
this.setCurrentAction = this.setCurrentAction.bind(this);
this.tabShown = this.tabShown.bind(this);
this.showTab = this.showTab.bind(this);
this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
if (mergeRequestTabs) {
this.stickyTop += mergeRequestTabs.offsetHeight;
}
if (stubLocation) {
location = stubLocation;
@ -278,7 +287,7 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
const $container = $('#diffs');
$container.html(data.html);
initChangesDropdown();
initChangesDropdown(this.stickyTop);
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
@ -292,7 +301,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

@ -5,15 +5,14 @@ default-case, prefer-template, consistent-return, no-alert, no-return-assign,
no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new,
brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow,
newline-per-chained-call, no-useless-escape, class-methods-use-this */
/* global Autosave */
/* global ResolveService */
/* global mrRefreshWidgetUrl */
import $ from 'jquery';
import _ from 'underscore';
import Cookies from 'js-cookie';
import autosize from 'vendor/autosize';
import Dropzone from 'dropzone';
import Autosize from 'autosize';
import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
@ -21,14 +20,12 @@ import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
import './autosave';
import './dropzone_input';
import Autosave from './autosave';
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;
window.autosize = Autosize;
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

@ -1,10 +1,9 @@
<script>
/* global Autosave */
import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore';
import autosize from 'vendor/autosize';
import Autosize from 'autosize';
import Flash from '../../flash';
import '../../autosave';
import Autosave from '../../autosave';
import TaskList from '../../task_list';
import * as constants from '../constants';
import eventHub from '../event_hub';
@ -220,7 +219,7 @@
},
resizeTextarea() {
this.$nextTick(() => {
autosize.update(this.$refs.textarea);
Autosize.update(this.$refs.textarea);
});
},
},

View File

@ -9,8 +9,8 @@
import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
import issueNoteEditedText from './issue_note_edited_text.vue';
import issueNoteForm from './issue_note_form.vue';
import placeholderNote from './issue_placeholder_note.vue';
import placeholderSystemNote from './issue_placeholder_system_note.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave';
export default {

View File

@ -5,10 +5,10 @@
import * as constants from '../constants';
import issueNote from './issue_note.vue';
import issueDiscussion from './issue_discussion.vue';
import issueSystemNote from './issue_system_note.vue';
import systemNote from '../../vue_shared/components/notes/system_note.vue';
import issueCommentForm from './issue_comment_form.vue';
import placeholderNote from './issue_placeholder_note.vue';
import placeholderSystemNote from './issue_placeholder_system_note.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
@ -37,7 +37,7 @@
components: {
issueNote,
issueDiscussion,
issueSystemNote,
systemNote,
issueCommentForm,
loadingIcon,
placeholderNote,
@ -68,7 +68,7 @@
}
return placeholderNote;
} else if (note.individual_note) {
return note.notes[0].system ? issueSystemNote : issueNote;
return note.notes[0].system ? systemNote : issueNote;
}
return issueDiscussion;

View File

@ -1,5 +1,4 @@
/* globals Autosave */
import '../../autosave';
import Autosave from '../../autosave';
export default {
methods: {

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

@ -2,13 +2,15 @@
import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button';
(function() {
this.ProjectSelect = (function() {
(function () {
this.ProjectSelect = (function () {
function ProjectSelect() {
$('.ajax-project-select').each(function(i, select) {
var placeholder;
const simpleFilter = $(select).data('simple-filter') || false;
this.groupId = $(select).data('group-id');
this.includeGroups = $(select).data('include-groups');
this.allProjects = $(select).data('all-projects') || false;
this.orderBy = $(select).data('order-by') || 'id';
this.withIssuesEnabled = $(select).data('with-issues-enabled');
this.withMergeRequestsEnabled = $(select).data('with-merge-requests-enabled');
@ -21,10 +23,10 @@ import ProjectSelectComboButton from './project_select_combo_button';
$(select).select2({
placeholder: placeholder,
minimumInputLength: 0,
query: (function(_this) {
return function(query) {
query: (function (_this) {
return function (query) {
var finalCallback, projectsCallback;
finalCallback = function(projects) {
finalCallback = function (projects) {
var data;
data = {
results: projects
@ -32,9 +34,9 @@ import ProjectSelectComboButton from './project_select_combo_button';
return query.callback(data);
};
if (_this.includeGroups) {
projectsCallback = function(projects) {
projectsCallback = function (projects) {
var groupsCallback;
groupsCallback = function(groups) {
groupsCallback = function (groups) {
var data;
data = groups.concat(projects);
return finalCallback(data);
@ -50,23 +52,25 @@ import ProjectSelectComboButton from './project_select_combo_button';
return Api.projects(query.term, {
order_by: _this.orderBy,
with_issues_enabled: _this.withIssuesEnabled,
with_merge_requests_enabled: _this.withMergeRequestsEnabled
with_merge_requests_enabled: _this.withMergeRequestsEnabled,
membership: !_this.allProjects,
}, projectsCallback);
}
};
})(this),
id: function(project) {
if (simpleFilter) return project.id;
return JSON.stringify({
name: project.name,
url: project.web_url,
});
},
text: function(project) {
text: function (project) {
return project.name_with_namespace || project.name;
},
dropdownCssClass: "ajax-project-dropdown"
});
if (simpleFilter) return select;
return new ProjectSelectComboButton(select);
});
}

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,108 @@
<script>
import { mapState, mapActions } from 'vuex';
import flash, { hideFlash } from '../../flash';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
components: {
loadingIcon,
},
data() {
return {
branchName: '',
loading: false,
};
},
computed: {
...mapState([
'currentBranch',
]),
btnDisabled() {
return this.loading || this.branchName === '';
},
},
methods: {
...mapActions([
'createNewBranch',
]),
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);
}
this.createNewBranch(this.branchName)
.then(() => {
this.loading = false;
this.branchName = '';
if (this.dropdownText) {
this.dropdownText.textContent = this.currentBranch;
}
this.toggleDropdown();
})
.catch(res => res.json().then((data) => {
this.loading = false;
flash(data.message, 'alert', this.$el);
}));
},
},
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');
},
};
</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

@ -0,0 +1,84 @@
<script>
import { mapState } from 'vuex';
import newModal from './modal.vue';
import upload from './upload.vue';
export default {
components: {
newModal,
upload,
},
data() {
return {
openModal: false,
modalType: '',
};
},
computed: {
...mapState([
'path',
]),
},
methods: {
createNewItem(type) {
this.modalType = type;
this.toggleModalOpen();
},
toggleModalOpen() {
this.openModal = !this.openModal;
},
},
};
</script>
<template>
<div>
<ul class="breadcrumb repo-breadcrumb">
<li class="dropdown">
<button
type="button"
class="btn btn-default dropdown-toggle add-to-tree"
data-toggle="dropdown"
aria-label="Create new file or directory"
>
<i
class="fa fa-plus"
aria-hidden="true"
>
</i>
</button>
<ul class="dropdown-menu">
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('blob')"
>
{{ __('New file') }}
</a>
</li>
<li>
<upload
:path="path"
/>
</li>
<li>
<a
href="#"
role="button"
@click.prevent="createNewItem('tree')"
>
{{ __('New directory') }}
</a>
</li>
</ul>
</li>
</ul>
<new-modal
v-if="openModal"
:type="modalType"
:path="path"
@toggle="toggleModalOpen"
/>
</div>
</template>

View File

@ -0,0 +1,98 @@
<script>
import { mapActions } from 'vuex';
import { __ } from '../../../locale';
import popupDialog from '../../../vue_shared/components/popup_dialog.vue';
export default {
props: {
type: {
type: String,
required: true,
},
path: {
type: String,
required: true,
},
},
data() {
return {
entryName: this.path !== '' ? `${this.path}/` : '',
};
},
components: {
popupDialog,
},
methods: {
...mapActions([
'createTempEntry',
]),
createEntryInStore() {
this.createTempEntry({
name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
type: this.type,
});
this.toggleModalOpen();
},
toggleModalOpen() {
this.$emit('toggle');
},
},
computed: {
modalTitle() {
if (this.type === 'tree') {
return __('Create new directory');
}
return __('Create new file');
},
buttonLabel() {
if (this.type === 'tree') {
return __('Create directory');
}
return __('Create file');
},
formLabelName() {
if (this.type === 'tree') {
return __('Directory name');
}
return __('File name');
},
},
mounted() {
this.$refs.fieldName.focus();
},
};
</script>
<template>
<popup-dialog
:title="modalTitle"
:primary-button-label="buttonLabel"
kind="success"
@toggle="toggleModalOpen"
@submit="createEntryInStore"
>
<form
class="form-horizontal"
slot="body"
@submit.prevent="createEntryInStore"
>
<fieldset class="form-group append-bottom-0">
<label class="label-light col-sm-3">
{{ formLabelName }}
</label>
<div class="col-sm-9">
<input
type="text"
class="form-control"
v-model="entryName"
ref="fieldName"
/>
</div>
</fieldset>
</form>
</popup-dialog>
</template>

View File

@ -0,0 +1,68 @@
<script>
import { mapActions } from 'vuex';
export default {
props: {
path: {
type: String,
required: true,
},
},
methods: {
...mapActions([
'createTempEntry',
]),
createFile(target, file, isText) {
const { name } = file;
let { result } = target;
if (!isText) {
result = result.split('base64,')[1];
}
this.createTempEntry({
name,
type: 'blob',
content: result,
base64: !isText,
});
},
readFile(file) {
const reader = new FileReader();
const isText = file.type.match(/text.*/) !== null;
reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true });
if (isText) {
reader.readAsText(file);
} else {
reader.readAsDataURL(file);
}
},
openFile() {
Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
},
},
mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile);
},
beforeDestroy() {
this.$refs.fileUpload.removeEventListener('change', this.openFile);
},
};
</script>
<template>
<label
role="button"
class="menu-item"
>
{{ __('Upload file') }}
<input
id="file-upload"
type="file"
class="hidden"
ref="fileUpload"
/>
</label>
</template>

View File

@ -1,70 +1,59 @@
<script>
import { mapState, mapGetters } from 'vuex';
import RepoSidebar from './repo_sidebar.vue';
import RepoCommitSection from './repo_commit_section.vue';
import RepoTabs from './repo_tabs.vue';
import RepoFileButtons from './repo_file_buttons.vue';
import RepoPreview from './repo_preview.vue';
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 MonacoLoaderHelper from '../helpers/monaco_loader_helper';
import repoEditor from './repo_editor.vue';
export default {
data: () => Store,
mixins: [RepoMixin],
computed: {
...mapState([
'currentBlobView',
]),
...mapGetters([
'isCollapsed',
'changedFiles',
]),
},
components: {
RepoSidebar,
RepoTabs,
RepoFileButtons,
'repo-editor': MonacoLoaderHelper.repoEditorLoader,
repoEditor,
RepoCommitSection,
PopupDialog,
RepoPreview,
},
mounted() {
Helper.getContent().catch(Helper.loadingError);
},
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = (e) => {
if (!this.changedFiles.length) return undefined;
methods: {
toggleDialogOpen(toggle) {
this.dialog.open = toggle;
},
dialogSubmitted(status) {
this.toggleDialogOpen(false);
this.dialog.status = status;
},
toggleBlobView: Store.toggleBlobView,
Object.assign(e, {
returnValue,
});
return returnValue;
};
},
};
</script>
<template>
<div class="repository-view">
<div class="tree-content-holder" :class="{'tree-content-holder-mini' : isMini}">
<div class="tree-content-holder" :class="{'tree-content-holder-mini' : isCollapsed}">
<repo-sidebar/>
<div v-if="isMini"
class="panel-right"
:class="{'edit-mode': editMode}">
<div
v-if="isCollapsed"
class="panel-right"
>
<repo-tabs/>
<component
:is="currentBlobView"
class="blob-viewer-container"/>
/>
<repo-file-buttons/>
</div>
</div>
<repo-commit-section/>
<popup-dialog
v-show="dialog.open"
:primary-button-label="__('Discard changes')"
kind="warning"
:title="__('Are you sure?')"
:text="__('Are you sure you want to discard your changes?')"
@toggle="toggleDialogOpen"
@submit="dialogSubmitted"
/>
<repo-commit-section v-if="changedFiles.length" />
</div>
</template>

View File

@ -1,137 +1,100 @@
<script>
import Flash from '../../flash';
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
import Service from '../services/repo_service';
import { mapGetters, mapState, mapActions } from 'vuex';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import { visitUrl } from '../../lib/utils/url_utility';
import { n__ } from '../../locale';
export default {
mixins: [RepoMixin],
data: () => Store,
components: {
PopupDialog,
},
data() {
return {
showNewBranchDialog: false,
submitCommitsLoading: false,
startNewMR: false,
commitMessage: '',
};
},
computed: {
showCommitable() {
return this.isCommitable && this.changedFiles.length;
},
branchPaths() {
return this.changedFiles.map(f => f.path);
},
cantCommitYet() {
...mapState([
'currentBranch',
]),
...mapGetters([
'changedFiles',
]),
commitButtonDisabled() {
return !this.commitMessage || this.submitCommitsLoading;
},
filePluralize() {
return this.changedFiles.length > 1 ? 'files' : 'file';
commitButtonText() {
return n__('Commit %d file', 'Commit %d files', this.changedFiles.length);
},
},
methods: {
commitToNewBranch(status) {
if (status) {
this.showNewBranchDialog = false;
this.tryCommit(null, true, true);
} else {
// reset the state
}
},
...mapActions([
'checkCommitStatus',
'commitChanges',
'getTreeData',
]),
makeCommit(newBranch = false) {
const createNewBranch = newBranch || this.startNewMR;
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 => ({
action: 'update',
file_path: f.path,
content: f.newContent,
}));
const branch = newBranch ? `${this.currentBranch}-${this.currentShortHash}` : this.currentBranch;
const payload = {
branch,
commit_message: commitMessage,
actions,
branch: createNewBranch ? `${this.currentBranch}-${new Date().getTime().toString()}` : this.currentBranch,
commit_message: this.commitMessage,
actions: this.changedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
file_path: f.path,
content: f.content,
encoding: f.base64 ? 'base64' : 'text',
})),
start_branch: createNewBranch ? this.currentBranch : undefined,
};
if (newBranch) {
payload.start_branch = this.currentBranch;
}
this.showNewBranchDialog = false;
this.submitCommitsLoading = true;
Service.commitFiles(payload)
this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => {
this.resetCommitState();
if (this.startNewMR) {
this.redirectToNewMr(branch);
this.submitCommitsLoading = false;
this.getTreeData();
})
.catch(() => {
this.submitCommitsLoading = false;
});
},
tryCommit() {
this.submitCommitsLoading = true;
this.checkCommitStatus()
.then((branchChanged) => {
if (branchChanged) {
this.showNewBranchDialog = true;
} else {
this.redirectToBranch(branch);
this.makeCommit();
}
})
.catch(() => {
Flash('An error occurred while committing your changes');
this.submitCommitsLoading = false;
});
},
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;
window.scrollTo(0, 0);
},
},
};
</script>
<template>
<div
v-if="showCommitable"
id="commit-area">
<div 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"
@toggle="showNewBranchDialog = false"
@submit="makeCommit(true)"
/>
<form
class="form-horizontal"
@submit.prevent="tryCommit">
@submit.prevent="tryCommit()">
<fieldset>
<div class="form-group">
<label class="col-md-4 control-label staged-files">
@ -140,10 +103,10 @@ export default {
<div class="col-md-6">
<ul class="list-unstyled changed-files">
<li
v-for="branchPath in branchPaths"
:key="branchPath">
v-for="(file, index) in changedFiles"
:key="index">
<span class="help-block">
{{branchPath}}
{{ file.path }}
</span>
</li>
</ul>
@ -178,9 +141,8 @@ export default {
</div>
<div class="col-md-offset-4 col-md-6">
<button
ref="submitCommit"
type="submit"
:disabled="cantCommitYet"
:disabled="commitButtonDisabled"
class="btn btn-success">
<i
v-if="submitCommitsLoading"
@ -189,7 +151,7 @@ export default {
aria-label="loading">
</i>
<span class="commit-summary">
Commit {{changedFiles.length}} {{filePluralize}}
{{ commitButtonText }}
</span>
</button>
</div>

View File

@ -1,48 +1,57 @@
<script>
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
import { mapGetters, mapActions, mapState } from 'vuex';
import popupDialog from '../../vue_shared/components/popup_dialog.vue';
export default {
data: () => Store,
mixins: [RepoMixin],
components: {
popupDialog,
},
computed: {
...mapState([
'editMode',
'discardPopupOpen',
]),
...mapGetters([
'canEditFile',
]),
buttonLabel() {
return this.editMode ? this.__('Cancel edit') : this.__('Edit');
},
showButton() {
return this.isCommitable &&
!this.activeFile.render_error &&
!this.binary &&
this.openedFiles.length;
},
},
methods: {
editCancelClicked() {
if (this.changedFiles.length) {
this.dialog.open = true;
return;
}
this.editMode = !this.editMode;
Store.toggleBlobView();
},
...mapActions([
'toggleEditMode',
'closeDiscardPopup',
]),
},
};
</script>
<template>
<button
v-if="showButton"
class="btn btn-default"
type="button"
@click.prevent="editCancelClicked">
<i
v-if="!editMode"
class="fa fa-pencil"
aria-hidden="true">
</i>
<span>
{{buttonLabel}}
</span>
</button>
<div class="editable-mode">
<button
v-if="canEditFile"
class="btn btn-default"
type="button"
@click.prevent="toggleEditMode()">
<i
v-if="!editMode"
class="fa fa-pencil"
aria-hidden="true">
</i>
<span>
{{buttonLabel}}
</span>
</button>
<popup-dialog
v-if="discardPopupOpen"
class="text-left"
:primary-button-label="__('Discard changes')"
kind="warning"
:title="__('Are you sure?')"
:text="__('Are you sure you want to discard your changes?')"
@toggle="closeDiscardPopup"
@submit="toggleEditMode(true)"
/>
</div>
</template>

View File

@ -1,121 +1,101 @@
<script>
/* global monaco */
import Store from '../stores/repo_store';
import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper';
const RepoEditor = {
data: () => Store,
import { mapGetters, mapActions } from 'vuex';
import flash from '../../flash';
import monacoLoader from '../monaco_loader';
export default {
destroyed() {
if (Helper.monacoInstance) {
Helper.monacoInstance.destroy();
if (this.monacoInstance) {
this.monacoInstance.destroy();
}
},
mounted() {
Service.getRaw(this.activeFile.raw_path)
.then((rawResponse) => {
Store.blobRaw = rawResponse.data;
Store.activeFile.plain = rawResponse.data;
if (this.monaco) {
this.initMonaco();
} else {
monacoLoader(['vs/editor/editor.main'], () => {
this.monaco = monaco;
const monacoInstance = Helper.monaco.editor.create(this.$el, {
model: null,
readOnly: false,
contextmenu: false,
});
Helper.monacoInstance = monacoInstance;
this.addMonacoEvents();
this.setupEditor();
})
.catch(Helper.loadingError);
this.initMonaco();
});
}
},
methods: {
...mapActions([
'getRawFileData',
'changeFileContent',
]),
initMonaco() {
if (this.monacoInstance) {
this.monacoInstance.setModel(null);
}
this.getRawFileData(this.activeFile)
.then(() => {
if (!this.monacoInstance) {
this.monacoInstance = this.monaco.editor.create(this.$el, {
model: null,
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
});
this.languages = this.monaco.languages.getLanguages();
this.addMonacoEvents();
}
this.setupEditor();
})
.catch(() => flash('Error setting up monaco. Please try again.'));
},
setupEditor() {
this.showHide();
if (!this.activeFile) return;
const content = this.activeFile.content !== '' ? this.activeFile.content : this.activeFile.raw;
Helper.setMonacoModelFromLanguage();
const foundLang = this.languages.find(lang =>
lang.extensions && lang.extensions.indexOf(this.activeFileExtension) === 0,
);
const newModel = this.monaco.editor.createModel(
content, foundLang ? foundLang.id : 'plaintext',
);
this.monacoInstance.setModel(newModel);
},
showHide() {
if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) {
this.$el.style.display = 'none';
} else {
this.$el.style.display = 'inline-block';
}
},
addMonacoEvents() {
Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp);
Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this));
},
onMonacoEditorKeysPressed() {
Store.setActiveFileContents(Helper.monacoInstance.getValue());
},
onMonacoEditorMouseUp(e) {
if (!e.target.position) return;
const lineNumber = e.target.position.lineNumber;
if (e.target.element.classList.contains('line-numbers')) {
location.hash = `L${lineNumber}`;
Store.setActiveLine(lineNumber);
}
this.monacoInstance.onKeyUp(() => {
this.changeFileContent({
file: this.activeFile,
content: this.monacoInstance.getValue(),
});
});
},
},
watch: {
dialog: {
handler(obj) {
const newObj = obj;
if (newObj.status) {
newObj.status = false;
this.openedFiles = this.openedFiles.map((file) => {
const f = file;
if (f.active) {
this.blobRaw = f.plain;
}
f.changed = false;
delete f.newContent;
return f;
});
this.editMode = false;
Store.toggleBlobView();
}
},
deep: true,
},
blobRaw() {
if (Helper.monacoInstance && !this.isTree) {
this.setupEditor();
}
},
activeLine() {
if (Helper.monacoInstance) {
Helper.monacoInstance.setPosition({
lineNumber: this.activeLine,
column: 1,
});
activeFile(oldVal, newVal) {
if (newVal && !newVal.active) {
this.initMonaco();
}
},
},
computed: {
...mapGetters([
'activeFile',
'activeFileExtension',
]),
shouldHideEditor() {
return !this.openedFiles.length || (this.binary && !this.activeFile.raw);
return this.activeFile.binary && !this.activeFile.raw;
},
},
};
export default RepoEditor;
</script>
<template>
<div id="ide" v-if='!shouldHideEditor'></div>
<div
id="ide"
v-if='!shouldHideEditor'
class="blob-viewer-container blob-editor-container"
>
</div>
</template>

View File

@ -1,107 +1,95 @@
<script>
import TimeAgoMixin from '../../vue_shared/mixins/timeago';
import { mapActions, mapGetters } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
const RepoFile = {
mixins: [TimeAgoMixin],
props: {
file: {
type: Object,
required: true,
export default {
mixins: [
timeAgoMixin,
],
props: {
file: {
type: Object,
required: true,
},
},
isMini: {
type: Boolean,
required: false,
default: false,
computed: {
...mapGetters([
'isCollapsed',
]),
fileIcon() {
return {
'fa-spinner fa-spin': this.file.loading,
[this.file.icon]: !this.file.loading,
'fa-folder-open': !this.file.loading && this.file.opened,
};
},
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: {
...mapActions([
'clickedTreeRow',
]),
},
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="clickedTreeRow(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="!isCollapsed">
<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

@ -1,40 +1,35 @@
<script>
import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper';
import RepoMixin from '../mixins/repo_mixin';
const RepoFileButtons = {
data: () => Store,
mixins: [RepoMixin],
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters([
'activeFile',
]),
showButtons() {
return this.activeFile.rawPath ||
this.activeFile.blamePath ||
this.activeFile.commitsPath ||
this.activeFile.permalink;
},
rawDownloadButtonLabel() {
return this.binary ? 'Download' : 'Raw';
return this.activeFile.binary ? 'Download' : 'Raw';
},
canPreview() {
return Helper.isRenderable();
},
},
methods: {
rawPreviewToggle: Store.toggleRawPreview,
},
};
export default RepoFileButtons;
</script>
<template>
<div id="repo-file-buttons">
<div
v-if="showButtons"
class="repo-file-buttons"
>
<a
:href="activeFile.raw_path"
:href="activeFile.rawPath"
target="_blank"
class="btn btn-default raw"
rel="noopener noreferrer">
{{rawDownloadButtonLabel}}
{{ rawDownloadButtonLabel }}
</a>
<div
@ -42,12 +37,12 @@ export default RepoFileButtons;
role="group"
aria-label="File actions">
<a
:href="activeFile.blame_path"
:href="activeFile.blamePath"
class="btn btn-default blame">
Blame
</a>
<a
:href="activeFile.commits_path"
:href="activeFile.commitsPath"
class="btn btn-default history">
History
</a>
@ -57,13 +52,5 @@ export default RepoFileButtons;
Permalink
</a>
</div>
<a
v-if="canPreview"
href="#"
@click.prevent="rawPreviewToggle"
class="btn btn-default preview">
{{activeFileLabel}}
</a>
</div>
</template>

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,25 @@
<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 { mapGetters } from 'vuex';
computed: {
showGhostLines() {
return this.loading.tree && !this.hasFiles;
export default {
computed: {
...mapGetters([
'isCollapsed',
]),
},
},
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 +30,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="!isCollapsed">
<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,34 @@
<script>
import RepoMixin from '../mixins/repo_mixin';
import { mapGetters, mapState, mapActions } from 'vuex';
const RepoPreviousDirectory = {
props: {
prevUrl: {
type: String,
required: true,
export default {
computed: {
...mapState([
'parentTreeUrl',
]),
...mapGetters([
'isCollapsed',
]),
colSpanCondition() {
return this.isCollapsed ? undefined : 3;
},
},
},
mixins: [RepoMixin],
computed: {
colSpanCondition() {
return this.isMini ? undefined : 3;
methods: {
...mapActions([
'getTreeData',
]),
},
},
methods: {
linkClicked(file) {
this.$emit('linkclicked', 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="getTreeData({ endpoint: parentTreeUrl })"
>
<a :href="parentTreeUrl">...</a>
</td>
</tr>
</template>

View File

@ -1,24 +1,20 @@
<script>
/* global LineHighlighter */
import Store from '../stores/repo_store';
import { mapGetters } from 'vuex';
export default {
data: () => Store,
computed: {
html() {
return this.activeFile.html;
...mapGetters([
'activeFile',
]),
renderErrorTooLarge() {
return this.activeFile.renderError === 'too_large';
},
},
methods: {
highlightFile() {
$(this.$el).find('.file-content').syntaxHighlight();
},
highlightLine() {
if (Store.activeLine > -1) {
this.lineHighlighter.highlightHash(`#L${Store.activeLine}`);
}
},
},
mounted() {
this.highlightFile();
@ -27,38 +23,39 @@ export default {
scrollFileHolder: true,
});
},
watch: {
html() {
this.$nextTick(() => {
this.highlightFile();
this.highlightLine();
});
},
activeLine() {
this.highlightLine();
},
updated() {
this.$nextTick(() => {
this.highlightFile();
});
},
};
</script>
<template>
<div>
<div class="blob-viewer-container">
<div
v-if="!activeFile.render_error"
v-if="!activeFile.renderError"
v-html="activeFile.html">
</div>
<div
v-else-if="activeFile.tooLarge"
v-else-if="activeFile.tempFile"
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.
The source could not be displayed for this temporary file.
</p>
</div>
<div
v-else-if="renderErrorTooLarge"
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because it is too large. You can <a :href="activeFile.rawPath" download>download</a> it instead.
</p>
</div>
<div
v-else
class="vertical-center render-error">
<p class="text-center">
The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.raw_path">download</a> it instead.
The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.rawPath" download>download</a> it instead.
</p>
</div>
</div>

View File

@ -1,129 +1,87 @@
<script>
import Service from '../services/repo_service';
import Helper from '../helpers/repo_helper';
import Store from '../stores/repo_store';
import { mapState, mapGetters, mapActions } from 'vuex';
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';
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);
window.addEventListener('popstate', this.popHistoryState);
},
destroyed() {
window.removeEventListener('popstate', this.checkHistory);
window.removeEventListener('popstate', this.popHistoryState);
},
mounted() {
this.getTreeData();
},
computed: {
...mapState([
'loading',
'isRoot',
]),
...mapState({
projectName(state) {
return state.project.name;
},
}),
...mapGetters([
'treeList',
'isCollapsed',
]),
},
data: () => Store,
methods: {
checkHistory() {
let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1);
if (!selectedFile) {
// Maybe it is not in the current tree but in the opened tabs
selectedFile = Helper.getFileFromPath(location.pathname);
}
let lineNumber = null;
if (location.hash.indexOf('#L') > -1) lineNumber = Number(location.hash.substr(2));
if (selectedFile) {
if (selectedFile.url !== this.activeFile.url) {
this.fileClicked(selectedFile, lineNumber);
} else {
Store.setActiveLine(lineNumber);
}
} else {
// Not opened at all lets open new tab
this.fileClicked({
url: location.href,
}, lineNumber);
}
},
fileClicked(clickedFile, lineNumber) {
let file = clickedFile;
if (file.loading) return;
file.loading = true;
if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file);
file.loading = false;
Store.setActiveLine(lineNumber);
} else {
const openFile = Helper.getFileFromPath(file.url);
if (openFile) {
file.loading = false;
Store.setActiveFiles(openFile);
Store.setActiveLine(lineNumber);
} else {
Service.url = file.url;
Helper.getContent(file)
.then(() => {
file.loading = false;
Helper.scrollTabsRight();
Store.setActiveLine(lineNumber);
})
.catch(Helper.loadingError);
}
}
},
goToPreviousDirectoryClicked(prevURL) {
Service.url = prevURL;
Helper.getContent(null)
.then(() => Helper.scrollTabsRight())
.catch(Helper.loadingError);
},
...mapActions([
'getTreeData',
'popHistoryState',
]),
},
};
</script>
<template>
<div id="sidebar" :class="{'sidebar-mini' : isMini}">
<div id="sidebar" :class="{'sidebar-mini' : isCollapsed}">
<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="isCollapsed"
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"
:prev-url="prevURL"
@linkclicked="goToPreviousDirectoryClicked(prevURL)"/>
v-if="!isRoot && treeList.length"
/>
<repo-loading-file
v-if="!treeList.length && loading"
v-for="n in 5"
:key="n"
:loading="loading"
:has-files="!!files.length"
:is-mini="isMini"
/>
<repo-file
v-for="file in files"
:key="file.id"
v-for="(file, index) in treeList"
:key="index"
:file="file"
:is-mini="isMini"
@linkclicked="fileClicked(file)"
:is-tree="isTree"
:has-files="!!files.length"
:active-file="activeFile"
/>
</tbody>
</table>

View File

@ -1,7 +1,7 @@
<script>
import Store from '../stores/repo_store';
import { mapActions } from 'vuex';
const RepoTab = {
export default {
props: {
tab: {
type: Object,
@ -11,53 +11,52 @@ const RepoTab = {
computed: {
closeLabel() {
if (this.tab.changed) {
if (this.tab.changed || this.tab.tempFile) {
return `${this.tab.name} changed`;
}
return `Close ${this.tab.name}`;
},
changedClass() {
const tabChangedObj = {
'fa-times close-icon': !this.tab.changed,
'fa-circle unsaved-icon': this.tab.changed,
'fa-times close-icon': !this.tab.changed && !this.tab.tempFile,
'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile,
};
return tabChangedObj;
},
},
methods: {
tabClicked: Store.setActiveFiles,
closeTab(file) {
if (file.changed) return;
this.$emit('tabclosed', file);
},
...mapActions([
'setFileActive',
'closeFile',
]),
},
};
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="setFileActive(tab)"
>
<button
type="button"
class="close-btn"
@click.stop.prevent="closeFile({ file: 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.stop="setFileActive(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 { mapState } from 'vuex';
import RepoTab from './repo_tab.vue';
const RepoTabs = {
mixins: [RepoMixin],
components: {
'repo-tab': RepoTab,
},
data: () => Store,
methods: {
tabClosed(file) {
Store.removeFromOpenedFiles(file);
export default {
components: {
'repo-tab': RepoTab,
},
},
};
export default RepoTabs;
computed: {
...mapState([
'openFiles',
]),
},
};
</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 openFiles"
:key="tab.id"
:tab="tab"
/>
<li class="tabs-divider" />
</ul>
</template>

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