Merge branch 'refactor-clusters' into 38464-k8s-apps
This commit is contained in:
commit
461c385ebc
152
.gitlab-ci.yml
152
.gitlab-ci.yml
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
# Documentation
|
||||
- source: /doc/(.+?)\.md/ # doc/administration/build_artifacts.md
|
||||
public: '\1.html' # doc/administration/build_artifacts.html
|
||||
|
|
@ -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 ########################################################################
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2.3.3
|
||||
2.3.5
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
215
CHANGELOG.md
215
CHANGELOG.md
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
0.47.0
|
||||
0.51.0
|
||||
|
|
|
|||
8
Gemfile
8
Gemfile
|
|
@ -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
|
||||
|
||||
|
|
|
|||
16
Gemfile.lock
16
Gemfile.lock
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 7.8 KiB |
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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',
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 || '/';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
/* globals Autosave */
|
||||
import '../../autosave';
|
||||
import Autosave from '../../autosave';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
},
|
||||
|
||||
showError(message) {
|
||||
Flash((errorMessages[message]));
|
||||
Flash(errorMessages[message]);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@
|
|||
},
|
||||
|
||||
showError(message) {
|
||||
Flash((errorMessages[message]));
|
||||
Flash(errorMessages[message]);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue