Merge branch 'master' into 'es-module-autosave'
# Conflicts: # app/assets/javascripts/issuable_form.js # app/assets/javascripts/notes.js
This commit is contained in:
commit
f7bdfe5098
144
.gitlab-ci.yml
144
.gitlab-ci.yml
|
@ -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
|
||||
|
|
|
@ -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 ########################################################################
|
||||
|
||||
|
|
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)
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.46.0
|
||||
0.49.0
|
6
Gemfile
6
Gemfile
|
@ -102,7 +102,7 @@ gem 'fog-google', '~> 0.5'
|
|||
gem 'fog-local', '~> 0.3'
|
||||
gem 'fog-openstack', '~> 0.1'
|
||||
gem 'fog-rackspace', '~> 0.1.1'
|
||||
gem 'fog-aliyun', '~> 0.1.0'
|
||||
gem 'fog-aliyun', '~> 0.2.0'
|
||||
|
||||
# for Google storage
|
||||
gem 'google-api-client', '~> 0.13.6'
|
||||
|
@ -281,7 +281,7 @@ group :metrics do
|
|||
gem 'influxdb', '~> 0.2', require: false
|
||||
|
||||
# Prometheus
|
||||
gem 'prometheus-client-mmap', '~>0.7.0.beta14'
|
||||
gem 'prometheus-client-mmap', '~>0.7.0.beta17'
|
||||
gem 'raindrops', '~> 0.18'
|
||||
end
|
||||
|
||||
|
@ -398,7 +398,7 @@ group :ed25519 do
|
|||
end
|
||||
|
||||
# Gitaly GRPC client
|
||||
gem 'gitaly-proto', '~> 0.41.0', require: 'gitaly'
|
||||
gem 'gitaly-proto', '~> 0.45.0', require: 'gitaly'
|
||||
|
||||
gem 'toml-rb', '~> 0.3.15', require: false
|
||||
|
||||
|
|
19
Gemfile.lock
19
Gemfile.lock
|
@ -214,7 +214,7 @@ GEM
|
|||
flowdock (0.7.1)
|
||||
httparty (~> 0.7)
|
||||
multi_json
|
||||
fog-aliyun (0.1.0)
|
||||
fog-aliyun (0.2.0)
|
||||
fog-core (~> 1.27)
|
||||
fog-json (~> 1.0)
|
||||
ipaddress (~> 0.8)
|
||||
|
@ -273,7 +273,7 @@ GEM
|
|||
po_to_json (>= 1.0.0)
|
||||
rails (>= 3.2.0)
|
||||
gherkin-ruby (0.3.2)
|
||||
gitaly-proto (0.41.0)
|
||||
gitaly-proto (0.45.0)
|
||||
google-protobuf (~> 3.1)
|
||||
grpc (~> 1.0)
|
||||
github-linguist (4.7.6)
|
||||
|
@ -324,7 +324,9 @@ GEM
|
|||
mime-types (~> 3.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.0)
|
||||
google-protobuf (3.4.0.2)
|
||||
google-protobuf (3.4.1.1)
|
||||
googleapis-common-protos-types (1.0.0)
|
||||
google-protobuf (~> 3.0)
|
||||
googleauth (0.5.3)
|
||||
faraday (~> 0.12)
|
||||
jwt (~> 1.4)
|
||||
|
@ -351,8 +353,9 @@ GEM
|
|||
rake
|
||||
grape_logging (1.7.0)
|
||||
grape
|
||||
grpc (1.6.0)
|
||||
grpc (1.6.6)
|
||||
google-protobuf (~> 3.1)
|
||||
googleapis-common-protos-types (~> 1.0.0)
|
||||
googleauth (~> 0.5.1)
|
||||
haml (4.0.7)
|
||||
tilt
|
||||
|
@ -620,7 +623,7 @@ GEM
|
|||
parser
|
||||
unparser
|
||||
procto (0.0.3)
|
||||
prometheus-client-mmap (0.7.0.beta14)
|
||||
prometheus-client-mmap (0.7.0.beta17)
|
||||
mmap2 (~> 2.2, >= 2.2.7)
|
||||
pry (0.10.4)
|
||||
coderay (~> 1.1.0)
|
||||
|
@ -1012,7 +1015,7 @@ DEPENDENCIES
|
|||
flay (~> 2.8.0)
|
||||
flipper (~> 0.10.2)
|
||||
flipper-active_record (~> 0.10.2)
|
||||
fog-aliyun (~> 0.1.0)
|
||||
fog-aliyun (~> 0.2.0)
|
||||
fog-aws (~> 1.4)
|
||||
fog-core (~> 1.44)
|
||||
fog-google (~> 0.5)
|
||||
|
@ -1027,7 +1030,7 @@ DEPENDENCIES
|
|||
gettext (~> 3.2.2)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.2.0)
|
||||
gitaly-proto (~> 0.41.0)
|
||||
gitaly-proto (~> 0.45.0)
|
||||
github-linguist (~> 4.7.0)
|
||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||
gitlab-markup (~> 1.6.2)
|
||||
|
@ -1103,7 +1106,7 @@ DEPENDENCIES
|
|||
pg (~> 0.18.2)
|
||||
poltergeist (~> 1.9.0)
|
||||
premailer-rails (~> 1.9.7)
|
||||
prometheus-client-mmap (~> 0.7.0.beta14)
|
||||
prometheus-client-mmap (~> 0.7.0.beta17)
|
||||
pry-byebug (~> 3.4.1)
|
||||
pry-rails (~> 0.3.4)
|
||||
rack-attack (~> 4.4.1)
|
||||
|
|
|
@ -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 |
|
@ -15,6 +15,7 @@ const Api = {
|
|||
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
|
||||
usersPath: '/api/:version/users.json',
|
||||
commitPath: '/api/:version/projects/:id/repository/commits',
|
||||
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
|
||||
|
||||
group(groupId, callback) {
|
||||
const url = Api.buildUrl(Api.groupPath)
|
||||
|
@ -123,6 +124,19 @@ const Api = {
|
|||
});
|
||||
},
|
||||
|
||||
branchSingle(id, branch) {
|
||||
const url = Api.buildUrl(Api.branchSinglePath)
|
||||
.replace(':id', id)
|
||||
.replace(':branch', branch);
|
||||
|
||||
return this.wrapAjaxCall({
|
||||
url,
|
||||
type: 'GET',
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
dataType: 'json',
|
||||
});
|
||||
},
|
||||
|
||||
// Return text for a specific license
|
||||
licenseText(key, data, callback) {
|
||||
const url = Api.buildUrl(Api.licensePath)
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -9,6 +9,7 @@ import Flash from '../../flash';
|
|||
import eventHub from '../../sidebar/event_hub';
|
||||
import AssigneeTitle from '../../sidebar/components/assignees/assignee_title';
|
||||
import Assignees from '../../sidebar/components/assignees/assignees';
|
||||
import DueDateSelectors from '../../due_date_select';
|
||||
import './sidebar/remove_issue';
|
||||
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
@ -113,7 +114,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({
|
|||
mounted () {
|
||||
new IssuableContext(this.currentUser);
|
||||
new MilestoneSelect();
|
||||
new gl.DueDateSelectors();
|
||||
new DueDateSelectors();
|
||||
new LabelsSelect();
|
||||
new Sidebar();
|
||||
gl.Subscription.bindAll('.subscription');
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -1,33 +1,28 @@
|
|||
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, no-else-return, object-shorthand, comma-dangle, max-len */
|
||||
export default function initBroadcastMessagesForm() {
|
||||
$('input#broadcast_message_color').on('input', function onMessageColorInput() {
|
||||
const previewColor = $(this).val();
|
||||
$('div.broadcast-message-preview').css('background-color', previewColor);
|
||||
});
|
||||
|
||||
$(function() {
|
||||
var previewPath;
|
||||
$('input#broadcast_message_color').on('input', function() {
|
||||
var previewColor;
|
||||
previewColor = $(this).val();
|
||||
return $('div.broadcast-message-preview').css('background-color', previewColor);
|
||||
$('input#broadcast_message_font').on('input', function onMessageFontInput() {
|
||||
const previewColor = $(this).val();
|
||||
$('div.broadcast-message-preview').css('color', previewColor);
|
||||
});
|
||||
$('input#broadcast_message_font').on('input', function() {
|
||||
var previewColor;
|
||||
previewColor = $(this).val();
|
||||
return $('div.broadcast-message-preview').css('color', previewColor);
|
||||
});
|
||||
previewPath = $('textarea#broadcast_message_message').data('preview-path');
|
||||
return $('textarea#broadcast_message_message').on('input', function() {
|
||||
var message;
|
||||
message = $(this).val();
|
||||
|
||||
const previewPath = $('textarea#broadcast_message_message').data('preview-path');
|
||||
|
||||
$('textarea#broadcast_message_message').on('input', _.debounce(function onMessageInput() {
|
||||
const message = $(this).val();
|
||||
if (message === '') {
|
||||
return $('.js-broadcast-message-preview').text("Your message here");
|
||||
$('.js-broadcast-message-preview').text('Your message here');
|
||||
} else {
|
||||
return $.ajax({
|
||||
$.ajax({
|
||||
url: previewPath,
|
||||
type: "POST",
|
||||
type: 'POST',
|
||||
data: {
|
||||
broadcast_message: {
|
||||
message: message
|
||||
}
|
||||
}
|
||||
broadcast_message: { message },
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}, 250));
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,8 +1,6 @@
|
|||
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
|
||||
/* global ProjectSelect */
|
||||
/* global ShortcutsNavigation */
|
||||
/* global IssuableIndex */
|
||||
/* global ShortcutsIssuable */
|
||||
/* global Milestone */
|
||||
/* global IssuableForm */
|
||||
/* global LabelsSelect */
|
||||
|
@ -10,7 +8,8 @@
|
|||
/* global NewBranchForm */
|
||||
/* global NotificationsForm */
|
||||
/* global NotificationsDropdown */
|
||||
/* global GroupAvatar */
|
||||
import groupAvatar from './group_avatar';
|
||||
import GroupLabelSubscription from './group_label_subscription';
|
||||
/* global LineHighlighter */
|
||||
import BuildArtifacts from './build_artifacts';
|
||||
import CILintEditor from './ci_lint_editor';
|
||||
|
@ -31,10 +30,7 @@ import CILintEditor from './ci_lint_editor';
|
|||
/* global ProjectImport */
|
||||
import Labels from './labels';
|
||||
import LabelManager from './label_manager';
|
||||
/* global Shortcuts */
|
||||
/* global ShortcutsFindFile */
|
||||
/* global Sidebar */
|
||||
/* global ShortcutsWiki */
|
||||
|
||||
import CommitsList from './commits';
|
||||
import Issue from './issue';
|
||||
|
@ -70,6 +66,7 @@ import initSettingsPanels from './settings_panels';
|
|||
import initExperimentalFlags from './experimental_flags';
|
||||
import OAuthRememberMe from './oauth_remember_me';
|
||||
import PerformanceBar from './performance_bar';
|
||||
import initBroadcastMessagesForm from './broadcast_message';
|
||||
import initNotes from './init_notes';
|
||||
import initLegacyFilters from './init_legacy_filters';
|
||||
import initIssuableSidebar from './init_issuable_sidebar';
|
||||
|
@ -77,12 +74,21 @@ import initProjectVisibilitySelector from './project_visibility';
|
|||
import GpgBadges from './gpg_badges';
|
||||
import UserFeatureHelper from './helpers/user_feature_helper';
|
||||
import initChangesDropdown from './init_changes_dropdown';
|
||||
import NewGroupChild from './groups/new_group_child';
|
||||
import AbuseReports from './abuse_reports';
|
||||
import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
|
||||
import AjaxLoadingSpinner from './ajax_loading_spinner';
|
||||
import GlFieldErrors from './gl_field_errors';
|
||||
import GLForm from './gl_form';
|
||||
import Shortcuts from './shortcuts';
|
||||
import ShortcutsNavigation from './shortcuts_navigation';
|
||||
import ShortcutsFindFile from './shortcuts_find_file';
|
||||
import ShortcutsIssuable from './shortcuts_issuable';
|
||||
import U2FAuthenticate from './u2f/authenticate';
|
||||
import Members from './members';
|
||||
import memberExpirationDate from './member_expiration_date';
|
||||
import DueDateSelectors from './due_date_select';
|
||||
import Diff from './diff';
|
||||
|
||||
(function() {
|
||||
var Dispatcher;
|
||||
|
@ -166,9 +172,6 @@ import U2FAuthenticate from './u2f/authenticate';
|
|||
const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
|
||||
filteredSearchManager.setup();
|
||||
}
|
||||
if (page === 'projects:merge_requests:index') {
|
||||
new UserCallout({ setCalloutPerProject: true });
|
||||
}
|
||||
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
|
||||
IssuableIndex.init(pagePrefix);
|
||||
|
||||
|
@ -232,11 +235,11 @@ import U2FAuthenticate from './u2f/authenticate';
|
|||
case 'groups:milestones:edit':
|
||||
case 'groups:milestones:update':
|
||||
new ZenMode();
|
||||
new gl.DueDateSelectors();
|
||||
new DueDateSelectors();
|
||||
new GLForm($('.milestone-form'), true);
|
||||
break;
|
||||
case 'projects:compare:show':
|
||||
new gl.Diff();
|
||||
new Diff();
|
||||
initChangesDropdown();
|
||||
break;
|
||||
case 'projects:branches:new':
|
||||
|
@ -272,7 +275,7 @@ import U2FAuthenticate from './u2f/authenticate';
|
|||
}
|
||||
case 'projects:merge_requests:creations:diffs':
|
||||
case 'projects:merge_requests:edit':
|
||||
new gl.Diff();
|
||||
new Diff();
|
||||
shortcut_handler = new ShortcutsNavigation();
|
||||
new GLForm($('.merge-request-form'), true);
|
||||
new IssuableForm($('.merge-request-form'));
|
||||
|
@ -306,7 +309,7 @@ import U2FAuthenticate from './u2f/authenticate';
|
|||
new GLForm($('.release-form'), true);
|
||||
break;
|
||||
case 'projects:merge_requests:show':
|
||||
new gl.Diff();
|
||||
new Diff();
|
||||
shortcut_handler = new ShortcutsIssuable(true);
|
||||
new ZenMode();
|
||||
|
||||
|
@ -322,7 +325,7 @@ import U2FAuthenticate from './u2f/authenticate';
|
|||
new gl.Activities();
|
||||
break;
|
||||
case 'projects:commit:show':
|
||||
new gl.Diff();
|
||||
new Diff();
|
||||
new ZenMode();
|
||||
shortcut_handler = new ShortcutsNavigation();
|
||||
new MiniPipelineGraph({
|
||||
|
@ -350,7 +353,10 @@ import U2FAuthenticate from './u2f/authenticate';
|
|||
case 'projects:show':
|
||||
shortcut_handler = new ShortcutsNavigation();
|
||||
new NotificationsForm();
|
||||
new UserCallout({ setCalloutPerProject: true });
|
||||
new UserCallout({
|
||||
setCalloutPerProject: true,
|
||||
className: 'js-autodevops-banner',
|
||||
});
|
||||
|
||||
if ($('#tree-slider').length) new TreeView();
|
||||
if ($('.blob-viewer').length) new BlobViewer();
|
||||
|
@ -370,9 +376,6 @@ import U2FAuthenticate from './u2f/authenticate';
|
|||
case 'projects:pipelines:new':
|
||||
new NewBranchForm($('.js-new-pipeline-form'));
|
||||
break;
|
||||
case 'projects:pipelines:index':
|
||||
new UserCallout({ setCalloutPerProject: true });
|
||||
break;
|
||||
case 'projects:pipelines:builds':
|
||||
case 'projects:pipelines:failures':
|
||||
case 'projects:pipelines:show':
|
||||
|
@ -393,21 +396,26 @@ import U2FAuthenticate from './u2f/authenticate';
|
|||
new gl.Activities();
|
||||
break;
|
||||
case 'groups:show':
|
||||
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
|
||||
shortcut_handler = new ShortcutsNavigation();
|
||||
new NotificationsForm();
|
||||
new NotificationsDropdown();
|
||||
new ProjectsList();
|
||||
|
||||
if (newGroupChildWrapper) {
|
||||
new NewGroupChild(newGroupChildWrapper);
|
||||
}
|
||||
break;
|
||||
case 'groups:group_members:index':
|
||||
new gl.MemberExpirationDate();
|
||||
new gl.Members();
|
||||
memberExpirationDate();
|
||||
new Members();
|
||||
new UsersSelect();
|
||||
break;
|
||||
case 'projects:project_members:index':
|
||||
new gl.MemberExpirationDate('.js-access-expiration-date-groups');
|
||||
memberExpirationDate('.js-access-expiration-date-groups');
|
||||
new GroupsSelect();
|
||||
new gl.MemberExpirationDate();
|
||||
new gl.Members();
|
||||
memberExpirationDate();
|
||||
new Members();
|
||||
new UsersSelect();
|
||||
break;
|
||||
case 'groups:new':
|
||||
|
@ -416,11 +424,11 @@ import U2FAuthenticate from './u2f/authenticate';
|
|||
case 'admin:groups:create':
|
||||
BindInOut.initAll();
|
||||
new Group();
|
||||
new GroupAvatar();
|
||||
groupAvatar();
|
||||
break;
|
||||
case 'groups:edit':
|
||||
case 'admin:groups:edit':
|
||||
new GroupAvatar();
|
||||
groupAvatar();
|
||||
break;
|
||||
case 'projects:tree:show':
|
||||
shortcut_handler = new ShortcutsNavigation();
|
||||
|
@ -430,7 +438,6 @@ import U2FAuthenticate from './u2f/authenticate';
|
|||
new TreeView();
|
||||
new BlobViewer();
|
||||
new NewCommitForm($('.js-create-dir-form'));
|
||||
new UserCallout({ setCalloutPerProject: true });
|
||||
$('#tree-slider').waitForImages(function() {
|
||||
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
|
||||
});
|
||||
|
@ -468,7 +475,7 @@ import U2FAuthenticate from './u2f/authenticate';
|
|||
const $el = $(el);
|
||||
|
||||
if ($el.find('.dropdown-group-label').length) {
|
||||
new gl.GroupLabelSubscription($el);
|
||||
new GroupLabelSubscription($el);
|
||||
} else {
|
||||
new gl.ProjectLabelSubscription($el);
|
||||
}
|
||||
|
@ -528,7 +535,7 @@ import U2FAuthenticate from './u2f/authenticate';
|
|||
break;
|
||||
case 'profiles:personal_access_tokens:index':
|
||||
case 'admin:impersonation_tokens:index':
|
||||
new gl.DueDateSelectors();
|
||||
new DueDateSelectors();
|
||||
break;
|
||||
case 'projects:clusters:show':
|
||||
import(/* webpackChunkName: "clusters" */ './clusters')
|
||||
|
@ -553,6 +560,9 @@ import U2FAuthenticate from './u2f/authenticate';
|
|||
case 'admin':
|
||||
new Admin();
|
||||
switch (path[1]) {
|
||||
case 'broadcast_messages':
|
||||
initBroadcastMessagesForm();
|
||||
break;
|
||||
case 'cohorts':
|
||||
new UsagePing();
|
||||
break;
|
||||
|
|
|
@ -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 */
|
||||
})
|
||||
|
|
|
@ -40,6 +40,10 @@ const createFlashEl = (message, type, isInContentWrapper = false) => `
|
|||
</div>
|
||||
`;
|
||||
|
||||
const removeFlashClickListener = (flashEl, fadeTransition) => {
|
||||
flashEl.parentNode.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
|
||||
};
|
||||
|
||||
/*
|
||||
* Flash banner supports different types of Flash configurations
|
||||
* along with ability to provide actionConfig which can be used to show
|
||||
|
@ -70,7 +74,7 @@ const createFlash = function createFlash(
|
|||
flashContainer.innerHTML = createFlashEl(message, type, isInContentWrapper);
|
||||
|
||||
const flashEl = flashContainer.querySelector(`.flash-${type}`);
|
||||
flashEl.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
|
||||
removeFlashClickListener(flashEl, fadeTransition);
|
||||
|
||||
if (actionConfig) {
|
||||
flashEl.innerHTML += createAction(actionConfig);
|
||||
|
@ -90,5 +94,6 @@ export {
|
|||
createFlashEl,
|
||||
createAction,
|
||||
hideFlash,
|
||||
removeFlashClickListener,
|
||||
};
|
||||
window.Flash = createFlash;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -4,6 +4,8 @@
|
|||
/* global IssuableContext */
|
||||
/* global Sidebar */
|
||||
|
||||
import DueDateSelectors from './due_date_select';
|
||||
|
||||
export default () => {
|
||||
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
|
||||
|
||||
|
@ -13,6 +15,6 @@ export default () => {
|
|||
new LabelsSelect();
|
||||
new IssuableContext(sidebarOptions.currentUser);
|
||||
gl.Subscription.bindAll('.subscription');
|
||||
new gl.DueDateSelectors();
|
||||
new DueDateSelectors();
|
||||
window.sidebar = new Sidebar();
|
||||
};
|
||||
|
|
|
@ -51,20 +51,19 @@ const PARTICIPANTS_ROW_COUNT = 7;
|
|||
}
|
||||
|
||||
IssuableContext.prototype.initParticipants = function() {
|
||||
$(document).on("click", ".js-participants-more", this.toggleHiddenParticipants);
|
||||
return $(".js-participants-author").each(function(i) {
|
||||
$(document).on('click', '.js-participants-more', this.toggleHiddenParticipants);
|
||||
return $('.js-participants-author').each(function(i) {
|
||||
if (i >= PARTICIPANTS_ROW_COUNT) {
|
||||
return $(this).addClass("js-participants-hidden").hide();
|
||||
return $(this).addClass('js-participants-hidden').hide();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
IssuableContext.prototype.toggleHiddenParticipants = function(e) {
|
||||
var currentText, lessText, originalText;
|
||||
e.preventDefault();
|
||||
currentText = $(this).text().trim();
|
||||
lessText = $(this).data("less-text");
|
||||
originalText = $(this).data("original-text");
|
||||
IssuableContext.prototype.toggleHiddenParticipants = function() {
|
||||
const currentText = $(this).text().trim();
|
||||
const lessText = $(this).data('less-text');
|
||||
const originalText = $(this).data('original-text');
|
||||
|
||||
if (currentText === originalText) {
|
||||
$(this).text(lessText);
|
||||
|
||||
|
@ -73,7 +72,7 @@ const PARTICIPANTS_ROW_COUNT = 7;
|
|||
$(this).text(originalText);
|
||||
}
|
||||
|
||||
$(".js-participants-hidden").toggle();
|
||||
$('.js-participants-hidden').toggle();
|
||||
};
|
||||
|
||||
return IssuableContext;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
|
||||
/* global GitLab */
|
||||
/* global dateFormat */
|
||||
|
||||
import Pikaday from 'pikaday';
|
||||
import Autosave from './autosave';
|
||||
import UsersSelect from './users_select';
|
||||
import GfmAutoComplete from './gfm_auto_complete';
|
||||
import ZenMode from './zen_mode';
|
||||
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
|
||||
|
||||
(function() {
|
||||
this.IssuableForm = (function() {
|
||||
|
@ -38,11 +38,13 @@ import ZenMode from './zen_mode';
|
|||
theme: 'gitlab-theme animate-picker',
|
||||
format: 'yyyy-mm-dd',
|
||||
container: $issuableDueDate.parent().get(0),
|
||||
parse: dateString => parsePikadayDate(dateString),
|
||||
toString: date => pikadayToString(date),
|
||||
onSelect: function(dateText) {
|
||||
$issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
|
||||
$issuableDueDate.val(calendar.toString(dateText));
|
||||
}
|
||||
});
|
||||
calendar.setDate(new Date($issuableDueDate.val()));
|
||||
calendar.setDate(parsePikadayDate($issuableDueDate.val()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -43,16 +43,6 @@
|
|||
type: 'link',
|
||||
});
|
||||
}
|
||||
|
||||
if (this.job.retry_path) {
|
||||
actions.push({
|
||||
label: 'Retry',
|
||||
path: this.job.retry_path,
|
||||
cssClass: 'js-retry-button btn btn-inverted-secondary visible-md-block visible-lg-block',
|
||||
type: 'ujs-link',
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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}`;
|
||||
};
|
||||
|
|
|
@ -85,7 +85,7 @@ w.gl.utils.getLocationHash = function(url) {
|
|||
return hashIndex === -1 ? null : url.substring(hashIndex + 1);
|
||||
};
|
||||
|
||||
w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
|
||||
w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(window.location.href);
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export function visitUrl(url, external = false) {
|
||||
|
@ -96,7 +96,7 @@ export function visitUrl(url, external = false) {
|
|||
otherWindow.opener = null;
|
||||
otherWindow.location = url;
|
||||
} else {
|
||||
document.location.href = url;
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,15 +21,6 @@ window._ = _;
|
|||
window.Dropzone = Dropzone;
|
||||
window.Sortable = Sortable;
|
||||
|
||||
// shortcuts
|
||||
import './shortcuts';
|
||||
import './shortcuts_blob';
|
||||
import './shortcuts_dashboard_navigation';
|
||||
import './shortcuts_navigation';
|
||||
import './shortcuts_find_file';
|
||||
import './shortcuts_issuable';
|
||||
import './shortcuts_network';
|
||||
|
||||
// templates
|
||||
import './templates/issuable_template_selector';
|
||||
import './templates/issuable_template_selectors';
|
||||
|
@ -52,24 +43,17 @@ import './admin';
|
|||
import './aside';
|
||||
import loadAwardsHandler from './awards_handler';
|
||||
import bp from './breakpoints';
|
||||
import './broadcast_message';
|
||||
import './commits';
|
||||
import './compare';
|
||||
import './compare_autocomplete';
|
||||
import './confirm_danger_modal';
|
||||
import './copy_as_gfm';
|
||||
import './copy_to_clipboard';
|
||||
import './diff';
|
||||
import './dropzone_input';
|
||||
import './due_date_select';
|
||||
import './files_comment_button';
|
||||
import Flash from './flash';
|
||||
import Flash, { removeFlashClickListener } from './flash';
|
||||
import './gl_dropdown';
|
||||
import './gl_field_error';
|
||||
import './gl_field_errors';
|
||||
import './gl_form';
|
||||
import './group_avatar';
|
||||
import './group_label_subscription';
|
||||
import './groups_select';
|
||||
import './header';
|
||||
import './importer_status';
|
||||
|
@ -83,8 +67,6 @@ import './layout_nav';
|
|||
import LazyLoader from './lazy_loader';
|
||||
import './line_highlighter';
|
||||
import './logo';
|
||||
import './member_expiration_date';
|
||||
import './members';
|
||||
import './merge_request';
|
||||
import './merge_request_tabs';
|
||||
import './milestone';
|
||||
|
@ -338,4 +320,10 @@ $(function () {
|
|||
event.preventDefault();
|
||||
gl.utils.visitUrl(`${action}${$(this).serialize()}`);
|
||||
});
|
||||
|
||||
const flashContainer = document.querySelector('.flash-container');
|
||||
|
||||
if (flashContainer && flashContainer.children.length) {
|
||||
removeFlashClickListener(flashContainer.children[0]);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,55 +1,53 @@
|
|||
/* global dateFormat */
|
||||
|
||||
import Pikaday from 'pikaday';
|
||||
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
|
||||
|
||||
(() => {
|
||||
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
|
||||
// children of an element with the `clearable-input` class, and have a sibling
|
||||
// `js-clear-input` element, then show that element when there is a value in the
|
||||
// datepicker, and make clicking on that element clear the field.
|
||||
//
|
||||
window.gl = window.gl || {};
|
||||
gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => {
|
||||
function toggleClearInput() {
|
||||
$(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
|
||||
}
|
||||
const inputs = $(selector);
|
||||
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
|
||||
// children of an element with the `clearable-input` class, and have a sibling
|
||||
// `js-clear-input` element, then show that element when there is a value in the
|
||||
// datepicker, and make clicking on that element clear the field.
|
||||
//
|
||||
export default function memberExpirationDate(selector = '.js-access-expiration-date') {
|
||||
function toggleClearInput() {
|
||||
$(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
|
||||
}
|
||||
const inputs = $(selector);
|
||||
|
||||
inputs.each((i, el) => {
|
||||
const $input = $(el);
|
||||
inputs.each((i, el) => {
|
||||
const $input = $(el);
|
||||
|
||||
const calendar = new Pikaday({
|
||||
field: $input.get(0),
|
||||
theme: 'gitlab-theme animate-picker',
|
||||
format: 'yyyy-mm-dd',
|
||||
minDate: new Date(),
|
||||
container: $input.parent().get(0),
|
||||
onSelect(dateText) {
|
||||
$input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
|
||||
const calendar = new Pikaday({
|
||||
field: $input.get(0),
|
||||
theme: 'gitlab-theme animate-picker',
|
||||
format: 'yyyy-mm-dd',
|
||||
minDate: new Date(),
|
||||
container: $input.parent().get(0),
|
||||
parse: dateString => parsePikadayDate(dateString),
|
||||
toString: date => pikadayToString(date),
|
||||
onSelect(dateText) {
|
||||
$input.val(calendar.toString(dateText));
|
||||
|
||||
$input.trigger('change');
|
||||
$input.trigger('change');
|
||||
|
||||
toggleClearInput.call($input);
|
||||
},
|
||||
});
|
||||
|
||||
calendar.setDate(new Date($input.val()));
|
||||
$input.data('pikaday', calendar);
|
||||
toggleClearInput.call($input);
|
||||
},
|
||||
});
|
||||
|
||||
inputs.next('.js-clear-input').on('click', function clicked(event) {
|
||||
event.preventDefault();
|
||||
calendar.setDate(parsePikadayDate($input.val()));
|
||||
$input.data('pikaday', calendar);
|
||||
});
|
||||
|
||||
const input = $(this).closest('.clearable-input').find(selector);
|
||||
const calendar = input.data('pikaday');
|
||||
inputs.next('.js-clear-input').on('click', function clicked(event) {
|
||||
event.preventDefault();
|
||||
|
||||
calendar.setDate(null);
|
||||
input.trigger('change');
|
||||
toggleClearInput.call(input);
|
||||
});
|
||||
const input = $(this).closest('.clearable-input').find(selector);
|
||||
const calendar = input.data('pikaday');
|
||||
|
||||
inputs.on('blur', toggleClearInput);
|
||||
calendar.setDate(null);
|
||||
input.trigger('change');
|
||||
toggleClearInput.call(input);
|
||||
});
|
||||
|
||||
inputs.each(toggleClearInput);
|
||||
};
|
||||
}).call(window);
|
||||
inputs.on('blur', toggleClearInput);
|
||||
|
||||
inputs.each(toggleClearInput);
|
||||
}
|
||||
|
|
|
@ -1,81 +1,74 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
export default class Members {
|
||||
constructor() {
|
||||
this.addListeners();
|
||||
this.initGLDropdown();
|
||||
}
|
||||
|
||||
class Members {
|
||||
constructor() {
|
||||
this.addListeners();
|
||||
this.initGLDropdown();
|
||||
}
|
||||
addListeners() {
|
||||
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
|
||||
$('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
|
||||
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
|
||||
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
|
||||
}
|
||||
|
||||
addListeners() {
|
||||
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
|
||||
$('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
|
||||
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
|
||||
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
|
||||
}
|
||||
initGLDropdown() {
|
||||
$('.js-member-permissions-dropdown').each((i, btn) => {
|
||||
const $btn = $(btn);
|
||||
|
||||
initGLDropdown() {
|
||||
$('.js-member-permissions-dropdown').each((i, btn) => {
|
||||
const $btn = $(btn);
|
||||
|
||||
$btn.glDropdown({
|
||||
selectable: true,
|
||||
isSelectable(selected, $el) {
|
||||
return !$el.hasClass('is-active');
|
||||
},
|
||||
fieldName: $btn.data('field-name'),
|
||||
id(selected, $el) {
|
||||
return $el.data('id');
|
||||
},
|
||||
toggleLabel(selected, $el) {
|
||||
return $el.text();
|
||||
},
|
||||
clicked: (options) => {
|
||||
this.formSubmit(null, options.$el);
|
||||
},
|
||||
});
|
||||
$btn.glDropdown({
|
||||
selectable: true,
|
||||
isSelectable(selected, $el) {
|
||||
return !$el.hasClass('is-active');
|
||||
},
|
||||
fieldName: $btn.data('field-name'),
|
||||
id(selected, $el) {
|
||||
return $el.data('id');
|
||||
},
|
||||
toggleLabel(selected, $el) {
|
||||
return $el.text();
|
||||
},
|
||||
clicked: (options) => {
|
||||
this.formSubmit(null, options.$el);
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
removeRow(e) {
|
||||
const $target = $(e.target);
|
||||
|
||||
removeRow(e) {
|
||||
const $target = $(e.target);
|
||||
|
||||
if ($target.hasClass('btn-remove')) {
|
||||
$target.closest('.member')
|
||||
.fadeOut(function fadeOutMemberRow() {
|
||||
$(this).remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
formSubmit(e, $el = null) {
|
||||
const $this = e ? $(e.currentTarget) : $el;
|
||||
const { $toggle, $dateInput } = this.getMemberListItems($this);
|
||||
|
||||
$this.closest('form').trigger('submit.rails');
|
||||
|
||||
$toggle.disable();
|
||||
$dateInput.disable();
|
||||
}
|
||||
|
||||
formSuccess(e) {
|
||||
const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member'));
|
||||
|
||||
$toggle.enable();
|
||||
$dateInput.enable();
|
||||
}
|
||||
|
||||
getMemberListItems($el) {
|
||||
const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`);
|
||||
|
||||
return {
|
||||
$memberListItem,
|
||||
$toggle: $memberListItem.find('.dropdown-menu-toggle'),
|
||||
$dateInput: $memberListItem.find('.js-access-expiration-date'),
|
||||
};
|
||||
if ($target.hasClass('btn-remove')) {
|
||||
$target.closest('.member')
|
||||
.fadeOut(function fadeOutMemberRow() {
|
||||
$(this).remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
gl.Members = Members;
|
||||
})();
|
||||
formSubmit(e, $el = null) {
|
||||
const $this = e ? $(e.currentTarget) : $el;
|
||||
const { $toggle, $dateInput } = this.getMemberListItems($this);
|
||||
|
||||
$this.closest('form').trigger('submit.rails');
|
||||
|
||||
$toggle.disable();
|
||||
$dateInput.disable();
|
||||
}
|
||||
|
||||
formSuccess(e) {
|
||||
const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member'));
|
||||
|
||||
$toggle.enable();
|
||||
$dateInput.enable();
|
||||
}
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getMemberListItems($el) {
|
||||
const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`);
|
||||
|
||||
return {
|
||||
$memberListItem,
|
||||
$toggle: $memberListItem.find('.dropdown-menu-toggle'),
|
||||
$dateInput: $memberListItem.find('.js-access-expiration-date'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@ import {
|
|||
handleLocationHash,
|
||||
isMetaClick,
|
||||
} from './lib/utils/common_utils';
|
||||
|
||||
import initDiscussionTab from './image_diff/init_discussion_tab';
|
||||
import Diff from './diff';
|
||||
|
||||
/* eslint-disable max-len */
|
||||
// MergeRequestTabs
|
||||
|
@ -292,7 +292,7 @@ import initDiscussionTab from './image_diff/init_discussion_tab';
|
|||
}
|
||||
this.diffsLoaded = true;
|
||||
|
||||
new gl.Diff();
|
||||
new Diff();
|
||||
this.scrollToElement('#diffs');
|
||||
|
||||
$('.diff-file').each((i, el) => {
|
||||
|
|
|
@ -146,7 +146,9 @@ import _ from 'underscore';
|
|||
clicked: function(options) {
|
||||
const { $el, e } = options;
|
||||
let selected = options.selectedObj;
|
||||
|
||||
var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
|
||||
if (!selected) return;
|
||||
page = $('body').attr('data-page');
|
||||
isIssueIndex = page === 'projects:issues:index';
|
||||
isMRIndex = (page === page && page === 'projects:merge_requests:index');
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */
|
||||
/* global ShortcutsNetwork */
|
||||
|
||||
import ShortcutsNetwork from '../shortcuts_network';
|
||||
import Network from './network';
|
||||
|
||||
$(function() {
|
||||
|
|
|
@ -13,7 +13,6 @@ import $ from 'jquery';
|
|||
import _ from 'underscore';
|
||||
import Cookies from 'js-cookie';
|
||||
import autosize from 'vendor/autosize';
|
||||
import Dropzone from 'dropzone';
|
||||
import 'vendor/jquery.caret'; // required by jquery.atwho
|
||||
import 'vendor/jquery.atwho';
|
||||
import AjaxCache from '~/lib/utils/ajax_cache';
|
||||
|
@ -22,13 +21,11 @@ import CommentTypeToggle from './comment_type_toggle';
|
|||
import GLForm from './gl_form';
|
||||
import loadAwardsHandler from './awards_handler';
|
||||
import Autosave from './autosave';
|
||||
import './dropzone_input';
|
||||
import TaskList from './task_list';
|
||||
import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
|
||||
import imageDiffHelper from './image_diff/helpers/index';
|
||||
|
||||
window.autosize = autosize;
|
||||
window.Dropzone = Dropzone;
|
||||
|
||||
function normalizeNewlines(str) {
|
||||
return str.replace(/\r\n/g, '\n');
|
||||
|
@ -1283,10 +1280,12 @@ export default class Notes {
|
|||
* Get data from Form attributes to use for saving/submitting comment.
|
||||
*/
|
||||
getFormData($form) {
|
||||
const content = $form.find('.js-note-text').val();
|
||||
return {
|
||||
formData: $form.serialize(),
|
||||
formContent: _.escape($form.find('.js-note-text').val()),
|
||||
formContent: _.escape(content),
|
||||
formAction: $form.attr('action'),
|
||||
formContentOriginal: content,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1418,7 +1417,7 @@ export default class Notes {
|
|||
const isMainForm = $form.hasClass('js-main-target-form');
|
||||
const isDiscussionForm = $form.hasClass('js-discussion-note-form');
|
||||
const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button');
|
||||
const { formData, formContent, formAction } = this.getFormData($form);
|
||||
const { formData, formContent, formAction, formContentOriginal } = this.getFormData($form);
|
||||
let noteUniqueId;
|
||||
let systemNoteUniqueId;
|
||||
let hasQuickActions = false;
|
||||
|
@ -1577,7 +1576,7 @@ export default class Notes {
|
|||
$form = $notesContainer.parent().find('form');
|
||||
}
|
||||
|
||||
$form.find('.js-note-text').val(formContent);
|
||||
$form.find('.js-note-text').val(formContentOriginal);
|
||||
this.reenableTargetFormSubmitButton(e);
|
||||
this.addNoteError($form);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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,115 @@
|
|||
<script>
|
||||
import flash, { hideFlash } from '../../flash';
|
||||
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
loadingIcon,
|
||||
},
|
||||
props: {
|
||||
currentBranch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
branchName: '',
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
btnDisabled() {
|
||||
return this.loading || this.branchName === '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleDropdown() {
|
||||
this.$dropdown.dropdown('toggle');
|
||||
},
|
||||
submitNewBranch() {
|
||||
// need to query as the element is appended outside of Vue
|
||||
const flashEl = this.$refs.flashContainer.querySelector('.flash-alert');
|
||||
|
||||
this.loading = true;
|
||||
|
||||
if (flashEl) {
|
||||
hideFlash(flashEl, false);
|
||||
}
|
||||
|
||||
eventHub.$emit('createNewBranch', this.branchName);
|
||||
},
|
||||
showErrorMessage(message) {
|
||||
this.loading = false;
|
||||
flash(message, 'alert', this.$el);
|
||||
},
|
||||
createdNewBranch(newBranchName) {
|
||||
this.loading = false;
|
||||
this.branchName = '';
|
||||
|
||||
if (this.dropdownText) {
|
||||
this.dropdownText.textContent = newBranchName;
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// Dropdown is outside of Vue instance & is controlled by Bootstrap
|
||||
this.$dropdown = $('.git-revision-dropdown');
|
||||
|
||||
// text element is outside Vue app
|
||||
this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text');
|
||||
|
||||
eventHub.$on('createNewBranchSuccess', this.createdNewBranch);
|
||||
eventHub.$on('createNewBranchError', this.showErrorMessage);
|
||||
eventHub.$on('toggleNewBranchDropdown', this.toggleDropdown);
|
||||
},
|
||||
destroyed() {
|
||||
eventHub.$off('createNewBranchSuccess', this.createdNewBranch);
|
||||
eventHub.$off('toggleNewBranchDropdown', this.toggleDropdown);
|
||||
eventHub.$off('createNewBranchError', this.showErrorMessage);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="flash-container"
|
||||
ref="flashContainer"
|
||||
>
|
||||
</div>
|
||||
<p>
|
||||
Create from:
|
||||
<code>{{ currentBranch }}</code>
|
||||
</p>
|
||||
<input
|
||||
class="form-control js-new-branch-name"
|
||||
type="text"
|
||||
placeholder="Name new branch"
|
||||
v-model="branchName"
|
||||
@keyup.enter.stop.prevent="submitNewBranch"
|
||||
/>
|
||||
<div class="prepend-top-default clearfix">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary pull-left"
|
||||
:disabled="btnDisabled"
|
||||
@click.stop.prevent="submitNewBranch"
|
||||
>
|
||||
<loading-icon
|
||||
v-if="loading"
|
||||
:inline="true"
|
||||
/>
|
||||
<span>Create</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default pull-right"
|
||||
@click.stop.prevent="toggleDropdown"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -8,10 +8,14 @@ import RepoMixin from '../mixins/repo_mixin';
|
|||
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
|
||||
import Store from '../stores/repo_store';
|
||||
import Helper from '../helpers/repo_helper';
|
||||
import Service from '../services/repo_service';
|
||||
import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
data: () => Store,
|
||||
data() {
|
||||
return Store;
|
||||
},
|
||||
mixins: [RepoMixin],
|
||||
components: {
|
||||
RepoSidebar,
|
||||
|
@ -22,12 +26,19 @@ export default {
|
|||
PopupDialog,
|
||||
RepoPreview,
|
||||
},
|
||||
|
||||
created() {
|
||||
eventHub.$on('createNewBranch', this.createNewBranch);
|
||||
},
|
||||
mounted() {
|
||||
Helper.getContent().catch(Helper.loadingError);
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
eventHub.$off('createNewBranch', this.createNewBranch);
|
||||
},
|
||||
methods: {
|
||||
getCurrentLocation() {
|
||||
return location.href;
|
||||
},
|
||||
toggleDialogOpen(toggle) {
|
||||
this.dialog.open = toggle;
|
||||
},
|
||||
|
@ -36,8 +47,25 @@ export default {
|
|||
this.toggleDialogOpen(false);
|
||||
this.dialog.status = status;
|
||||
},
|
||||
|
||||
toggleBlobView: Store.toggleBlobView,
|
||||
createNewBranch(branch) {
|
||||
Service.createBranch({
|
||||
branch,
|
||||
ref: Store.currentBranch,
|
||||
}).then((res) => {
|
||||
const newBranchName = res.data.name;
|
||||
const newUrl = this.getCurrentLocation().replace(Store.currentBranch, newBranchName);
|
||||
|
||||
Store.currentBranch = newBranchName;
|
||||
|
||||
history.pushState({ key: Helper.key }, '', newUrl);
|
||||
|
||||
eventHub.$emit('createNewBranchSuccess', newBranchName);
|
||||
eventHub.$emit('toggleNewBranchDropdown');
|
||||
}).catch((err) => {
|
||||
eventHub.$emit('createNewBranchError', err.response.data.message);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -3,12 +3,20 @@ import Flash from '../../flash';
|
|||
import Store from '../stores/repo_store';
|
||||
import RepoMixin from '../mixins/repo_mixin';
|
||||
import Service from '../services/repo_service';
|
||||
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
|
||||
import { visitUrl } from '../../lib/utils/url_utility';
|
||||
|
||||
export default {
|
||||
data: () => Store,
|
||||
|
||||
mixins: [RepoMixin],
|
||||
|
||||
data() {
|
||||
return Store;
|
||||
},
|
||||
|
||||
components: {
|
||||
PopupDialog,
|
||||
},
|
||||
|
||||
computed: {
|
||||
showCommitable() {
|
||||
return this.isCommitable && this.changedFiles.length;
|
||||
|
@ -28,7 +36,16 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
makeCommit() {
|
||||
commitToNewBranch(status) {
|
||||
if (status) {
|
||||
this.showNewBranchDialog = false;
|
||||
this.tryCommit(null, true, true);
|
||||
} else {
|
||||
// reset the state
|
||||
}
|
||||
},
|
||||
|
||||
makeCommit(newBranch) {
|
||||
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
|
||||
const commitMessage = this.commitMessage;
|
||||
const actions = this.changedFiles.map(f => ({
|
||||
|
@ -36,19 +53,63 @@ export default {
|
|||
file_path: f.path,
|
||||
content: f.newContent,
|
||||
}));
|
||||
const branch = newBranch ? `${this.currentBranch}-${this.currentShortHash}` : this.currentBranch;
|
||||
const payload = {
|
||||
branch: Store.currentBranch,
|
||||
branch,
|
||||
commit_message: commitMessage,
|
||||
actions,
|
||||
};
|
||||
Store.submitCommitsLoading = true;
|
||||
if (newBranch) {
|
||||
payload.start_branch = this.currentBranch;
|
||||
}
|
||||
this.submitCommitsLoading = true;
|
||||
Service.commitFiles(payload)
|
||||
.then(this.resetCommitState)
|
||||
.catch(() => Flash('An error occurred while committing your changes'));
|
||||
.then(() => {
|
||||
this.resetCommitState();
|
||||
if (this.startNewMR) {
|
||||
this.redirectToNewMr(branch);
|
||||
} else {
|
||||
this.redirectToBranch(branch);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
Flash('An error occurred while committing your changes');
|
||||
});
|
||||
},
|
||||
|
||||
tryCommit(e, skipBranchCheck = false, newBranch = false) {
|
||||
if (skipBranchCheck) {
|
||||
this.makeCommit(newBranch);
|
||||
} else {
|
||||
Store.setBranchHash()
|
||||
.then(() => {
|
||||
if (Store.branchChanged) {
|
||||
Store.showNewBranchDialog = true;
|
||||
return;
|
||||
}
|
||||
this.makeCommit(newBranch);
|
||||
})
|
||||
.catch(() => {
|
||||
Flash('An error occurred while committing your changes');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
redirectToNewMr(branch) {
|
||||
visitUrl(this.newMrTemplateUrl.replace('{{source_branch}}', branch));
|
||||
},
|
||||
|
||||
redirectToBranch(branch) {
|
||||
visitUrl(this.customBranchURL.replace('{{branch}}', branch));
|
||||
},
|
||||
|
||||
resetCommitState() {
|
||||
this.submitCommitsLoading = false;
|
||||
this.openedFiles = this.openedFiles.map((file) => {
|
||||
const f = file;
|
||||
f.changed = false;
|
||||
return f;
|
||||
});
|
||||
this.changedFiles = [];
|
||||
this.commitMessage = '';
|
||||
this.editMode = false;
|
||||
|
@ -62,9 +123,17 @@ export default {
|
|||
<div
|
||||
v-if="showCommitable"
|
||||
id="commit-area">
|
||||
<popup-dialog
|
||||
v-if="showNewBranchDialog"
|
||||
:primary-button-label="__('Create new branch')"
|
||||
kind="primary"
|
||||
:title="__('Branch has changed')"
|
||||
:text="__('This branch has changed since you started editing. Would you like to create a new branch?')"
|
||||
@submit="commitToNewBranch"
|
||||
/>
|
||||
<form
|
||||
class="form-horizontal"
|
||||
@submit.prevent="makeCommit">
|
||||
@submit.prevent="tryCommit">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label staged-files">
|
||||
|
@ -117,7 +186,7 @@ export default {
|
|||
class="btn btn-success">
|
||||
<i
|
||||
v-if="submitCommitsLoading"
|
||||
class="fa fa-spinner fa-spin"
|
||||
class="js-commit-loading-icon fa fa-spinner fa-spin"
|
||||
aria-hidden="true"
|
||||
aria-label="loading">
|
||||
</i>
|
||||
|
@ -126,6 +195,14 @@ export default {
|
|||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-offset-4 col-md-6">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" v-model="startNewMR">
|
||||
<span>Start a <strong>new merge request</strong> with these changes</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,9 @@ import Store from '../stores/repo_store';
|
|||
import RepoMixin from '../mixins/repo_mixin';
|
||||
|
||||
export default {
|
||||
data: () => Store,
|
||||
data() {
|
||||
return Store;
|
||||
},
|
||||
mixins: [RepoMixin],
|
||||
computed: {
|
||||
buttonLabel() {
|
||||
|
|
|
@ -5,7 +5,9 @@ import Service from '../services/repo_service';
|
|||
import Helper from '../helpers/repo_helper';
|
||||
|
||||
const RepoEditor = {
|
||||
data: () => Store,
|
||||
data() {
|
||||
return Store;
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
if (Helper.monacoInstance) {
|
||||
|
@ -22,7 +24,8 @@ const RepoEditor = {
|
|||
const monacoInstance = Helper.monaco.editor.create(this.$el, {
|
||||
model: null,
|
||||
readOnly: false,
|
||||
contextmenu: false,
|
||||
contextmenu: true,
|
||||
scrollBeyondLastLine: false,
|
||||
});
|
||||
|
||||
Helper.monacoInstance = monacoInstance;
|
||||
|
@ -92,7 +95,7 @@ const RepoEditor = {
|
|||
},
|
||||
|
||||
blobRaw() {
|
||||
if (Helper.monacoInstance && !this.isTree) {
|
||||
if (Helper.monacoInstance) {
|
||||
this.setupEditor();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,107 +1,95 @@
|
|||
<script>
|
||||
import TimeAgoMixin from '../../vue_shared/mixins/timeago';
|
||||
import timeAgoMixin from '../../vue_shared/mixins/timeago';
|
||||
import eventHub from '../event_hub';
|
||||
import repoMixin from '../mixins/repo_mixin';
|
||||
|
||||
const RepoFile = {
|
||||
mixins: [TimeAgoMixin],
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
export default {
|
||||
mixins: [
|
||||
repoMixin,
|
||||
timeAgoMixin,
|
||||
],
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
isMini: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
computed: {
|
||||
fileIcon() {
|
||||
const classObj = {
|
||||
'fa-spinner fa-spin': this.file.loading,
|
||||
[this.file.icon]: !this.file.loading,
|
||||
'fa-folder-open': !this.file.loading && this.file.opened,
|
||||
};
|
||||
return classObj;
|
||||
},
|
||||
levelIndentation() {
|
||||
return {
|
||||
marginLeft: `${this.file.level * 16}px`,
|
||||
};
|
||||
},
|
||||
shortId() {
|
||||
return this.file.id.substr(0, 8);
|
||||
},
|
||||
},
|
||||
loading: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default() { return { tree: false }; },
|
||||
methods: {
|
||||
linkClicked(file) {
|
||||
eventHub.$emit('fileNameClicked', file);
|
||||
},
|
||||
},
|
||||
hasFiles: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
activeFile: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
canShowFile() {
|
||||
return !this.loading.tree || this.hasFiles;
|
||||
},
|
||||
|
||||
fileIcon() {
|
||||
const classObj = {
|
||||
'fa-spinner fa-spin': this.file.loading,
|
||||
[this.file.icon]: !this.file.loading,
|
||||
};
|
||||
return classObj;
|
||||
},
|
||||
|
||||
fileIndentation() {
|
||||
return {
|
||||
'margin-left': `${this.file.level * 10}px`,
|
||||
};
|
||||
},
|
||||
|
||||
activeFileClass() {
|
||||
return {
|
||||
active: this.activeFile.url === this.file.url,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
linkClicked(file) {
|
||||
this.$emit('linkclicked', file);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default RepoFile;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr
|
||||
v-if="canShowFile"
|
||||
class="file"
|
||||
:class="activeFileClass"
|
||||
@click.prevent="linkClicked(file)">
|
||||
<td>
|
||||
<i
|
||||
class="fa fa-fw file-icon"
|
||||
:class="fileIcon"
|
||||
:style="fileIndentation"
|
||||
aria-label="file icon">
|
||||
</i>
|
||||
<a
|
||||
:href="file.url"
|
||||
class="repo-file-name"
|
||||
:title="file.url">
|
||||
{{file.name}}
|
||||
</a>
|
||||
</td>
|
||||
<tr
|
||||
class="file"
|
||||
@click.prevent="linkClicked(file)">
|
||||
<td>
|
||||
<i
|
||||
class="fa fa-fw file-icon"
|
||||
:class="fileIcon"
|
||||
:style="levelIndentation"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</i>
|
||||
<a
|
||||
:href="file.url"
|
||||
class="repo-file-name"
|
||||
>
|
||||
{{ file.name }}
|
||||
</a>
|
||||
<template v-if="file.type === 'submodule' && file.id">
|
||||
@
|
||||
<span class="commit-sha">
|
||||
<a
|
||||
@click.stop
|
||||
:href="file.tree_url"
|
||||
>
|
||||
{{ shortId }}
|
||||
</a>
|
||||
</span>
|
||||
</template>
|
||||
</td>
|
||||
|
||||
<template v-if="!isMini">
|
||||
<td class="hidden-sm hidden-xs">
|
||||
<div class="commit-message">
|
||||
<a @click.stop :href="file.lastCommitUrl">
|
||||
{{file.lastCommitMessage}}
|
||||
<template v-if="!isMini">
|
||||
<td class="hidden-sm hidden-xs">
|
||||
<a
|
||||
@click.stop
|
||||
:href="file.lastCommit.url"
|
||||
class="commit-message"
|
||||
>
|
||||
{{ file.lastCommit.message }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</td>
|
||||
|
||||
<td class="hidden-xs text-right">
|
||||
<span
|
||||
class="commit-update"
|
||||
:title="tooltipTitle(file.lastCommitUpdate)">
|
||||
{{timeFormated(file.lastCommitUpdate)}}
|
||||
</span>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
<td class="commit-update hidden-xs text-right">
|
||||
<span
|
||||
v-if="file.lastCommit.updatedAt"
|
||||
:title="tooltipTitle(file.lastCommit.updatedAt)"
|
||||
>
|
||||
{{ timeFormated(file.lastCommit.updatedAt) }}
|
||||
</span>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</template>
|
||||
|
|
|
@ -4,7 +4,9 @@ import Helper from '../helpers/repo_helper';
|
|||
import RepoMixin from '../mixins/repo_mixin';
|
||||
|
||||
const RepoFileButtons = {
|
||||
data: () => Store,
|
||||
data() {
|
||||
return Store;
|
||||
},
|
||||
|
||||
mixins: [RepoMixin],
|
||||
|
||||
|
|
|
@ -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,23 @@
|
|||
<script>
|
||||
const RepoLoadingFile = {
|
||||
props: {
|
||||
loading: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: {},
|
||||
},
|
||||
hasFiles: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isMini: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
import repoMixin from '../mixins/repo_mixin';
|
||||
|
||||
computed: {
|
||||
showGhostLines() {
|
||||
return this.loading.tree && !this.hasFiles;
|
||||
export default {
|
||||
mixins: [
|
||||
repoMixin,
|
||||
],
|
||||
methods: {
|
||||
lineOfCode(n) {
|
||||
return `skeleton-line-${n}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
lineOfCode(n) {
|
||||
return `skeleton-line-${n}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default RepoLoadingFile;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr
|
||||
v-if="showGhostLines"
|
||||
class="loading-file">
|
||||
class="loading-file"
|
||||
aria-label="Loading files"
|
||||
>
|
||||
<td>
|
||||
<div
|
||||
class="animation-container animation-container-small">
|
||||
|
@ -48,29 +28,28 @@ export default RepoLoadingFile;
|
|||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td
|
||||
v-if="!isMini"
|
||||
class="hidden-sm hidden-xs">
|
||||
<div class="animation-container">
|
||||
<div
|
||||
v-for="n in 6"
|
||||
:key="n"
|
||||
:class="lineOfCode(n)">
|
||||
<template v-if="!isMini">
|
||||
<td
|
||||
class="hidden-sm hidden-xs">
|
||||
<div class="animation-container">
|
||||
<div
|
||||
v-for="n in 6"
|
||||
:key="n"
|
||||
:class="lineOfCode(n)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</td>
|
||||
|
||||
<td
|
||||
v-if="!isMini"
|
||||
class="hidden-xs">
|
||||
<div class="animation-container animation-container-small">
|
||||
<div
|
||||
v-for="n in 6"
|
||||
:key="n"
|
||||
:class="lineOfCode(n)">
|
||||
<td
|
||||
class="hidden-xs">
|
||||
<div class="animation-container animation-container-small animation-container-right">
|
||||
<div
|
||||
v-for="n in 6"
|
||||
:key="n"
|
||||
:class="lineOfCode(n)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</template>
|
||||
|
|
|
@ -1,38 +1,38 @@
|
|||
<script>
|
||||
import RepoMixin from '../mixins/repo_mixin';
|
||||
import eventHub from '../event_hub';
|
||||
import repoMixin from '../mixins/repo_mixin';
|
||||
|
||||
const RepoPreviousDirectory = {
|
||||
props: {
|
||||
prevUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
export default {
|
||||
mixins: [
|
||||
repoMixin,
|
||||
],
|
||||
props: {
|
||||
prevUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
mixins: [RepoMixin],
|
||||
|
||||
computed: {
|
||||
colSpanCondition() {
|
||||
return this.isMini ? undefined : 3;
|
||||
computed: {
|
||||
colSpanCondition() {
|
||||
return this.isMini ? undefined : 3;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
linkClicked(file) {
|
||||
this.$emit('linkclicked', file);
|
||||
methods: {
|
||||
linkClicked(file) {
|
||||
eventHub.$emit('goToPreviousDirectoryClicked', file);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default RepoPreviousDirectory;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr class="prev-directory">
|
||||
<td
|
||||
:colspan="colSpanCondition"
|
||||
@click.prevent="linkClicked(prevUrl)">
|
||||
<a :href="prevUrl">..</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="file prev-directory">
|
||||
<td
|
||||
:colspan="colSpanCondition"
|
||||
class="table-cell"
|
||||
@click.prevent="linkClicked(prevUrl)"
|
||||
>
|
||||
<a :href="prevUrl">...</a>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
import Store from '../stores/repo_store';
|
||||
|
||||
export default {
|
||||
data: () => Store,
|
||||
data() {
|
||||
return Store;
|
||||
},
|
||||
computed: {
|
||||
html() {
|
||||
return this.activeFile.html;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<script>
|
||||
import _ from 'underscore';
|
||||
import Service from '../services/repo_service';
|
||||
import Helper from '../helpers/repo_helper';
|
||||
import Store from '../stores/repo_store';
|
||||
import eventHub from '../event_hub';
|
||||
import RepoPreviousDirectory from './repo_prev_directory.vue';
|
||||
import RepoFileOptions from './repo_file_options.vue';
|
||||
import RepoFile from './repo_file.vue';
|
||||
import RepoLoadingFile from './repo_loading_file.vue';
|
||||
import RepoMixin from '../mixins/repo_mixin';
|
||||
|
@ -11,21 +12,35 @@ import RepoMixin from '../mixins/repo_mixin';
|
|||
export default {
|
||||
mixins: [RepoMixin],
|
||||
components: {
|
||||
'repo-file-options': RepoFileOptions,
|
||||
'repo-previous-directory': RepoPreviousDirectory,
|
||||
'repo-file': RepoFile,
|
||||
'repo-loading-file': RepoLoadingFile,
|
||||
},
|
||||
|
||||
created() {
|
||||
window.addEventListener('popstate', this.checkHistory);
|
||||
},
|
||||
destroyed() {
|
||||
eventHub.$off('fileNameClicked', this.fileClicked);
|
||||
eventHub.$off('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
|
||||
window.removeEventListener('popstate', this.checkHistory);
|
||||
},
|
||||
mounted() {
|
||||
eventHub.$on('fileNameClicked', this.fileClicked);
|
||||
eventHub.$on('goToPreviousDirectoryClicked', this.goToPreviousDirectoryClicked);
|
||||
},
|
||||
data() {
|
||||
return Store;
|
||||
},
|
||||
computed: {
|
||||
flattendFiles() {
|
||||
const mapFiles = arr => (!arr.files.length ? [] : _.map(arr.files, a => [a, mapFiles(a)]));
|
||||
|
||||
data: () => Store,
|
||||
|
||||
return _.chain(this.files)
|
||||
.map(arr => [arr, mapFiles(arr)])
|
||||
.flatten()
|
||||
.value();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
checkHistory() {
|
||||
let selectedFile = this.files.find(file => location.pathname.indexOf(file.url) > -1);
|
||||
|
@ -52,21 +67,25 @@ export default {
|
|||
},
|
||||
|
||||
fileClicked(clickedFile, lineNumber) {
|
||||
let file = clickedFile;
|
||||
const file = clickedFile;
|
||||
|
||||
if (file.loading) return;
|
||||
file.loading = true;
|
||||
|
||||
if (file.type === 'tree' && file.opened) {
|
||||
file = Store.removeChildFilesOfTree(file);
|
||||
file.loading = false;
|
||||
Helper.setDirectoryToClosed(file);
|
||||
Store.setActiveLine(lineNumber);
|
||||
} else if (file.type === 'submodule') {
|
||||
file.loading = true;
|
||||
|
||||
gl.utils.visitUrl(file.url);
|
||||
} else {
|
||||
const openFile = Helper.getFileFromPath(file.url);
|
||||
|
||||
if (openFile) {
|
||||
file.loading = false;
|
||||
Store.setActiveFiles(openFile);
|
||||
Store.setActiveLine(lineNumber);
|
||||
} else {
|
||||
file.loading = true;
|
||||
Service.url = file.url;
|
||||
Helper.getContent(file)
|
||||
.then(() => {
|
||||
|
@ -81,7 +100,7 @@ export default {
|
|||
|
||||
goToPreviousDirectoryClicked(prevURL) {
|
||||
Service.url = prevURL;
|
||||
Helper.getContent(null)
|
||||
Helper.getContent(null, true)
|
||||
.then(() => Helper.scrollTabsRight())
|
||||
.catch(Helper.loadingError);
|
||||
},
|
||||
|
@ -92,38 +111,43 @@ export default {
|
|||
<template>
|
||||
<div id="sidebar" :class="{'sidebar-mini' : isMini}">
|
||||
<table class="table">
|
||||
<thead v-if="!isMini">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="name">Name</th>
|
||||
<th class="hidden-sm hidden-xs last-commit">Last commit</th>
|
||||
<th class="hidden-xs last-update text-right">Last update</th>
|
||||
<th
|
||||
v-if="isMini"
|
||||
class="repo-file-options title"
|
||||
>
|
||||
<strong class="clgray">
|
||||
{{ projectName }}
|
||||
</strong>
|
||||
</th>
|
||||
<template v-else>
|
||||
<th class="name">
|
||||
Name
|
||||
</th>
|
||||
<th class="hidden-sm hidden-xs last-commit">
|
||||
Last commit
|
||||
</th>
|
||||
<th class="hidden-xs last-update text-right">
|
||||
Last update
|
||||
</th>
|
||||
</template>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<repo-file-options
|
||||
:is-mini="isMini"
|
||||
:project-name="projectName"
|
||||
/>
|
||||
<repo-previous-directory
|
||||
v-if="isRoot"
|
||||
v-if="!isRoot && !loading.tree"
|
||||
:prev-url="prevURL"
|
||||
@linkclicked="goToPreviousDirectoryClicked(prevURL)"/>
|
||||
/>
|
||||
<repo-loading-file
|
||||
v-if="!flattendFiles.length && loading.tree"
|
||||
v-for="n in 5"
|
||||
:key="n"
|
||||
:loading="loading"
|
||||
:has-files="!!files.length"
|
||||
:is-mini="isMini"
|
||||
/>
|
||||
<repo-file
|
||||
v-for="file in files"
|
||||
v-for="file in flattendFiles"
|
||||
:key="file.id"
|
||||
:file="file"
|
||||
:is-mini="isMini"
|
||||
@linkclicked="fileClicked(file)"
|
||||
:is-tree="isTree"
|
||||
:has-files="!!files.length"
|
||||
:active-file="activeFile"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -26,11 +26,13 @@ const RepoTab = {
|
|||
},
|
||||
|
||||
methods: {
|
||||
tabClicked: Store.setActiveFiles,
|
||||
|
||||
tabClicked(file) {
|
||||
Store.setActiveFiles(file);
|
||||
},
|
||||
closeTab(file) {
|
||||
if (file.changed) return;
|
||||
this.$emit('tabclosed', file);
|
||||
|
||||
Store.removeFromOpenedFiles(file);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -39,25 +41,28 @@ export default RepoTab;
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<li @click="tabClicked(tab)">
|
||||
<a
|
||||
href="#0"
|
||||
class="close"
|
||||
@click.stop.prevent="closeTab(tab)"
|
||||
:aria-label="closeLabel">
|
||||
<i
|
||||
class="fa"
|
||||
:class="changedClass"
|
||||
aria-hidden="true">
|
||||
</i>
|
||||
</a>
|
||||
<li
|
||||
:class="{ active : tab.active }"
|
||||
@click="tabClicked(tab)"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="close-btn"
|
||||
@click.stop.prevent="closeTab(tab)"
|
||||
:aria-label="closeLabel">
|
||||
<i
|
||||
class="fa"
|
||||
:class="changedClass"
|
||||
aria-hidden="true">
|
||||
</i>
|
||||
</button>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
class="repo-tab"
|
||||
:title="tab.url"
|
||||
@click.prevent="tabClicked(tab)">
|
||||
{{tab.name}}
|
||||
</a>
|
||||
</li>
|
||||
<a
|
||||
href="#"
|
||||
class="repo-tab"
|
||||
:title="tab.url"
|
||||
@click.prevent="tabClicked(tab)">
|
||||
{{tab.name}}
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
|
|
|
@ -1,36 +1,29 @@
|
|||
<script>
|
||||
import Store from '../stores/repo_store';
|
||||
import RepoTab from './repo_tab.vue';
|
||||
import RepoMixin from '../mixins/repo_mixin';
|
||||
import Store from '../stores/repo_store';
|
||||
import RepoTab from './repo_tab.vue';
|
||||
import RepoMixin from '../mixins/repo_mixin';
|
||||
|
||||
const RepoTabs = {
|
||||
mixins: [RepoMixin],
|
||||
|
||||
components: {
|
||||
'repo-tab': RepoTab,
|
||||
},
|
||||
|
||||
data: () => Store,
|
||||
|
||||
methods: {
|
||||
tabClosed(file) {
|
||||
Store.removeFromOpenedFiles(file);
|
||||
export default {
|
||||
mixins: [RepoMixin],
|
||||
components: {
|
||||
'repo-tab': RepoTab,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default RepoTabs;
|
||||
data() {
|
||||
return Store;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul id="tabs">
|
||||
<repo-tab
|
||||
v-for="tab in openedFiles"
|
||||
:key="tab.id"
|
||||
:tab="tab"
|
||||
:class="{'active' : tab.active}"
|
||||
@tabclosed="tabClosed"
|
||||
/>
|
||||
<li class="tabs-divider" />
|
||||
</ul>
|
||||
<ul
|
||||
id="tabs"
|
||||
class="list-unstyled"
|
||||
>
|
||||
<repo-tab
|
||||
v-for="tab in openedFiles"
|
||||
:key="tab.id"
|
||||
:tab="tab"
|
||||
/>
|
||||
<li class="tabs-divider" />
|
||||
</ul>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
export default new Vue();
|
|
@ -1,3 +1,4 @@
|
|||
import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
|
||||
import Service from '../services/repo_service';
|
||||
import Store from '../stores/repo_store';
|
||||
import Flash from '../../flash';
|
||||
|
@ -25,10 +26,6 @@ const RepoHelper = {
|
|||
|
||||
key: '',
|
||||
|
||||
isTree(data) {
|
||||
return Object.hasOwnProperty.call(data, 'blobs');
|
||||
},
|
||||
|
||||
Time: window.performance
|
||||
&& window.performance.now
|
||||
? window.performance
|
||||
|
@ -58,13 +55,20 @@ const RepoHelper = {
|
|||
},
|
||||
|
||||
setDirectoryOpen(tree, title) {
|
||||
const file = tree;
|
||||
if (!file) return undefined;
|
||||
if (!tree) return;
|
||||
|
||||
file.opened = true;
|
||||
file.icon = 'fa-folder-open';
|
||||
RepoHelper.updateHistoryEntry(file.url, title);
|
||||
return file;
|
||||
Object.assign(tree, {
|
||||
opened: true,
|
||||
});
|
||||
|
||||
RepoHelper.updateHistoryEntry(tree.url, title);
|
||||
},
|
||||
|
||||
setDirectoryToClosed(entry) {
|
||||
Object.assign(entry, {
|
||||
opened: false,
|
||||
files: [],
|
||||
});
|
||||
},
|
||||
|
||||
isRenderable() {
|
||||
|
@ -81,63 +85,23 @@ const RepoHelper = {
|
|||
.catch(RepoHelper.loadingError);
|
||||
},
|
||||
|
||||
// when you open a directory you need to put the directory files under
|
||||
// the directory... This will merge the list of the current directory and the new list.
|
||||
getNewMergedList(inDirectory, currentList, newList) {
|
||||
const newListSorted = newList.sort(this.compareFilesCaseInsensitive);
|
||||
if (!inDirectory) return newListSorted;
|
||||
const indexOfFile = currentList.findIndex(file => file.url === inDirectory.url);
|
||||
if (!indexOfFile) return newListSorted;
|
||||
return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile);
|
||||
},
|
||||
|
||||
// within the get new merged list this does the merging of the current list of files
|
||||
// and the new list of files. The files are never "in" another directory they just
|
||||
// appear like they are because of the margin.
|
||||
mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) {
|
||||
newList.reverse().forEach((newFile) => {
|
||||
const fileIndex = indexOfFile + 1;
|
||||
const file = newFile;
|
||||
file.level = inDirectory.level + 1;
|
||||
oldList.splice(fileIndex, 0, file);
|
||||
});
|
||||
|
||||
return oldList;
|
||||
},
|
||||
|
||||
compareFilesCaseInsensitive(a, b) {
|
||||
const aName = a.name.toLowerCase();
|
||||
const bName = b.name.toLowerCase();
|
||||
if (a.level > 0) return 0;
|
||||
if (aName < bName) { return -1; }
|
||||
if (aName > bName) { return 1; }
|
||||
return 0;
|
||||
},
|
||||
|
||||
isRoot(url) {
|
||||
// the url we are requesting -> split by the project URL. Grab the right side.
|
||||
const isRoot = !!url.split(Store.projectUrl)[1]
|
||||
// remove the first "/"
|
||||
.slice(1)
|
||||
// split this by "/"
|
||||
.split('/')
|
||||
// remove the first two items of the array... usually /tree/master.
|
||||
.slice(2)
|
||||
// we want to know the length of the array.
|
||||
// If greater than 0 not root.
|
||||
.length;
|
||||
return isRoot;
|
||||
},
|
||||
|
||||
getContent(treeOrFile) {
|
||||
getContent(treeOrFile, emptyFiles = false) {
|
||||
let file = treeOrFile;
|
||||
|
||||
if (!Store.files.length) {
|
||||
Store.loading.tree = true;
|
||||
}
|
||||
|
||||
return Service.getContent()
|
||||
.then((response) => {
|
||||
const data = response.data;
|
||||
if (response.headers && response.headers['page-title']) data.pageTitle = response.headers['page-title'];
|
||||
if (response.headers && response.headers['page-title']) data.pageTitle = decodeURI(response.headers['page-title']);
|
||||
if (response.headers && response.headers['is-root'] && !Store.isInitialRoot) {
|
||||
Store.isRoot = convertPermissionToBoolean(response.headers['is-root']);
|
||||
Store.isInitialRoot = Store.isRoot;
|
||||
}
|
||||
|
||||
Store.isTree = RepoHelper.isTree(data);
|
||||
if (!Store.isTree) {
|
||||
if (file && file.type === 'blob') {
|
||||
if (!file) file = data;
|
||||
Store.binary = data.binary;
|
||||
|
||||
|
@ -145,38 +109,40 @@ const RepoHelper = {
|
|||
// file might be undefined
|
||||
RepoHelper.setBinaryDataAsBase64(data);
|
||||
Store.setViewToPreview();
|
||||
} else if (!Store.isPreviewView()) {
|
||||
if (!data.render_error) {
|
||||
Service.getRaw(data.raw_path)
|
||||
.then((rawResponse) => {
|
||||
Store.blobRaw = rawResponse.data;
|
||||
data.plain = rawResponse.data;
|
||||
RepoHelper.setFile(data, file);
|
||||
}).catch(RepoHelper.loadingError);
|
||||
}
|
||||
} else if (!Store.isPreviewView() && !data.render_error) {
|
||||
Service.getRaw(data.raw_path)
|
||||
.then((rawResponse) => {
|
||||
Store.blobRaw = rawResponse.data;
|
||||
data.plain = rawResponse.data;
|
||||
RepoHelper.setFile(data, file);
|
||||
}).catch(RepoHelper.loadingError);
|
||||
}
|
||||
|
||||
if (Store.isPreviewView()) {
|
||||
RepoHelper.setFile(data, file);
|
||||
}
|
||||
|
||||
// if the file tree is empty
|
||||
if (Store.files.length === 0) {
|
||||
const parentURL = Service.blobURLtoParentTree(Service.url);
|
||||
Service.url = parentURL;
|
||||
RepoHelper.getContent();
|
||||
}
|
||||
} else {
|
||||
// it's a tree
|
||||
if (!file) Store.isRoot = RepoHelper.isRoot(Service.url);
|
||||
file = RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name);
|
||||
const newDirectory = RepoHelper.dataToListOfFiles(data);
|
||||
Store.addFilesToDirectory(file, Store.files, newDirectory);
|
||||
Store.loading.tree = false;
|
||||
RepoHelper.setDirectoryOpen(file, data.pageTitle || data.name);
|
||||
|
||||
if (emptyFiles) {
|
||||
Store.files = [];
|
||||
}
|
||||
|
||||
this.addToDirectory(file, data);
|
||||
|
||||
Store.prevURL = Service.blobURLtoParentTree(Service.url);
|
||||
}
|
||||
}).catch(RepoHelper.loadingError);
|
||||
},
|
||||
|
||||
addToDirectory(file, data) {
|
||||
const tree = file || Store;
|
||||
const files = tree.files.concat(this.dataToListOfFiles(data, file ? file.level + 1 : 0));
|
||||
|
||||
tree.files = files;
|
||||
},
|
||||
|
||||
setFile(data, file) {
|
||||
const newFile = data;
|
||||
newFile.url = file.url || Service.url; // Grab the URL from service, happens on page refresh.
|
||||
|
@ -190,57 +156,41 @@ const RepoHelper = {
|
|||
Store.setActiveFiles(newFile);
|
||||
},
|
||||
|
||||
serializeBlob(blob) {
|
||||
const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob);
|
||||
simpleBlob.lastCommitMessage = blob.last_commit.message;
|
||||
simpleBlob.lastCommitUpdate = blob.last_commit.committed_date;
|
||||
simpleBlob.loading = false;
|
||||
serializeRepoEntity(type, entity, level = 0) {
|
||||
const { id, url, name, icon, last_commit, tree_url } = entity;
|
||||
|
||||
return simpleBlob;
|
||||
},
|
||||
|
||||
serializeTree(tree) {
|
||||
return RepoHelper.serializeRepoEntity('tree', tree);
|
||||
},
|
||||
|
||||
serializeSubmodule(submodule) {
|
||||
return RepoHelper.serializeRepoEntity('submodule', submodule);
|
||||
},
|
||||
|
||||
serializeRepoEntity(type, entity) {
|
||||
const { url, name, icon, last_commit } = entity;
|
||||
const returnObj = {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
url,
|
||||
tree_url,
|
||||
level,
|
||||
icon: `fa-${icon}`,
|
||||
level: 0,
|
||||
files: [],
|
||||
loading: false,
|
||||
opened: false,
|
||||
// eslint-disable-next-line camelcase
|
||||
lastCommit: last_commit ? {
|
||||
url: `${Store.projectUrl}/commit/${last_commit.id}`,
|
||||
message: last_commit.message,
|
||||
updatedAt: last_commit.committed_date,
|
||||
} : {},
|
||||
};
|
||||
|
||||
if (entity.last_commit) {
|
||||
returnObj.lastCommitUrl = `${Store.projectUrl}/commit/${last_commit.id}`;
|
||||
} else {
|
||||
returnObj.lastCommitUrl = '';
|
||||
}
|
||||
return returnObj;
|
||||
},
|
||||
|
||||
scrollTabsRight() {
|
||||
// wait for the transition. 0.1 seconds.
|
||||
setTimeout(() => {
|
||||
const tabs = document.getElementById('tabs');
|
||||
if (!tabs) return;
|
||||
tabs.scrollLeft = tabs.scrollWidth;
|
||||
}, 200);
|
||||
const tabs = document.getElementById('tabs');
|
||||
if (!tabs) return;
|
||||
tabs.scrollLeft = tabs.scrollWidth;
|
||||
},
|
||||
|
||||
dataToListOfFiles(data) {
|
||||
dataToListOfFiles(data, level) {
|
||||
const { blobs, trees, submodules } = data;
|
||||
return [
|
||||
...blobs.map(blob => RepoHelper.serializeBlob(blob)),
|
||||
...trees.map(tree => RepoHelper.serializeTree(tree)),
|
||||
...submodules.map(submodule => RepoHelper.serializeSubmodule(submodule)),
|
||||
...trees.map(tree => RepoHelper.serializeRepoEntity('tree', tree, level)),
|
||||
...submodules.map(submodule => RepoHelper.serializeRepoEntity('submodule', submodule, level)),
|
||||
...blobs.map(blob => RepoHelper.serializeRepoEntity('blob', blob, level)),
|
||||
];
|
||||
},
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
|
||||
import Service from './services/repo_service';
|
||||
import Store from './stores/repo_store';
|
||||
import Repo from './components/repo.vue';
|
||||
import RepoEditButton from './components/repo_edit_button.vue';
|
||||
import newBranchForm from './components/new_branch_form.vue';
|
||||
import Translate from '../vue_shared/translate';
|
||||
|
||||
function initDropdowns() {
|
||||
|
@ -31,8 +33,13 @@ function setInitialStore(data) {
|
|||
Store.projectUrl = data.projectUrl;
|
||||
Store.canCommit = data.canCommit;
|
||||
Store.onTopOfBranch = data.onTopOfBranch;
|
||||
Store.newMrTemplateUrl = decodeURIComponent(data.newMrTemplateUrl);
|
||||
Store.customBranchURL = decodeURIComponent(data.blobUrl);
|
||||
Store.isRoot = convertPermissionToBoolean(data.root);
|
||||
Store.isInitialRoot = convertPermissionToBoolean(data.root);
|
||||
Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
|
||||
Store.checkIsCommitable();
|
||||
Store.setBranchHash();
|
||||
}
|
||||
|
||||
function initRepo(el) {
|
||||
|
@ -56,6 +63,26 @@ function initRepoEditButton(el) {
|
|||
});
|
||||
}
|
||||
|
||||
function initNewBranchForm() {
|
||||
const el = document.querySelector('.js-new-branch-dropdown');
|
||||
|
||||
if (!el) return null;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
components: {
|
||||
newBranchForm,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('new-branch-form', {
|
||||
props: {
|
||||
currentBranch: Store.currentBranch,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function initRepoBundle() {
|
||||
const repo = document.getElementById('repo');
|
||||
const editButton = document.querySelector('.editable-mode');
|
||||
|
@ -67,6 +94,7 @@ function initRepoBundle() {
|
|||
|
||||
initRepo(repo);
|
||||
initRepoEditButton(editButton);
|
||||
initNewBranchForm();
|
||||
}
|
||||
|
||||
$(initRepoBundle);
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import axios from 'axios';
|
||||
import csrf from '../../lib/utils/csrf';
|
||||
import Store from '../stores/repo_store';
|
||||
import Api from '../../api';
|
||||
import Helper from '../helpers/repo_helper';
|
||||
|
||||
axios.defaults.headers.common[csrf.headerKey] = csrf.token;
|
||||
|
||||
const RepoService = {
|
||||
url: '',
|
||||
options: {
|
||||
|
@ -10,6 +13,7 @@ const RepoService = {
|
|||
format: 'json',
|
||||
},
|
||||
},
|
||||
createBranchPath: '/api/:version/projects/:id/repository/branches',
|
||||
richExtensionRegExp: /md/,
|
||||
|
||||
getRaw(url) {
|
||||
|
@ -64,11 +68,21 @@ const RepoService = {
|
|||
return urlArray.join('/');
|
||||
},
|
||||
|
||||
getBranch() {
|
||||
return Api.branchSingle(Store.projectId, Store.currentBranch);
|
||||
},
|
||||
|
||||
commitFiles(payload) {
|
||||
return Api.commitMultiple(Store.projectId, payload)
|
||||
.then(this.commitFlash);
|
||||
},
|
||||
|
||||
createBranch(payload) {
|
||||
const url = Api.buildUrl(this.createBranchPath)
|
||||
.replace(':id', Store.projectId);
|
||||
return axios.post(url, payload);
|
||||
},
|
||||
|
||||
commitFlash(data) {
|
||||
if (data.short_id && data.stats) {
|
||||
window.Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
|
||||
|
|
|
@ -2,18 +2,18 @@ import Helper from '../helpers/repo_helper';
|
|||
import Service from '../services/repo_service';
|
||||
|
||||
const RepoStore = {
|
||||
monaco: {},
|
||||
monacoLoading: false,
|
||||
service: '',
|
||||
canCommit: false,
|
||||
onTopOfBranch: false,
|
||||
editMode: false,
|
||||
isTree: false,
|
||||
isRoot: false,
|
||||
isRoot: null,
|
||||
isInitialRoot: null,
|
||||
prevURL: '',
|
||||
projectId: '',
|
||||
projectName: '',
|
||||
projectUrl: '',
|
||||
branchUrl: '',
|
||||
blobRaw: '',
|
||||
currentBlobView: 'repo-preview',
|
||||
openedFiles: [],
|
||||
|
@ -23,6 +23,7 @@ const RepoStore = {
|
|||
title: '',
|
||||
status: false,
|
||||
},
|
||||
showNewBranchDialog: false,
|
||||
activeFile: Helper.getDefaultActiveFile(),
|
||||
activeFileIndex: 0,
|
||||
activeLine: -1,
|
||||
|
@ -31,22 +32,27 @@ const RepoStore = {
|
|||
isCommitable: false,
|
||||
binary: false,
|
||||
currentBranch: '',
|
||||
startNewMR: false,
|
||||
currentHash: '',
|
||||
currentShortHash: '',
|
||||
customBranchURL: '',
|
||||
newMrTemplateUrl: '',
|
||||
branchChanged: false,
|
||||
commitMessage: '',
|
||||
binaryTypes: {
|
||||
png: false,
|
||||
md: false,
|
||||
svg: false,
|
||||
unknown: false,
|
||||
},
|
||||
loading: {
|
||||
tree: false,
|
||||
blob: false,
|
||||
},
|
||||
|
||||
resetBinaryTypes() {
|
||||
Object.keys(RepoStore.binaryTypes).forEach((key) => {
|
||||
RepoStore.binaryTypes[key] = false;
|
||||
});
|
||||
setBranchHash() {
|
||||
return Service.getBranch()
|
||||
.then((data) => {
|
||||
if (RepoStore.currentHash !== '' && data.commit.id !== RepoStore.currentHash) {
|
||||
RepoStore.branchChanged = true;
|
||||
}
|
||||
RepoStore.currentHash = data.commit.id;
|
||||
RepoStore.currentShortHash = data.commit.short_id;
|
||||
});
|
||||
},
|
||||
|
||||
// mutations
|
||||
|
@ -54,10 +60,6 @@ const RepoStore = {
|
|||
RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit;
|
||||
},
|
||||
|
||||
addFilesToDirectory(inDirectory, currentList, newList) {
|
||||
RepoStore.files = Helper.getNewMergedList(inDirectory, currentList, newList);
|
||||
},
|
||||
|
||||
toggleRawPreview() {
|
||||
RepoStore.activeFile.raw = !RepoStore.activeFile.raw;
|
||||
RepoStore.activeFileLabel = RepoStore.activeFile.raw ? 'Display rendered file' : 'Display source';
|
||||
|
@ -111,30 +113,6 @@ const RepoStore = {
|
|||
RepoStore.activeFileLabel = 'Display source';
|
||||
},
|
||||
|
||||
removeChildFilesOfTree(tree) {
|
||||
let foundTree = false;
|
||||
const treeToClose = tree;
|
||||
let canStopSearching = false;
|
||||
RepoStore.files = RepoStore.files.filter((file) => {
|
||||
const isItTheTreeWeWant = file.url === treeToClose.url;
|
||||
// if it's the next tree
|
||||
if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) {
|
||||
canStopSearching = true;
|
||||
return true;
|
||||
}
|
||||
if (canStopSearching) return true;
|
||||
|
||||
if (isItTheTreeWeWant) foundTree = true;
|
||||
|
||||
if (foundTree) return file.level <= treeToClose.level;
|
||||
return true;
|
||||
});
|
||||
|
||||
treeToClose.opened = false;
|
||||
treeToClose.icon = 'fa-folder';
|
||||
return treeToClose;
|
||||
},
|
||||
|
||||
removeFromOpenedFiles(file) {
|
||||
if (file.type === 'tree') return;
|
||||
let foundIndex;
|
||||
|
@ -168,6 +146,7 @@ const RepoStore = {
|
|||
if (openedFilesAlreadyExists) return;
|
||||
|
||||
openFile.changed = false;
|
||||
openFile.active = true;
|
||||
RepoStore.openedFiles.push(openFile);
|
||||
},
|
||||
|
||||
|
|
|
@ -1,128 +1,116 @@
|
|||
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */
|
||||
/* global Mousetrap */
|
||||
import Cookies from 'js-cookie';
|
||||
import Mousetrap from 'mousetrap';
|
||||
|
||||
import findAndFollowLink from './shortcuts_dashboard_navigation';
|
||||
|
||||
(function() {
|
||||
this.Shortcuts = (function() {
|
||||
function Shortcuts(skipResetBindings) {
|
||||
this.onToggleHelp = this.onToggleHelp.bind(this);
|
||||
this.enabledHelp = [];
|
||||
if (!skipResetBindings) {
|
||||
Mousetrap.reset();
|
||||
}
|
||||
Mousetrap.bind('?', this.onToggleHelp);
|
||||
Mousetrap.bind('s', Shortcuts.focusSearch);
|
||||
Mousetrap.bind('f', (e => this.focusFilter(e)));
|
||||
Mousetrap.bind('p b', this.onTogglePerfBar);
|
||||
const defaultStopCallback = Mousetrap.stopCallback;
|
||||
Mousetrap.stopCallback = (e, element, combo) => {
|
||||
if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const findFileURL = document.body.dataset.findFile;
|
||||
return defaultStopCallback(e, element, combo);
|
||||
};
|
||||
|
||||
Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos'));
|
||||
Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity'));
|
||||
Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues'));
|
||||
Mousetrap.bind('shift+m', () => findAndFollowLink('.dashboard-shortcuts-merge_requests'));
|
||||
Mousetrap.bind('shift+p', () => findAndFollowLink('.dashboard-shortcuts-projects'));
|
||||
Mousetrap.bind('shift+g', () => findAndFollowLink('.dashboard-shortcuts-groups'));
|
||||
Mousetrap.bind('shift+l', () => findAndFollowLink('.dashboard-shortcuts-milestones'));
|
||||
Mousetrap.bind('shift+s', () => findAndFollowLink('.dashboard-shortcuts-snippets'));
|
||||
export default class Shortcuts {
|
||||
constructor(skipResetBindings) {
|
||||
this.onToggleHelp = this.onToggleHelp.bind(this);
|
||||
this.enabledHelp = [];
|
||||
if (!skipResetBindings) {
|
||||
Mousetrap.reset();
|
||||
}
|
||||
Mousetrap.bind('?', this.onToggleHelp);
|
||||
Mousetrap.bind('s', Shortcuts.focusSearch);
|
||||
Mousetrap.bind('f', this.focusFilter.bind(this));
|
||||
Mousetrap.bind('p b', Shortcuts.onTogglePerfBar);
|
||||
|
||||
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview);
|
||||
if (typeof findFileURL !== "undefined" && findFileURL !== null) {
|
||||
Mousetrap.bind('t', function() {
|
||||
return gl.utils.visitUrl(findFileURL);
|
||||
});
|
||||
}
|
||||
const findFileURL = document.body.dataset.findFile;
|
||||
|
||||
Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos'));
|
||||
Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity'));
|
||||
Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues'));
|
||||
Mousetrap.bind('shift+m', () => findAndFollowLink('.dashboard-shortcuts-merge_requests'));
|
||||
Mousetrap.bind('shift+p', () => findAndFollowLink('.dashboard-shortcuts-projects'));
|
||||
Mousetrap.bind('shift+g', () => findAndFollowLink('.dashboard-shortcuts-groups'));
|
||||
Mousetrap.bind('shift+l', () => findAndFollowLink('.dashboard-shortcuts-milestones'));
|
||||
Mousetrap.bind('shift+s', () => findAndFollowLink('.dashboard-shortcuts-snippets'));
|
||||
|
||||
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], Shortcuts.toggleMarkdownPreview);
|
||||
|
||||
if (typeof findFileURL !== 'undefined' && findFileURL !== null) {
|
||||
Mousetrap.bind('t', () => {
|
||||
gl.utils.visitUrl(findFileURL);
|
||||
});
|
||||
}
|
||||
|
||||
Shortcuts.prototype.onToggleHelp = function(e) {
|
||||
$(document).on('click.more_help', '.js-more-help-button', function clickMoreHelp(e) {
|
||||
$(this).remove();
|
||||
$('.hidden-shortcut').show();
|
||||
e.preventDefault();
|
||||
return Shortcuts.toggleHelp(this.enabledHelp);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Shortcuts.prototype.onTogglePerfBar = function(e) {
|
||||
e.preventDefault();
|
||||
const performanceBarCookieName = 'perf_bar_enabled';
|
||||
if (Cookies.get(performanceBarCookieName) === 'true') {
|
||||
Cookies.remove(performanceBarCookieName, { path: '/' });
|
||||
} else {
|
||||
Cookies.set(performanceBarCookieName, 'true', { path: '/' });
|
||||
}
|
||||
gl.utils.refreshCurrentPage();
|
||||
};
|
||||
onToggleHelp(e) {
|
||||
e.preventDefault();
|
||||
Shortcuts.toggleHelp(this.enabledHelp);
|
||||
}
|
||||
|
||||
Shortcuts.prototype.toggleMarkdownPreview = function(e) {
|
||||
// Check if short-cut was triggered while in Write Mode
|
||||
const $target = $(e.target);
|
||||
const $form = $target.closest('form');
|
||||
static onTogglePerfBar(e) {
|
||||
e.preventDefault();
|
||||
const performanceBarCookieName = 'perf_bar_enabled';
|
||||
if (Cookies.get(performanceBarCookieName) === 'true') {
|
||||
Cookies.remove(performanceBarCookieName, { path: '/' });
|
||||
} else {
|
||||
Cookies.set(performanceBarCookieName, 'true', { path: '/' });
|
||||
}
|
||||
gl.utils.refreshCurrentPage();
|
||||
}
|
||||
|
||||
if ($target.hasClass('js-note-text')) {
|
||||
$('.js-md-preview-button', $form).focus();
|
||||
}
|
||||
return $(document).triggerHandler('markdown-preview:toggle', [e]);
|
||||
};
|
||||
static toggleMarkdownPreview(e) {
|
||||
// Check if short-cut was triggered while in Write Mode
|
||||
const $target = $(e.target);
|
||||
const $form = $target.closest('form');
|
||||
|
||||
Shortcuts.toggleHelp = function(location) {
|
||||
var $modal;
|
||||
$modal = $('#modal-shortcuts');
|
||||
if ($modal.length) {
|
||||
$modal.modal('toggle');
|
||||
return;
|
||||
}
|
||||
return $.ajax({
|
||||
url: gon.shortcuts_path,
|
||||
dataType: 'script',
|
||||
success: function(e) {
|
||||
var i, l, len, results;
|
||||
if (location && location.length > 0) {
|
||||
results = [];
|
||||
for (i = 0, len = location.length; i < len; i += 1) {
|
||||
l = location[i];
|
||||
results.push($(l).show());
|
||||
}
|
||||
return results;
|
||||
} else {
|
||||
$('.hidden-shortcut').show();
|
||||
return $('.js-more-help-button').remove();
|
||||
if ($target.hasClass('js-note-text')) {
|
||||
$('.js-md-preview-button', $form).focus();
|
||||
}
|
||||
$(document).triggerHandler('markdown-preview:toggle', [e]);
|
||||
}
|
||||
|
||||
static toggleHelp(location) {
|
||||
const $modal = $('#modal-shortcuts');
|
||||
|
||||
if ($modal.length) {
|
||||
$modal.modal('toggle');
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: gon.shortcuts_path,
|
||||
dataType: 'script',
|
||||
success() {
|
||||
if (location && location.length > 0) {
|
||||
const results = [];
|
||||
for (let i = 0, len = location.length; i < len; i += 1) {
|
||||
results.push($(location[i]).show());
|
||||
}
|
||||
return results;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Shortcuts.prototype.focusFilter = function(e) {
|
||||
if (this.filterInput == null) {
|
||||
this.filterInput = $('input[type=search]', '.nav-controls');
|
||||
}
|
||||
this.filterInput.focus();
|
||||
return e.preventDefault();
|
||||
};
|
||||
$('.hidden-shortcut').show();
|
||||
return $('.js-more-help-button').remove();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Shortcuts.focusSearch = function(e) {
|
||||
$('#search').focus();
|
||||
return e.preventDefault();
|
||||
};
|
||||
focusFilter(e) {
|
||||
if (!this.filterInput) {
|
||||
this.filterInput = $('input[type=search]', '.nav-controls');
|
||||
}
|
||||
this.filterInput.focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
return Shortcuts;
|
||||
})();
|
||||
|
||||
$(document).on('click.more_help', '.js-more-help-button', function(e) {
|
||||
$(this).remove();
|
||||
$('.hidden-shortcut').show();
|
||||
return e.preventDefault();
|
||||
});
|
||||
|
||||
Mousetrap.stopCallback = (function() {
|
||||
var defaultStopCallback;
|
||||
defaultStopCallback = Mousetrap.stopCallback;
|
||||
return function(e, element, combo) {
|
||||
// allowed shortcuts if textarea, input, contenteditable are focused
|
||||
if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) {
|
||||
return false;
|
||||
} else {
|
||||
return defaultStopCallback.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
})();
|
||||
}).call(window);
|
||||
static focusSearch(e) {
|
||||
$('#search').focus();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/* global Mousetrap */
|
||||
/* global Shortcuts */
|
||||
|
||||
import './shortcuts';
|
||||
import Shortcuts from './shortcuts';
|
||||
|
||||
const defaults = {
|
||||
skipResetBindings: false,
|
||||
|
|
|
@ -1,38 +1,30 @@
|
|||
/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife */
|
||||
/* global Mousetrap */
|
||||
/* global ShortcutsNavigation */
|
||||
|
||||
import './shortcuts_navigation';
|
||||
import ShortcutsNavigation from './shortcuts_navigation';
|
||||
|
||||
(function() {
|
||||
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
|
||||
hasProp = {}.hasOwnProperty;
|
||||
export default class ShortcutsFindFile extends ShortcutsNavigation {
|
||||
constructor(projectFindFile) {
|
||||
super();
|
||||
|
||||
this.ShortcutsFindFile = (function(superClass) {
|
||||
extend(ShortcutsFindFile, superClass);
|
||||
const oldStopCallback = Mousetrap.stopCallback;
|
||||
this.projectFindFile = projectFindFile;
|
||||
|
||||
function ShortcutsFindFile(projectFindFile) {
|
||||
var _oldStopCallback;
|
||||
this.projectFindFile = projectFindFile;
|
||||
ShortcutsFindFile.__super__.constructor.call(this);
|
||||
_oldStopCallback = Mousetrap.stopCallback;
|
||||
Mousetrap.stopCallback = (function(_this) {
|
||||
// override to fire shortcuts action when focus in textbox
|
||||
return function(event, element, combo) {
|
||||
if (element === _this.projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')) {
|
||||
// when press up/down key in textbox, cusor prevent to move to home/end
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
return _oldStopCallback(event, element, combo);
|
||||
};
|
||||
})(this);
|
||||
Mousetrap.bind('up', this.projectFindFile.selectRowUp);
|
||||
Mousetrap.bind('down', this.projectFindFile.selectRowDown);
|
||||
Mousetrap.bind('esc', this.projectFindFile.goToTree);
|
||||
Mousetrap.bind('enter', this.projectFindFile.goToBlob);
|
||||
}
|
||||
Mousetrap.stopCallback = (e, element, combo) => {
|
||||
if (
|
||||
element === this.projectFindFile.inputElement[0] &&
|
||||
(combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')
|
||||
) {
|
||||
// when press up/down key in textbox, cusor prevent to move to home/end
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
return ShortcutsFindFile;
|
||||
})(ShortcutsNavigation);
|
||||
}).call(window);
|
||||
return oldStopCallback(e, element, combo);
|
||||
};
|
||||
|
||||
Mousetrap.bind('up', this.projectFindFile.selectRowUp);
|
||||
Mousetrap.bind('down', this.projectFindFile.selectRowDown);
|
||||
Mousetrap.bind('esc', this.projectFindFile.goToTree);
|
||||
Mousetrap.bind('enter', this.projectFindFile.goToBlob);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,100 +1,74 @@
|
|||
/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators */
|
||||
/* global Mousetrap */
|
||||
/* global ShortcutsNavigation */
|
||||
/* global sidebar */
|
||||
|
||||
import _ from 'underscore';
|
||||
import 'mousetrap';
|
||||
import './shortcuts_navigation';
|
||||
import ShortcutsNavigation from './shortcuts_navigation';
|
||||
|
||||
(function() {
|
||||
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
|
||||
hasProp = {}.hasOwnProperty;
|
||||
export default class ShortcutsIssuable extends ShortcutsNavigation {
|
||||
constructor(isMergeRequest) {
|
||||
super();
|
||||
|
||||
this.ShortcutsIssuable = (function(superClass) {
|
||||
extend(ShortcutsIssuable, superClass);
|
||||
this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form');
|
||||
this.editBtn = document.querySelector('.issuable-edit');
|
||||
|
||||
function ShortcutsIssuable(isMergeRequest) {
|
||||
ShortcutsIssuable.__super__.constructor.call(this);
|
||||
Mousetrap.bind('a', this.openSidebarDropdown.bind(this, 'assignee'));
|
||||
Mousetrap.bind('m', this.openSidebarDropdown.bind(this, 'milestone'));
|
||||
Mousetrap.bind('r', (function(_this) {
|
||||
return function() {
|
||||
_this.replyWithSelectedText(isMergeRequest);
|
||||
return false;
|
||||
};
|
||||
})(this));
|
||||
Mousetrap.bind('e', (function(_this) {
|
||||
return function() {
|
||||
_this.editIssue();
|
||||
return false;
|
||||
};
|
||||
})(this));
|
||||
Mousetrap.bind('l', this.openSidebarDropdown.bind(this, 'labels'));
|
||||
if (isMergeRequest) {
|
||||
this.enabledHelp.push('.hidden-shortcut.merge_requests');
|
||||
} else {
|
||||
this.enabledHelp.push('.hidden-shortcut.issues');
|
||||
}
|
||||
Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee'));
|
||||
Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone'));
|
||||
Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels'));
|
||||
Mousetrap.bind('r', this.replyWithSelectedText.bind(this));
|
||||
Mousetrap.bind('e', this.editIssue.bind(this));
|
||||
|
||||
if (isMergeRequest) {
|
||||
this.enabledHelp.push('.hidden-shortcut.merge_requests');
|
||||
} else {
|
||||
this.enabledHelp.push('.hidden-shortcut.issues');
|
||||
}
|
||||
}
|
||||
|
||||
replyWithSelectedText() {
|
||||
const documentFragment = window.gl.utils.getSelectedFragment();
|
||||
|
||||
if (!documentFragment) {
|
||||
this.$replyField.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
ShortcutsIssuable.prototype.replyWithSelectedText = function(isMergeRequest) {
|
||||
var quote, documentFragment, el, selected, separator;
|
||||
let replyField;
|
||||
const el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
|
||||
const selected = window.gl.CopyAsGFM.nodeToGFM(el);
|
||||
|
||||
if (isMergeRequest) {
|
||||
replyField = $('.js-main-target-form #note_note');
|
||||
} else {
|
||||
replyField = $('.js-main-target-form .js-vue-comment-form');
|
||||
}
|
||||
|
||||
documentFragment = window.gl.utils.getSelectedFragment();
|
||||
if (!documentFragment) {
|
||||
replyField.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
|
||||
selected = window.gl.CopyAsGFM.nodeToGFM(el);
|
||||
|
||||
if (selected.trim() === "") {
|
||||
return;
|
||||
}
|
||||
quote = _.map(selected.split("\n"), function(val) {
|
||||
return ("> " + val).trim() + "\n";
|
||||
});
|
||||
|
||||
// If replyField already has some content, add a newline before our quote
|
||||
separator = replyField.val().trim() !== "" && "\n\n" || '';
|
||||
replyField.val(function(a, current) {
|
||||
return current + separator + quote.join('') + "\n";
|
||||
});
|
||||
|
||||
// Trigger autosave
|
||||
replyField.trigger('input').trigger('change');
|
||||
|
||||
// Trigger autosize
|
||||
var event = document.createEvent('Event');
|
||||
event.initEvent('autosize:update', true, false);
|
||||
replyField.get(0).dispatchEvent(event);
|
||||
|
||||
// Focus the input field
|
||||
return replyField.focus();
|
||||
};
|
||||
|
||||
ShortcutsIssuable.prototype.editIssue = function() {
|
||||
var $editBtn;
|
||||
$editBtn = $('.issuable-edit');
|
||||
// Need to click the element as on issues, editing is inline
|
||||
// on merge request, editing is on a different page
|
||||
$editBtn.get(0).click();
|
||||
};
|
||||
|
||||
ShortcutsIssuable.prototype.openSidebarDropdown = function(name) {
|
||||
sidebar.openDropdown(name);
|
||||
if (selected.trim() === '') {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
return ShortcutsIssuable;
|
||||
})(ShortcutsNavigation);
|
||||
}).call(window);
|
||||
const quote = _.map(selected.split('\n'), val => `${(`> ${val}`).trim()}\n`);
|
||||
|
||||
// If replyField already has some content, add a newline before our quote
|
||||
const separator = (this.$replyField.val().trim() !== '' && '\n\n') || '';
|
||||
this.$replyField.val((a, current) => `${current}${separator}${quote.join('')}\n`)
|
||||
.trigger('input')
|
||||
.trigger('change');
|
||||
|
||||
// Trigger autosize
|
||||
const event = document.createEvent('Event');
|
||||
event.initEvent('autosize:update', true, false);
|
||||
this.$replyField.get(0).dispatchEvent(event);
|
||||
|
||||
// Focus the input field
|
||||
this.$replyField.focus();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
editIssue() {
|
||||
// Need to click the element as on issues, editing is inline
|
||||
// on merge request, editing is on a different page
|
||||
this.editBtn.click();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static openSidebarDropdown(name) {
|
||||
sidebar.openDropdown(name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,36 +1,27 @@
|
|||
/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */
|
||||
/* global Mousetrap */
|
||||
/* global Shortcuts */
|
||||
|
||||
import findAndFollowLink from './shortcuts_dashboard_navigation';
|
||||
import './shortcuts';
|
||||
import Shortcuts from './shortcuts';
|
||||
|
||||
(function() {
|
||||
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
|
||||
hasProp = {}.hasOwnProperty;
|
||||
export default class ShortcutsNavigation extends Shortcuts {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.ShortcutsNavigation = (function(superClass) {
|
||||
extend(ShortcutsNavigation, superClass);
|
||||
Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project'));
|
||||
Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity'));
|
||||
Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree'));
|
||||
Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
|
||||
Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
|
||||
Mousetrap.bind('g n', () => findAndFollowLink('.shortcuts-network'));
|
||||
Mousetrap.bind('g d', () => findAndFollowLink('.shortcuts-repository-charts'));
|
||||
Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues'));
|
||||
Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards'));
|
||||
Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests'));
|
||||
Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos'));
|
||||
Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki'));
|
||||
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
|
||||
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
|
||||
|
||||
function ShortcutsNavigation() {
|
||||
ShortcutsNavigation.__super__.constructor.call(this);
|
||||
Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project'));
|
||||
Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity'));
|
||||
Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree'));
|
||||
Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
|
||||
Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
|
||||
Mousetrap.bind('g n', () => findAndFollowLink('.shortcuts-network'));
|
||||
Mousetrap.bind('g d', () => findAndFollowLink('.shortcuts-repository-charts'));
|
||||
Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues'));
|
||||
Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards'));
|
||||
Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests'));
|
||||
Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos'));
|
||||
Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki'));
|
||||
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
|
||||
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
|
||||
this.enabledHelp.push('.hidden-shortcut.project');
|
||||
}
|
||||
|
||||
return ShortcutsNavigation;
|
||||
})(Shortcuts);
|
||||
}).call(window);
|
||||
this.enabledHelp.push('.hidden-shortcut.project');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,17 @@
|
|||
/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, max-len */
|
||||
/* global Mousetrap */
|
||||
/* global ShortcutsNavigation */
|
||||
import ShortcutsNavigation from './shortcuts_navigation';
|
||||
|
||||
import './shortcuts_navigation';
|
||||
export default class ShortcutsNetwork extends ShortcutsNavigation {
|
||||
constructor(graph) {
|
||||
super();
|
||||
|
||||
(function() {
|
||||
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
|
||||
hasProp = {}.hasOwnProperty;
|
||||
Mousetrap.bind(['left', 'h'], graph.scrollLeft);
|
||||
Mousetrap.bind(['right', 'l'], graph.scrollRight);
|
||||
Mousetrap.bind(['up', 'k'], graph.scrollUp);
|
||||
Mousetrap.bind(['down', 'j'], graph.scrollDown);
|
||||
Mousetrap.bind(['shift+up', 'shift+k'], graph.scrollTop);
|
||||
Mousetrap.bind(['shift+down', 'shift+j'], graph.scrollBottom);
|
||||
|
||||
this.ShortcutsNetwork = (function(superClass) {
|
||||
extend(ShortcutsNetwork, superClass);
|
||||
|
||||
function ShortcutsNetwork(graph) {
|
||||
this.graph = graph;
|
||||
ShortcutsNetwork.__super__.constructor.call(this);
|
||||
Mousetrap.bind(['left', 'h'], this.graph.scrollLeft);
|
||||
Mousetrap.bind(['right', 'l'], this.graph.scrollRight);
|
||||
Mousetrap.bind(['up', 'k'], this.graph.scrollUp);
|
||||
Mousetrap.bind(['down', 'j'], this.graph.scrollDown);
|
||||
Mousetrap.bind(['shift+up', 'shift+k'], this.graph.scrollTop);
|
||||
Mousetrap.bind(['shift+down', 'shift+j'], this.graph.scrollBottom);
|
||||
this.enabledHelp.push('.hidden-shortcut.network');
|
||||
}
|
||||
|
||||
return ShortcutsNetwork;
|
||||
})(ShortcutsNavigation);
|
||||
}).call(window);
|
||||
this.enabledHelp.push('.hidden-shortcut.network');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
/* global Mousetrap */
|
||||
/* global ShortcutsNavigation */
|
||||
|
||||
import ShortcutsNavigation from './shortcuts_navigation';
|
||||
import findAndFollowLink from './shortcuts_dashboard_navigation';
|
||||
|
||||
export default class ShortcutsWiki extends ShortcutsNavigation {
|
||||
|
|
|
@ -1,52 +1,64 @@
|
|||
<script>
|
||||
import ciIcon from './ci_icon.vue';
|
||||
/**
|
||||
* Renders CI Badge link with CI icon and status text based on
|
||||
* API response shared between all places where it is used.
|
||||
*
|
||||
* Receives status object containing:
|
||||
* status: {
|
||||
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
|
||||
* group:"running" // used for CSS class
|
||||
* icon: "icon_status_running" // used to render the icon
|
||||
* label:"running" // used for potential tooltip
|
||||
* text:"running" // text rendered
|
||||
* }
|
||||
*
|
||||
* Used in:
|
||||
* - Pipelines table - first column
|
||||
* - Jobs table - first column
|
||||
* - Pipeline show view - header
|
||||
* - Job show view - header
|
||||
* - MR widget
|
||||
*/
|
||||
import ciIcon from './ci_icon.vue';
|
||||
import tooltip from '../directives/tooltip';
|
||||
/**
|
||||
* Renders CI Badge link with CI icon and status text based on
|
||||
* API response shared between all places where it is used.
|
||||
*
|
||||
* Receives status object containing:
|
||||
* status: {
|
||||
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
|
||||
* group:"running" // used for CSS class
|
||||
* icon: "icon_status_running" // used to render the icon
|
||||
* label:"running" // used for potential tooltip
|
||||
* text:"running" // text rendered
|
||||
* }
|
||||
*
|
||||
* Used in:
|
||||
* - Pipelines table - first column
|
||||
* - Jobs table - first column
|
||||
* - Pipeline show view - header
|
||||
* - Job show view - header
|
||||
* - MR widget
|
||||
*/
|
||||
|
||||
export default {
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
required: true,
|
||||
export default {
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
showText: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
ciIcon,
|
||||
},
|
||||
|
||||
computed: {
|
||||
cssClass() {
|
||||
const className = this.status.group;
|
||||
|
||||
return className ? `ci-status ci-${this.status.group}` : 'ci-status';
|
||||
components: {
|
||||
ciIcon,
|
||||
},
|
||||
},
|
||||
};
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
computed: {
|
||||
cssClass() {
|
||||
const className = this.status.group;
|
||||
|
||||
return className ? `ci-status ci-${className}` : 'ci-status';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a
|
||||
:href="status.details_path"
|
||||
:class="cssClass">
|
||||
:class="cssClass"
|
||||
v-tooltip
|
||||
:title="!showText ? status.text : ''">
|
||||
<ci-icon :status="status" />
|
||||
{{status.text}}
|
||||
|
||||
<template v-if="showText">
|
||||
{{status.text}}
|
||||
</template>
|
||||
</a>
|
||||
</template>
|
||||
|
|
|
@ -63,14 +63,17 @@
|
|||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
showBranch: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* Used to verify if all the properties needed to render the commit
|
||||
* ref section were provided.
|
||||
*
|
||||
* TODO: Improve this! Use lodash _.has when we have it.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
hasCommitRef() {
|
||||
|
@ -80,8 +83,6 @@
|
|||
* Used to verify if all the properties needed to render the commit
|
||||
* author section were provided.
|
||||
*
|
||||
* TODO: Improve this! Use lodash _.has when we have it.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
hasAuthor() {
|
||||
|
@ -114,31 +115,30 @@
|
|||
</script>
|
||||
<template>
|
||||
<div class="branch-commit">
|
||||
<div
|
||||
v-if="hasCommitRef"
|
||||
class="icon-container hidden-xs">
|
||||
<i
|
||||
v-if="tag"
|
||||
class="fa fa-tag"
|
||||
aria-hidden="true">
|
||||
</i>
|
||||
<i
|
||||
v-if="!tag"
|
||||
class="fa fa-code-fork"
|
||||
aria-hidden="true">
|
||||
</i>
|
||||
</div>
|
||||
|
||||
<a
|
||||
v-if="hasCommitRef"
|
||||
class="ref-name hidden-xs"
|
||||
:href="commitRef.ref_url"
|
||||
v-tooltip
|
||||
data-container="body"
|
||||
:title="commitRef.name">
|
||||
{{commitRef.name}}
|
||||
</a>
|
||||
<template v-if="hasCommitRef && showBranch">
|
||||
<div
|
||||
class="icon-container hidden-xs">
|
||||
<i
|
||||
v-if="tag"
|
||||
class="fa fa-tag"
|
||||
aria-hidden="true">
|
||||
</i>
|
||||
<i
|
||||
v-if="!tag"
|
||||
class="fa fa-code-fork"
|
||||
aria-hidden="true">
|
||||
</i>
|
||||
</div>
|
||||
|
||||
<a
|
||||
class="ref-name hidden-xs"
|
||||
:href="commitRef.ref_url"
|
||||
v-tooltip
|
||||
data-container="body"
|
||||
:title="commitRef.name">
|
||||
{{commitRef.name}}
|
||||
</a>
|
||||
</template>
|
||||
<div
|
||||
v-html="commitIconSvg"
|
||||
class="commit-icon js-commit-icon">
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
<script>
|
||||
|
||||
/* This is a re-usable vue component for rendering a button
|
||||
that will probably be sending off ajax requests and need
|
||||
to show the loading status by setting the `loading` option.
|
||||
This can also be used for initial page load when you don't
|
||||
know the action of the button yet by setting
|
||||
`loading: true, label: undefined`.
|
||||
|
||||
Sample configuration:
|
||||
|
||||
<loading-button
|
||||
:loading="true"
|
||||
:label="Hello"
|
||||
@click="..."
|
||||
/>
|
||||
|
||||
*/
|
||||
|
||||
import loadingIcon from './loading_icon.vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
loadingIcon,
|
||||
},
|
||||
methods: {
|
||||
onClick(e) {
|
||||
this.$emit('click', e);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="btn btn-align-content"
|
||||
@click="onClick"
|
||||
type="button"
|
||||
:disabled="loading"
|
||||
>
|
||||
<transition name="fade">
|
||||
<loading-icon
|
||||
v-if="loading"
|
||||
:inline="true"
|
||||
class="js-loading-button-icon"
|
||||
:class="{
|
||||
'append-right-5': label
|
||||
}"
|
||||
/>
|
||||
</transition>
|
||||
<transition name="fade">
|
||||
<span
|
||||
v-if="label"
|
||||
class="js-loading-button-label"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
</transition>
|
||||
</button>
|
||||
</template>
|
|
@ -16,6 +16,11 @@ export default {
|
|||
required: false,
|
||||
default: 'primary',
|
||||
},
|
||||
closeKind: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'default',
|
||||
},
|
||||
closeButtonLabel: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -33,6 +38,11 @@ export default {
|
|||
[`btn-${this.kind}`]: true,
|
||||
};
|
||||
},
|
||||
btnCancelKindClass() {
|
||||
return {
|
||||
[`btn-${this.closeKind}`]: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -70,7 +80,8 @@ export default {
|
|||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
class="btn"
|
||||
:class="btnCancelKindClass"
|
||||
@click="emitSubmit(false)">
|
||||
{{closeButtonLabel}}
|
||||
</button>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
Sample configuration:
|
||||
|
||||
<user-avatar-image
|
||||
:lazy="true"
|
||||
:img-src="userAvatarSrc"
|
||||
:img-alt="tooltipText"
|
||||
:tooltip-text="tooltipText"
|
||||
|
@ -16,11 +17,17 @@
|
|||
*/
|
||||
|
||||
import defaultAvatarUrl from 'images/no_avatar.png';
|
||||
import { placeholderImage } from '../../../lazy_loader';
|
||||
import tooltip from '../../directives/tooltip';
|
||||
|
||||
export default {
|
||||
name: 'UserAvatarImage',
|
||||
props: {
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
imgSrc: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -56,18 +63,21 @@ export default {
|
|||
tooltip,
|
||||
},
|
||||
computed: {
|
||||
// API response sends null when gravatar is disabled and
|
||||
// we provide an empty string when we use it inside user avatar link.
|
||||
// In both cases we should render the defaultAvatarUrl
|
||||
sanitizedSource() {
|
||||
return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
|
||||
},
|
||||
resultantSrcAttribute() {
|
||||
return this.lazy ? placeholderImage : this.sanitizedSource;
|
||||
},
|
||||
tooltipContainer() {
|
||||
return this.tooltipText ? 'body' : null;
|
||||
},
|
||||
avatarSizeClass() {
|
||||
return `s${this.size}`;
|
||||
},
|
||||
// API response sends null when gravatar is disabled and
|
||||
// we provide an empty string when we use it inside user avatar link.
|
||||
// In both cases we should render the defaultAvatarUrl
|
||||
imageSource() {
|
||||
return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -76,11 +86,16 @@ export default {
|
|||
<img
|
||||
v-tooltip
|
||||
class="avatar"
|
||||
:class="[avatarSizeClass, cssClasses]"
|
||||
:src="imageSource"
|
||||
:class="{
|
||||
lazy,
|
||||
[avatarSizeClass]: true,
|
||||
[cssClasses]: true
|
||||
}"
|
||||
:src="resultantSrcAttribute"
|
||||
:width="size"
|
||||
:height="size"
|
||||
:alt="imgAlt"
|
||||
:data-src="sanitizedSource"
|
||||
:data-container="tooltipContainer"
|
||||
:data-placement="tooltipPlacement"
|
||||
:title="tooltipText"
|
||||
|
|
|
@ -11,8 +11,6 @@ import Dropzone from 'dropzone';
|
|||
import 'mousetrap';
|
||||
import 'mousetrap/plugins/pause/mousetrap-pause';
|
||||
|
||||
window.Dropzone = Dropzone;
|
||||
|
||||
//
|
||||
// ### Events
|
||||
//
|
||||
|
|
|
@ -5,8 +5,10 @@
|
|||
@import "framework/layout";
|
||||
|
||||
@import "framework/animations";
|
||||
@import "framework/vue_transitions";
|
||||
@import "framework/avatar";
|
||||
@import "framework/asciidoctor";
|
||||
@import "framework/banner";
|
||||
@import "framework/blocks";
|
||||
@import "framework/buttons";
|
||||
@import "framework/badges";
|
||||
|
|
|
@ -23,6 +23,11 @@
|
|||
@include webkit-prefix(animation-duration, 2s);
|
||||
}
|
||||
|
||||
&.spin {
|
||||
transform-origin: center;
|
||||
animation: spin 4s linear infinite;
|
||||
}
|
||||
|
||||
&.flipOutX,
|
||||
&.flipOutY,
|
||||
&.bounceIn,
|
||||
|
@ -198,6 +203,13 @@ a {
|
|||
height: 12px;
|
||||
}
|
||||
|
||||
&.animation-container-right {
|
||||
.skeleton-line-2 {
|
||||
left: 0;
|
||||
right: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
animation-duration: 1s;
|
||||
animation-fill-mode: forwards;
|
||||
|
@ -264,3 +276,9 @@ a {
|
|||
transform: translateX(468px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue